70 lines
3.1 KiB
Python
70 lines
3.1 KiB
Python
"""L1 category allowlist + the always-forbidden hard floor.
|
|
|
|
DEFAULT_L1_CATEGORIES seeds an account's enabled set. HARD_FLOOR_FORBIDDEN is a
|
|
category-independent safety floor the AI tree builder must never emit and admins
|
|
cannot enable. See spec §5.1/§5.2.
|
|
"""
|
|
from uuid import UUID
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.models.account import Account
|
|
|
|
# WARNING: keep in sync with Account.enabled_l1_categories server_default in
|
|
# app/models/account.py. The migration default (cb9e282267d2) is intentionally
|
|
# a frozen copy and is NOT updated when this list changes.
|
|
DEFAULT_L1_CATEGORIES: list[str] = [
|
|
"password_reset", "account_lockout", "printer", "email_outlook_client",
|
|
"wifi_network_basics", "vpn_connect", "teams_zoom_av",
|
|
"browser_cache_cookies", "peripheral_reconnect", "os_restart_update",
|
|
]
|
|
|
|
# Always-forbidden action classes (keys are stable identifiers; the human-readable
|
|
# phrasing lives in the builder system prompt). Admins cannot enable these.
|
|
HARD_FLOOR_FORBIDDEN: list[str] = [
|
|
"registry_edit", "system_file_or_boot_edit", "data_or_disk_deletion",
|
|
"credential_or_mfa_change", "security_or_av_or_firewall_change",
|
|
"elevated_or_admin_script", "domain_dns_dhcp_change",
|
|
"server_or_production_config", "billing_or_license_change",
|
|
]
|
|
|
|
# Substrings that, if present in a generated node's text, indicate a hard-floor
|
|
# violation. Used by ai_tree_builder per-node validation (defense in depth).
|
|
HARD_FLOOR_TEXT_PATTERNS: list[str] = [
|
|
"regedit", "registry", "format ", "delete partition", "diskpart",
|
|
"reset password for", "disable firewall", "disable antivirus", "disable defender",
|
|
"run as administrator", "sudo ", "domain controller", "dns record", "dhcp scope",
|
|
"uninstall security", "bitlocker",
|
|
]
|
|
|
|
|
|
def is_category_enabled(category: str, enabled: list[str]) -> bool:
|
|
"""A category is buildable only if explicitly enabled and not hard-floored."""
|
|
if category in HARD_FLOOR_FORBIDDEN:
|
|
return False
|
|
return category in enabled
|
|
|
|
|
|
async def get_enabled_categories(account_id: UUID, db: AsyncSession) -> list[str]:
|
|
"""Return the account's enabled L1 categories (``or []`` guards pre-default rows)."""
|
|
acct = (await db.execute(select(Account).where(Account.id == account_id))).scalar_one()
|
|
return list(acct.enabled_l1_categories or [])
|
|
|
|
|
|
async def set_enabled_categories(
|
|
account_id: UUID, categories: list[str], db: AsyncSession
|
|
) -> list[str]:
|
|
"""Persist the enabled set, dropping anything unknown or hard-floored.
|
|
|
|
Hard-floored keys (HARD_FLOOR_FORBIDDEN) are by design never present in
|
|
DEFAULT_L1_CATEGORIES, so the DEFAULT membership filter already excludes them.
|
|
If you ever add a key to DEFAULT_L1_CATEGORIES, verify it is not also in
|
|
HARD_FLOOR_FORBIDDEN. dict.fromkeys dedupes while preserving first-seen order.
|
|
"""
|
|
cleaned = list(dict.fromkeys(c for c in categories if c in DEFAULT_L1_CATEGORIES))
|
|
acct = (await db.execute(select(Account).where(Account.id == account_id))).scalar_one()
|
|
acct.enabled_l1_categories = cleaned
|
|
await db.flush()
|
|
return cleaned
|