Computer Use Agent Guards
Five guards in chio-guards work the CUA surface: a coarse allowlist of action types, a fine-grained gate on input injection, per-channel toggles for remote-desktop side channels, a browser-automation gate with credential detection, and a cosine-similarity anomaly detector. They compose conjunctively so a coarse pass does not weaken a fine-grained deny.
Verdict shape
Verdict::Allow or Verdict::Deny. The CUA surface does not produce Verdict::PendingApproval; approval is a separate guard. See Approval & HITL.ComputerUseGuard
Source: crates/chio-guards/src/computer_use.rs. Coarse gate that recognises three CUA surfaces:
- Remote-session and side-channel actions whose tool name or
action_type/custom_typeargument starts withremote.orinput.. - Browser navigation verbs (
navigate,goto,open) checked against a blocked-domain list and an optional allowed-domain list. - Screenshot-family verbs rate-limited via a token bucket (
screenshot,screen_capture,screen_shot,capture,capture_screen,browser_screenshot).
Config
ComputerUseConfig fields:
| Knob | Type | Default | Purpose |
|---|---|---|---|
enabled | bool | true | Master switch. Disabled guard returns Allow. |
allowed_action_types | Vec<String> | 10 entries (see below) | Allowlist for remote.* / input.* action types. |
mode | EnforcementMode | Guardrail | Observe / Guardrail / FailClosed. |
blocked_domains | Vec<String> | empty | Hosts denied for navigation. Supports *.suffix. |
allowed_domains | Vec<String> | empty | When non-empty, only listed hosts pass under FailClosed. |
screenshot_rate_per_second | Option<f64> | None | Refill rate. None disables rate limiting. |
screenshot_burst | Option<u32> | Some(5) when rate is set | Token-bucket burst capacity. |
Default allowed action types
pub fn default_allowed_action_types() -> Vec<String> {
vec![
"remote.session.connect".into(),
"remote.session.disconnect".into(),
"remote.session.reconnect".into(),
"input.inject".into(),
"remote.clipboard".into(),
"remote.file_transfer".into(),
"remote.audio".into(),
"remote.drive_mapping".into(),
"remote.printing".into(),
"remote.session_share".into(),
]
}Enforcement modes
The mode determines how the allowlist outcome maps to a verdict:
| Mode | In allowlist | Out of allowlist |
|---|---|---|
Observe | Allow | Allow (logged) |
Guardrail | Allow | Allow (warning) |
FailClosed | Allow | Deny |
Guardrail does not block
Guardrail mode is the default and never returns Deny on the action-type allowlist path. It emits a warning. Operators that want hard enforcement on this surface must set mode: fail_closed. The navigation and screenshot paths are stricter (Guardrail denies on a blocked domain or empty bucket).Detection algorithm
- Step 1: extract a CUA action type. Tool name itself if it begins with
remote.orinput.; otherwise anaction_type/actionType/custom_type/customTypeargument with the same prefix. If found, apply the mode mapping against the action-type allowlist. - Step 2: extract the action via
extract_action. If it isToolAction::BrowserAction, first match against the screenshot verb set; on a screenshot, consume a token from the bucket. On nav verbs, parse the host and check the blocked / allowed lists. - Step 3: anything else passes with
Verdict::Allow.
Failure modes
- Token-bucket mutex poisoning collapses to "no tokens" so the rate-limiter denies under Guardrail / FailClosed.
- Opaque navigation targets (CSS selectors, data URIs,
about:,javascript:) are passed through. Fine-grained domain checks belong toBrowserAutomationGuard. - Mixed-case schemes, scheme-relative URLs (
//host), userinfo segments (user@host), and bracketed IPv6 hosts all parse to the bare host so wildcards match correctly.
InputInjectionCapabilityGuard
Source: crates/chio-guards/src/input_injection.rs. Fine-grained gate that fires when tool_name == "input.inject" / "input_inject", when an action_type / custom_type argument equals input.inject, or when a recognised tool name (keyboard, mouse, touch, input) carries an input_type field.
Config
| Knob | Type | Default | Purpose |
|---|---|---|---|
enabled | bool | true | Master switch. |
allowed_input_types | Vec<String> | ["keyboard", "mouse", "touch"] | Permitted input_type values. |
require_postcondition_probe | bool | false | When true, requires a non-empty postcondition_probe_hash. |
strict | bool | true | Deny calls that look like injection but lack input_type. |
Failure modes
input_typeoutside the allowlist: Deny.- Missing
input_typeon an injection-flagged call withstrict = true: Deny; otherwise Allow. require_postcondition_probeset and the call is missing bothpostcondition_probe_hashandpostconditionProbeHash: Deny. Empty strings count as missing.
Postcondition probes bind blind action
require_postcondition_probe forces the agent to attest a hash (typically a screenshot digest) it will verify after the injection. The hash binds the action to a verification step, so the agent cannot inject keystrokes without committing to confirm the result.RemoteDesktopSideChannelGuard
Source: crates/chio-guards/src/remote_desktop.rs. Per-channel toggles for six named side channels, plus a transfer-size ceiling. Session-lifecycle actions (remote.session.connect, remote.session.disconnect, remote.session.reconnect) are not claimed here; they belong to ComputerUseGuard.
| Channel | Action type | Config field |
|---|---|---|
| Clipboard | remote.clipboard | clipboard_enabled |
| File transfer | remote.file_transfer | file_transfer_enabled |
| Session share | remote.session_share | session_share_enabled |
| Audio | remote.audio | audio_enabled |
| Drive mapping | remote.drive_mapping | drive_mapping_enabled |
| Printing | remote.printing | printing_enabled |
Every channel toggle defaults to true. The max_transfer_size_bytes knob is Option<u64> and defaults to None. When set, remote.file_transfer calls must include a transfer_size / transferSize u64 argument. Missing or non-integer transfer size with a configured ceiling: Deny.
Unknown remote.* channels deny
remote.webrtc action type the kernel has never seen returns Deny rather than silently passing. New side channels need a config update before they go live.BrowserAutomationGuard
Source: crates/chio-guards/src/browser_automation.rs. Three policy axes on ToolAction::BrowserAction:
- Verb allowlist. Empty means "any verb"; defaults to a read-only set.
- Navigation domain gating with separate blocked and allowed lists.
- Credential detection on type / input verbs via built-in regex patterns plus operator-supplied extras.
Default allowed verbs
pub fn default_allowed_verbs() -> Vec<String> {
vec![
"navigate".into(),
"goto".into(),
"open".into(),
"screenshot".into(),
"screen_capture".into(),
"capture".into(),
"browser_screenshot".into(),
"get_url".into(),
"get_title".into(),
"read".into(),
"get_content".into(),
"close".into(),
"back".into(),
"forward".into(),
"reload".into(),
]
}The default verb set is read-only. Verbs like type, click, fill, and submit_form are denied unless added explicitly. Operators that want a writeable session need to widen allowed_verbs and accept the credential-detection cost on every type call.
Config
| Knob | Type | Default | Purpose |
|---|---|---|---|
enabled | bool | true | Master switch. |
allowed_domains | Vec<String> | empty | When set, the only hosts the agent may navigate to. |
blocked_domains | Vec<String> | empty | Always denied. Evaluated before the allowlist. |
allowed_verbs | Vec<String> | 15-entry read-only set | Permitted action verbs. Empty means any verb. |
credential_detection | bool | true | Run regex detection on type / input action text. |
extra_credential_patterns | Vec<String> | empty | Operator regex patterns layered on top of built-ins. |
Built-in credential patterns
Compiled once via OnceLock. Cover AWS access keys (AKIA / ASIA + 16 chars), GitHub PATs (gh[pousr]_*), Slack tokens (xox[abopsr]-*), JWT shapes (eyJ*.*.* ), PEM private-key headers, generic password = ... / token = ... assignments, OpenAI sk-* keys, and Stripe sk_live_* / sk_test_* keys.
Selectors are not URLs
#, ., [, single /, or xpath= as CSS selectors and skips navigation gating on them. URLs like //host still parse and check.SpiderSenseGuard
Source: crates/chio-guards/src/spider_sense.rs. Cosine-similarity anomaly detector over a pre-loaded pattern database of known-threat embeddings.
Detection algorithm
- Extract a query embedding from the arguments. Preferred shapes:
embedding(array of numbers),vector, orembeddings(list of vectors, mean-pooled). - If no embedding is present: Allow.
- If the embedding has the wrong dimension or non-finite values: Deny.
- Score the request via cosine similarity against every pattern in the DB. Hand-rolled f64 accumulation, no BLAS dependency.
- Apply the threshold band: top score ≥
threshold + ambiguity_band: Deny. Top score ≤threshold - ambiguity_band: Allow. In-band: dispatch on the configuredambiguous_policy.
Config
| Knob | Type | Default | Purpose |
|---|---|---|---|
similarity_threshold | f64 | 0.85 | Center of the deny / allow band. |
ambiguity_band | f64 | 0.10 | Half-width of the band. |
top_k | usize | 5 | Top-K matches retained per query. |
ambiguous_policy | AmbiguousPolicy | Allow | Verdict for in-band scores. |
The pattern DB is loaded at construction time (PatternDb::from_json or SpiderSenseGuard::from_json_file). It must be non-empty, have a uniform non-zero dimensionality, and carry only finite values. Construction errors surface as SpiderSenseError::Parse, SpiderSenseError::Invalid, or SpiderSenseError::Config.
Evidence
Each PatternEntry carries an id, category (e.g. prompt_injection), stage (perception / cognition / action / feedback), a human-readable label, and the embedding vector. The guard scans the full DB for the maximum cosine; receipts emit the top scoring entry alongside the threshold and band.
Performance class
Cost ordering, lowest first:
| Guard | Hot path | Class |
|---|---|---|
RemoteDesktopSideChannelGuard | Set lookup + optional u64 compare | O(1) |
InputInjectionCapabilityGuard | HashSet lookup on input type | O(1) |
ComputerUseGuard | Action-type set + URL parse + token bucket | O(1) amortised |
BrowserAutomationGuard | Verb set + URL parse + 8 regex passes on type text | O(N regex) per type call |
SpiderSenseGuard | Cosine over the full pattern DB | O(N·D) |
HushSpec snippet
hushspec: "0.1.0"
guards:
cua:
computer_use:
enabled: true
mode: fail_closed
blocked_domains:
- "169.254.169.254"
- "*.internal.example"
allowed_domains:
- "*.corp.example"
screenshot_rate_per_second: 2.0
screenshot_burst: 5
input_injection:
enabled: true
allowed_input_types: ["keyboard", "mouse"]
require_postcondition_probe: true
strict: true
remote_desktop:
enabled: true
clipboard_enabled: false
file_transfer_enabled: true
session_share_enabled: false
audio_enabled: false
drive_mapping_enabled: false
printing_enabled: false
max_transfer_size_bytes: 1048576
browser_automation:
enabled: true
allowed_domains:
- "*.corp.example"
blocked_domains:
- "169.254.169.254"
allowed_verbs:
- "navigate"
- "screenshot"
- "get_content"
- "type"
credential_detection: true
spider_sense:
enabled: true
pattern_db_path: "/etc/chio/spider_sense_patterns.json"
similarity_threshold: 0.85
ambiguity_band: 0.10
top_k: 5
ambiguous_policy: denyNext steps
- Approval & HITL for human escalation on CUA actions that exceed automatic policy.
- Network Guards for the broader egress and SSRF posture that browser nav inherits.
- Advisory Guards for non-blocking observation of CUA patterns before promotion.