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.
|
# concrete rendered script so a draft_template can be proposed.
|
||||||
# Creates a persistent library artifact on accept, so Sonnet.
|
# Creates a persistent library artifact on accept, so Sonnet.
|
||||||
"template_extraction": "standard",
|
"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:
|
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_provider: Mapped[Optional[str]] = mapped_column(String(20), nullable=True) # "saml" | "oidc"
|
||||||
sso_config: Mapped[Optional[dict]] = mapped_column(JSONB, nullable=True)
|
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(
|
enabled_l1_categories: Mapped[list[str]] = mapped_column(
|
||||||
JSONB(), nullable=False,
|
JSONB(), nullable=False,
|
||||||
server_default=sa_text(
|
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