Skip to content

Security boundary: UGC-zone read-only downscaling + origin-pinning + clean write hand-off #242

Description

@NotASithLord

Security-boundary arc, item 2 of 4. Depends on / extends #237 (per-origin site-clients). Sibling issues: deterministic-schema return path, PreToolUse egress firewall, CDR + tainting.

Problem

The web actor holds both read and mutating tools in one instance (tools/exposure.js ACTOR_TYPE_TOOLS.web: reads = snapshot/read_page/read_state/query_dom/read_pdf/view; mutations = click/type/navigate/page_keys/site_client_*). On a trusted site hosting untrusted content (GitHub issues/PRs, Jira/Linear, Google Docs, feeds), an indirect injection can hijack the actor while it holds the tools to act within your authenticated session — the confused-deputy residual that no return-path boundary closes, because the damage happens in-tab before any reply.

Confirmed mitigations already in place: the web actor cannot open or address a new tab (gate-enforced — open_tab is main-only, actorTierGate refuses tabId != owned). But its one owned tab is freely cross-origin re-navigable (only http/https + denylist + confirm), so the real gap is origin-pinning, not just tab-pinning.

The affirmative model (owner's direction)

A web actor is pinned to ONE origin and is read-only by default in a UGC zone; mutations require a separate, single-purpose write actor spawned after the read output has crossed the sanitation boundary (the schema-validated return path, item 1). The read actor ingests untrusted content and never holds mutation authority; the write actor is minted with a narrow, explicit intent and never sees the raw page.

This is also where origin-pinned site-clients (#237) pay off: the actor accretes a deterministic, reusable navigation client scoped to its one origin, so acting becomes deterministic code rather than model-in-the-loop DOM mutation.

Where it lands

  • A UGC trust registry in the SW: URL/path patterns → UGC_ZONE (issue trackers, docs, feeds on trusted-but-untrusted-content hosts). Never a channel probe; classification is deterministic.
  • A surface:'read' variant in tools/exposure.js actorAllowedToolsFor(kind, backing, surface) (mirrors the existing tools/code split), enforced at the same actorTierGate wall that already does the tab pin. A UGC-zone spawn drops the mutating tools.
  • Origin-pinning: extend the tab pin to refuse cross-origin navigation of the owned tab in a UGC zone (or re-mint on cross-origin, same discipline as the egress boundary's origin checks).
  • The clean hand-off: read actor → schema-validated JSON → orchestrator → a distinct write actor for the mutation, if any.

Open questions

  1. Registry as a static pattern list vs. a heuristic (a page with a comment box + third-party authorship = UGC). Leaning: static list for v1, extensible.
  2. Downscale scope — all mutations, or a graduated posture (reads free, same-origin GET-shaped actions free, mutations hand-off). Ties to Adaptive per-origin action pacing (ANTI-BOT-POSTURE Option 0 resolution) #234 (pacing) posture ladder.
  3. UX of the hand-off — silent (the orchestrator re-delegates) vs. a visible "this needs a write action" step.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    Status
    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions