Skip to content

runtime: optimistic side-effect queue — simulate unapproved actions, bulk-approve later (Gatekeeper pattern) #317

Description

@NotASithLord

Motivation

Today the async-confirmation gate (peerd-runtime/tools/gates.js) is synchronous from the agent's point of view: a side-effecting tool call stops the loop until the user answers. That's the classic failure mode Cloudflare OS's README calls out — the user walks away, comes back to an agent stuck on the first approval, and eventually flips confirmation off entirely (the --dangerously-skip-permissions spiral). The safety mechanism trains users to disable it.

The pattern (from Cloudflare OS Gatekeepers)

When an action requires approval, the mediating layer:

  1. Queues the action with a human-readable description, and tells the agent it succeeded.
  2. Simulates its outcome, so subsequent reads reflect the pending write and the agent can keep working — even queuing dependent actions.
  3. The user approves or rejects in bulk later, at their convenience. Rejection may cascade (dependent actions) and may require restarting the workload if simulation state can't be rolled back.

Supporting contract details worth copying from packages/workshop-shared/src/gatekeeper.ts (github.com/cloudflare/cloudflare-os):

  • implementsRevert per action — approved-then-regretted actions get automated revert where possible; the UI only offers revert when the author says it's implementable.
  • awaitDecision hint — actions that can't be simulated declare it, and the harness suspends the turn as today. The mechanism is additive; blocking remains the fallback, not the default.
  • autoApprovable needs two signals — the author's per-action verdict AND a user-enabled rule for that action kind. Neither alone suffices. (Their drain loop also never applies past the first non-eligible action — ordering is preserved through a human gate.)

Mapping into peerd

Where it lives: the dispatcher's confirmation gate + pre/post tool-use hooks (gates.js); the checkpoint/review substrate already gives diff-and-revert for file-shaped mutations. Candidate surfaces, in rough order of tractability:

  1. Sandbox file/app/notebook writes — trivially simulable: apply to a checkpoint/shadow layer immediately (the agent reads its own pending writes back), commit on approve, discard on reject. Checkpoints already exist; this is mostly wiring + UI.
  2. dweb share / install — currently force-confirmed every time (tools/defs/dweb-share.js, dweb-install.js). Could become queued cards (share stays consent-heavy — publishing is irreversible — but the queue lets goal mode line up several and the user approve as a batch).
  3. Outbound messages (a2a ask/send) — queue with the rendered payload; inherently non-simulable responses ⇒ awaitDecision semantics when the agent needs the reply.
  4. Web-actor page mutations — hardest; true simulation of a third-party page isn't feasible. Likely stays blocking (awaitDecision) or limited to a described-intent queue for low-risk actions.

Shape of an action record: tool + args digest, human description, revertability, author auto-approvable verdict, lineage (already attached to every tool result). Batch approval UI in the side panel; every queue/approve/reject/revert event goes to the append-only audit log.

Cautions

  • Injection fencing: a queued action's description is rendered to the user and can embed page-derived text — must be wrapUntrusted-fenced like actor replies.
  • Plan/Act interplay: Plan mode already refuses mutations outright; the queue is an Act-mode feature. It must not become a Plan bypass (queuing in Plan = still refused).
  • Honest simulation: never simulate what can't be made true later (e.g. don't fabricate a peer's reply). When in doubt, awaitDecision.
  • Goal mode is the biggest beneficiary (long autonomous runs) and the biggest risk surface (large queues) — consider a queue-depth cap per session.

Related: #238 (reversible execution traces — same revert substrate, complementary mechanism).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    Status
    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions