refactor: unify tool approval into a single engine and cut prompt storms
Collapse the dual permission stack into one policy. The runtime-side ToolPermissionResolver (own safe lists, own grant memory, bypassed the ApprovalEngine whenever it said ALLOW) is deleted; runtime_v2 now consults ApprovalEngine.predict(), a synchronous fast path reading the same config and the same persisted allowlist as the async authorize pipeline, so a grant given anywhere is honored everywhere. permissions.py keeps only a policy-free adapter; the duplicated permissions_v2 config fields and the runtime grant persistence loop are removed (stale YAML keys are ignored). New shell_safety module becomes the single source of truth for shell classification: flag-audited read-only commands (awk/od/jq/sed -n/diff/ git subcommand table/... auto-allow; find -delete, sort -o, curl -o/-d, rg --pre still prompt even when the bare name is config-listed), keyword-aware compound splitting (loop/branch headers no longer poison grants), expansion-safe $() handling, and fail-closed treatment of anything unparseable or substitution-bearing. Grant semantics are rebuilt around derived word-boundary prefixes: "python3 -c" instead of token bags, interpreter -c/-m kept in the prefix, bash/eval/sudo never grantable as prefixes, read-only segments exempt from the every-candidate-must-match rule so a granted command chained with ls/echo verification passes, and approve-once now records the exact candidates as a session grant so identical re-runs stop re-prompting. The authorize heuristic also audits the original command text instead of the quote-dropping preview (echo "<EOF>" no longer reads as redirection). Validated live on zz_perm_probe1 (native minimal org): awk/od/ls/cat/ sha256sum ran with zero cards, python3 -c parked once and three different python3 -c commands then passed via the persisted prefix grant, and an agent-issued rm -f compound correctly re-prompted showing only the segments needing approval. Full suite failures are byte-identical to the pre-change HEAD baseline (27 pre-existing). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+8
-56
@@ -691,23 +691,11 @@ class NativeSubagentProfileConfig(BaseModel):
|
||||
allowed_tools: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ClassifierThresholdsConfig(BaseModel):
|
||||
allow: float = 0.2
|
||||
ask: float = 0.5
|
||||
deny: float = 0.8
|
||||
|
||||
|
||||
class DenialMemoryConfig(BaseModel):
|
||||
enabled: bool = True
|
||||
repeat_threshold: int = 2
|
||||
|
||||
|
||||
class SandboxPolicyConfig(BaseModel):
|
||||
treat_network_as_risky: bool = True
|
||||
treat_external_paths_as_high_risk: bool = True
|
||||
explicit_prefix_allowlist: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class GuardianConfig(BaseModel):
|
||||
enabled: bool = True
|
||||
auto_allow_read_only: bool = True
|
||||
@@ -717,57 +705,21 @@ class GuardianConfig(BaseModel):
|
||||
|
||||
|
||||
class PermissionsV2Config(BaseModel):
|
||||
"""Runtime knobs for the unified permission predictor (ApprovalEngine.predict).
|
||||
|
||||
Shell safe-command policy lives in ``autonomy.safe_command_prefixes`` plus
|
||||
the built-in flag-audited classifier (``shell_safety.py``); legacy
|
||||
duplicate fields (safe_shell_prefixes, classifier_*, sandbox_policy, ...)
|
||||
from the removed runtime-side resolver are ignored on load.
|
||||
"""
|
||||
|
||||
enabled: bool = True
|
||||
fail_closed: bool = True
|
||||
classifier_enabled: bool = True
|
||||
shell_ast_validation: bool = True
|
||||
llm_classifier_model: str = ""
|
||||
classifier_thresholds: ClassifierThresholdsConfig = Field(default_factory=ClassifierThresholdsConfig)
|
||||
denial_memory: DenialMemoryConfig = Field(default_factory=DenialMemoryConfig)
|
||||
sandbox_policy: SandboxPolicyConfig = Field(default_factory=SandboxPolicyConfig)
|
||||
candidate_extractors: list[str] = Field(default_factory=lambda: [
|
||||
"path",
|
||||
"file_path",
|
||||
"directory",
|
||||
"working_directory",
|
||||
"target_output_dir",
|
||||
"workspace_path",
|
||||
"command",
|
||||
"cmd",
|
||||
"url",
|
||||
])
|
||||
default_scope: str = "once"
|
||||
allow_scopes: list[str] = Field(default_factory=lambda: ["once", "session", "project", "global"])
|
||||
allow_tools: list[str] = Field(default_factory=list)
|
||||
deny_tools: list[str] = Field(default_factory=list)
|
||||
allowed_paths: list[str] = Field(default_factory=list)
|
||||
denied_paths: list[str] = Field(default_factory=list)
|
||||
safe_shell_prefixes: list[str] = Field(default_factory=lambda: [
|
||||
"ls",
|
||||
"pwd",
|
||||
"echo",
|
||||
"rg",
|
||||
"git status",
|
||||
"git diff",
|
||||
"curl",
|
||||
"wget",
|
||||
"yt-dlp",
|
||||
"aria2c",
|
||||
"ffmpeg",
|
||||
"python -V",
|
||||
"python3 -V",
|
||||
"node -v",
|
||||
"npm -v",
|
||||
])
|
||||
ask_shell_prefixes: list[str] = Field(default_factory=lambda: [
|
||||
"git commit",
|
||||
"git push",
|
||||
"npm install",
|
||||
"pip install",
|
||||
"pnpm install",
|
||||
"cargo test",
|
||||
"pytest",
|
||||
])
|
||||
guardian: GuardianConfig = Field(default_factory=GuardianConfig)
|
||||
dangerous_shell_patterns: list[str] = Field(default_factory=lambda: [
|
||||
r"\brm\s+-rf\b",
|
||||
|
||||
Reference in New Issue
Block a user