Two-layer safety for every Flow B tool action. ScopeGate answers what may be touched (allow/deny asset rules, forbidden actions, third-party detection, rate limits). RiskController answers how it may be touched (destructive-pattern deny, dangerous-tool deny, action-category permission, task/command budgets, human-approval flag). Every ToolRouter.route and every AgentLoop cycle passes through both; cli run-task re-implements the same two-call sequence.
Per docs/safety-model.md, Flow B safety depends on these files — they are frozen (no feature edits).
Shared by Flow A only for the Path-B no-MCP Orchestrator branch (tools/autonomous_orchestrator.AttackModuleExecutor.scope_gate.check_scope) as its target-lock.
Hold allowed_assets/disallowed_assets as in-memory rule lists + forbidden_actions (unioned with _HARD_FORBIDDEN_ACTIONS deny-of-service/malware/social/physical etc., scope_gate.py:33) and rate_limits/_default_rps (scope_gate.py:99).
Reload from DB (scope_gate.py:146load_from_db: get_scope_rules → separates allow vs deny/action).
Provide rule engine: _rule_matches (domain exact, wildcard *., ip, cidr subnet_of, url_prefix via _url_prefix_matches), _classify_target_type (recognizes http(s):// as url_prefix), _is_third_party_asset (anchored regex + explicit cdn checks), _clean_asset (IPv6-safe, CIDR-preserving, host-only — never used for URL rules).
URL-prefix scope (_canonicalize_url / _url_prefix_matches): rules keep scheme + hostname (case-insensitive) + normalized port (default 80/443 folded away) + path, parsed with urllib.parse — never string splitting. Path comparison is directory-boundary safe (/admin authorizes /admin and /admin/..., never /administrator); query/fragment are ignored; malformed URLs fail closed. check_scope matches url_prefix rules against the raw asset while host rules keep using the normalized host, so host-level scope is unaffected.
Rate bucket: token-bucket _RateBucket (tokens refill at the configured RPS up to max(1, rate) burst; fractional rates honored, monotonic clock, never sleeps; denials report retry_after_seconds).
Gate high risk by profile: low_noise_non_destructive → deny; standard_authorized → allow but requires_human_approval=True; high_authorized_testing → no extra gate (risk_controller.py:255).
ScopeGate caches _allow_rules, _deny_rules, _forbidden_action_strs in memory; refreshed via load_from_db(). Rate buckets _rate_buckets: dict[str,_RateBucket] keyed by f"{asset}:{action_type[:20]}" are process-local token buckets (refilled by time.monotonic, capped at 10k entries with stalest-evicted).
RiskController holds _commands_executed / _tasks_completed counters in memory (incremented by ToolRouter after each successful execution and by AgentLoop after each complete).
No DB writes here except indirect audit via ToolRouter._log_block on deny.
flowchart TD
A[task: asset, phase, tool, risk_level] --> B[ScopeGate.check_scope]
B --> C{forbidden exact / substring?}
C -->|yes| X[deny: ScopeCheckResult.allowed=false]
C -->|no| D{third_party && not explicitly allowed?}
D -->|yes| X
D -->|no| E{deny rule matches?}
E -->|yes| X
E -->|no| F{allow rule matches?}
F -->|no| X
F -->|yes| G[_check_rate_limit token bucket]
G -->|exceeded| X
G -->|ok| H{high risk && profile != high_authorized_testing?}
H -->|yes| I[allow=true + requires_human_approval=true]
H -->|no| J[allow=true]
I --> K[RiskController.assess_action]
J --> K
K --> L{budget exhausted? pivot/exploit/credential not allowed?\ndestructive verb / sensitive overwrite?\ndangerous tool & profile=low?}
L -->|yes| Y[RiskAssessment.allowed=false]
L -->|no| M{high && profile == low -> deny;\n high && standard -> requires_human}
M --> N[RiskAssessment allowed=true + maybe requires_human]
X & Y --> Z[ToolRouter blocks -> audit tool_blocked]
N --> AA[ToolRouter -> human_approval_fn gate if needed -> execute]
These are the only Flow B authorization surfaces; removing either collapses safety. Must remain untouched for Flow A feature work (AGENTS.md §2).
risk_controller._DESTRUCTIVE_PATTERNS + _SENSITIVE_OVERWRITE_PATTERNS are load-bearing defense-in-depth even on high_authorized_testing — they are not relaxed by profile.
Flow A attack mode's single safety is the target-IP allowlist lock (tools/mcp_shared._allowed_target_list + tools/mcp_tools/terminal._target_lock_block), not these gates.