Approval
Capability-domain approval engine for browser, desktop, desktop automation, PIM, and media capture actions.
The approval module provides an action-typed capability-domain approval engine covering browser, desktop, desktop automation, PIM write, and media capture. It is a layer distinct from the action-aware exec approval gate at src/sandbox/exec_approval/, which covers shell / tool calls and is enforced at src/tools/scoped/.
Design Philosophy
- Default deny — Without a matching rule or default, every request falls back to
Ask - Fail closed — A missing or unparseable policy file yields
safe_default(everythingAsk);Default::default()andsafe_defaultnever disagree - Configuration-driven — Rules load from
~/.aleph/approval-policy.json, not from source
Architecture
ActionRequest ──▶ ApprovalPolicy::check() ──▶ ApprovalDecision
│
│ (blocklist → allowlist → defaults → ask)
▼
ConfigApprovalPolicyCore Types
ActionRequest
Describes the action an agent wants to perform:
pub struct ActionRequest {
pub action_type: ActionType,
pub target: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub display_target: String,
pub agent_id: String,
pub context: String,
pub timestamp: DateTime<Utc>,
}ActionType
The legitimate request categories by capability domain:
pub enum ActionType {
BrowserNavigate,
BrowserClick,
BrowserType,
BrowserFill,
BrowserEvaluate,
DesktopClick,
DesktopType,
DesktopKeyCombo,
DesktopLaunchApp,
DesktopAutomation,
PimWrite,
MediaCapture,
}ApprovalDecision
The outcome of a policy check:
pub enum ApprovalDecision {
Allow,
Deny { reason: String },
Ask { prompt: String },
}Policy Implementation
ConfigApprovalPolicy
The primary policy implementation that loads rules from JSON configuration:
pub struct ConfigApprovalPolicy { /* ... */ }
impl ConfigApprovalPolicy {
pub fn new(config: PolicyConfig) -> Self;
pub fn load() -> Self;
pub fn load_from(path: PathBuf) -> Self;
}
#[async_trait]
impl ApprovalPolicy for ConfigApprovalPolicy {
async fn check(&self, request: &ActionRequest) -> ApprovalDecision;
async fn record(&self, request: &ActionRequest, decision: &ApprovalDecision);
}Decision flow (in order):
- Blocklist — if the target matches any
blocklistentry for thisActionType⇒Deny - Allowlist — if the target matches any
allowlistentry for thisActionType⇒Allow - Defaults — look up the per-
ActionTypeDefaultDecisionindefaults - Fallback — no match ⇒
Ask
Fail closed: when ~/.aleph/approval-policy.json is missing, unreadable, or fails to parse, load_from returns safe_default (empty allowlist / empty blocklist / empty defaults) so every action becomes Ask. Default::default() and safe_default no longer disagree.
PolicyRule
pub struct PolicyRule {
#[serde(rename = "type")]
pub action_type: ActionType,
pub pattern: String,
}PolicyConfig top-level shape:
pub struct PolicyConfig {
pub defaults: HashMap<ActionType, DefaultDecision>,
#[serde(default)] pub allowlist: Vec<PolicyRule>,
#[serde(default)] pub blocklist: Vec<PolicyRule>,
}Glob Matching
Patterns use glob syntax with path-safe semantics:
pub fn matches_glob(value: &str, pattern: &str) -> bool;*— matches any characters except/(including newline)**— matches any path segment, including across/and across newlines?— matches any single character except/
The ? pattern compiles to [^/] internally so a ** block rule cannot be evaded by a multi-line target. Compiled regular expressions are cached in a process-wide bounded cache so the hot path never re-compiles.
Both check and record route target through redact_target before writing a tracing event — the field becomes <redacted len=… sha=…> so clipboard text, PIM bodies, and script bodies cannot leak into logs.
Configuration File
Policies load from a JSON file whose path ConfigApprovalPolicy::config_path() resolves:
{
"defaults": {
"browser_navigate": "allow",
"browser_evaluate": "ask",
"desktop_automation": "ask",
"pim_write": "ask"
},
"allowlist": [
{ "type": "browser_navigate", "pattern": "https://*.github.com/**" }
],
"blocklist": [
{ "type": "browser_navigate", "pattern": "**/secrets/**" }
]
}DefaultDecision serializes in lowercase ("allow" / "deny" / "ask"); invalid values are rejected at parse time.
Guardian Judge
src/approval/guardian_requester.rs::GuardianApprovalRequester is an LLM risk reviewer that wraps the human approval requester. Its verdict semantics:
risk == "low"andallow == true— auto-approved, human is skipped- Anything else — escalates to the wrapped human requester
The Guardian never denies on its own authority and never widens what a human would have been asked about. Code parse failures, provider errors, the 30-second timeout, and the provider circuit breaker (three consecutive failures trigger a five-minute cooldown) all escalate to the human. The reviewer consumes src/exec/masker.rs::SecretMasker and masks both the summary and per-segment raw text, so secrets are never serialized into the verdict payload and never reach the audit trail.
The verdict payload carries the stable GUARDIAN_SYSTEM rubric as a cache: true SystemPromptPart, letting Anthropic-compatible providers reuse the prompt cache across approvals (the per-action payload is the only dynamic tail).
The wiring lives at bin/aleph-server/commands/start/mod.rs::set_requester and is enabled only when [policies] guardian_review = true and a default provider is present.
Discord Interactive Approval
src/approval/callback_sink.rs::ApprovalCallbackSink provides a two-way, cross-channel stream — mcp.respond_approval / mcp.cancel_approval lets Discord buttons share a single pending-approval seat pool with TUI and Panel forms.
Relationship to the Exec Approval Gate
src/approval/ and the exec approval gate are two separate layers:
src/approval/— capability domain (older): browser / desktop / PIM / automation / media capture decisionssrc/sandbox/exec_approval/— action-aware (newer): shell commands / tool-call fingerprint, denial ledger, and the Ask / Auto / Full tier
The exec gate forces every tool through a single chokepoint at src/tools/scoped/. The Ask / Auto / Full tiers live at src/config/types/policies/exec_tier.rs, read declared ToolFacts metadata, and fail closed for unknown tools in the ask tier. No execution bypasses src/tools/scoped/.
Code Location
src/approval/mod.rs— module entry and public exportssrc/approval/types.rs—ActionRequest/ActionType/ApprovalDecision/DefaultDecisionsrc/approval/policy.rs—ApprovalPolicytraitsrc/approval/config.rs—ConfigApprovalPolicy/PolicyConfig/ glob matching / cache /safe_defaultsrc/approval/guardian_requester.rs— Guardian Judge triagesrc/approval/callback_sink.rs— cross-channel approval callback streamsrc/approval/adapters.rs— builtin tool adapters
See Also
- Configuration — config file and defaults
- Security Primitives — SSRF, content sanitization, runtime guard, audit log
- Secret Management — encrypted credential storage and the
secrets.*RPC