feat(l1): category service (defaults + hard floor) and AI action keys
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -211,6 +211,10 @@ class Settings(BaseSettings):
|
||||
# concrete rendered script so a draft_template can be proposed.
|
||||
# Creates a persistent library artifact on accept, so Sonnet.
|
||||
"template_extraction": "standard",
|
||||
# L1 AI tree builder (Phase 2A): per-node generation is latency-sensitive
|
||||
# on a live call → Sonnet; classification is a short label task → Haiku.
|
||||
"l1_realtime_build": "standard",
|
||||
"l1_classify": "fast",
|
||||
}
|
||||
|
||||
def get_model_for_action(self, action_type: str) -> str:
|
||||
|
||||
@@ -67,7 +67,9 @@ class Account(Base):
|
||||
sso_provider: Mapped[Optional[str]] = mapped_column(String(20), nullable=True) # "saml" | "oidc"
|
||||
sso_config: Mapped[Optional[dict]] = mapped_column(JSONB, nullable=True)
|
||||
|
||||
# L1 AI tree builder — per-account allowlist of problem categories
|
||||
# L1 AI tree builder — per-account allowlist of problem categories.
|
||||
# Keep this server_default in sync with DEFAULT_L1_CATEGORIES in
|
||||
# app/services/l1_category_service.py when adding/removing categories.
|
||||
enabled_l1_categories: Mapped[list[str]] = mapped_column(
|
||||
JSONB(), nullable=False,
|
||||
server_default=sa_text(
|
||||
|
||||
69
backend/app/services/l1_category_service.py
Normal file
69
backend/app/services/l1_category_service.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""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
|
||||
16
backend/tests/test_l1_category_service.py
Normal file
16
backend/tests/test_l1_category_service.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from app.services.l1_category_service import (
|
||||
DEFAULT_L1_CATEGORIES, HARD_FLOOR_FORBIDDEN, is_category_enabled,
|
||||
)
|
||||
|
||||
|
||||
def test_defaults_and_hard_floor_present():
|
||||
assert "password_reset" in DEFAULT_L1_CATEGORIES
|
||||
assert "registry_edit" in HARD_FLOOR_FORBIDDEN # representative forbidden action key
|
||||
assert len(DEFAULT_L1_CATEGORIES) == 10
|
||||
|
||||
|
||||
def test_is_category_enabled():
|
||||
enabled = ["printer", "vpn_connect"]
|
||||
assert is_category_enabled("printer", enabled) is True
|
||||
assert is_category_enabled("registry_edit", enabled) is False
|
||||
assert is_category_enabled("unknown", enabled) is False
|
||||
Reference in New Issue
Block a user