Skip to content

Allow restricted data writes to its own restricted gatekeeper - #310

Draft
Maximo-Guk wants to merge 13 commits into
restricted-data-followupsfrom
restricted-data-writes-to-self
Draft

Allow restricted data writes to its own restricted gatekeeper#310
Maximo-Guk wants to merge 13 commits into
restricted-data-followupsfrom
restricted-data-writes-to-self

Conversation

@Maximo-Guk

@Maximo-Guk Maximo-Guk commented Aug 24, 2026

Copy link
Copy Markdown
Member

WIP ( DO NOT MERGE )

@github-actions github-actions Bot added kernel Changes to the Workshop kernel workshop/shared Changes to shared Workshop APIs labels Aug 24, 2026
@github-actions

Copy link
Copy Markdown

Preview: pr310-restricted-da-d8e7843e

https://pr310-restricted-da-d8e7843e-router.cloudflare-os-previews.workers.dev

Dashboard · deleted when this PR closes

@github-actions github-actions Bot added workshop/frontend Changes to the Workshop frontend gatekeeper Changes to a gatekeeper integration labels Aug 24, 2026
@Maximo-Guk
Maximo-Guk force-pushed the restricted-data-writes-to-self branch from 18f44d4 to 2ac9a92 Compare August 24, 2026 04:01
@Maximo-Guk
Maximo-Guk force-pushed the restricted-data-writes-to-self branch from 2ac9a92 to 513e542 Compare August 24, 2026 04:46
darrinm added a commit to darrinm/cloudflare-os that referenced this pull request Aug 24, 2026
Opening a Bot crashes the page: hook after an early return (cloudflare#310)
@Maximo-Guk
Maximo-Guk force-pushed the restricted-data-writes-to-self branch from 513e542 to 7be8dca Compare August 24, 2026 13:45
@Maximo-Guk
Maximo-Guk force-pushed the restricted-data-writes-to-self branch from 7be8dca to dcf5c50 Compare August 24, 2026 14:27
@Maximo-Guk
Maximo-Guk force-pushed the restricted-data-writes-to-self branch from dcf5c50 to ae7a3ff Compare August 24, 2026 16:10
@Maximo-Guk
Maximo-Guk force-pushed the restricted-data-writes-to-self branch from ae7a3ff to 83d06ba Compare August 24, 2026 19:19
Maximo-Guk and others added 4 commits August 24, 2026 16:03
The restricted-data latch used to block every action once any sensitive
(containsRestrictedData) observation occurred. Carve out writes-to-self:
a latched workspace may still submit actions targeting the connections
that produced its restricted data -- sending the data back where it came
from reveals nothing new to that system -- while any other target, and
all web fetches, stay blocked.

Provenance is derived, not persisted: restrictedProducerIds() already
scans the never-deleted action log for restricted observations (under
either of the flag's names), with the invariant that the latch and the
producing record are written in one synchronous block, so a latched
workspace always yields a non-empty set. That makes the earlier design's
persisted producer-set singleton and its backfill migration unnecessary
-- the same scan IS the backfill, run on demand. If the set is ever
empty under the latch anyway, every target fails the membership check
and all actions are refused (conservative fallback).

Latched actions are never auto-approved: the human approving each write
is the interim mitigation for hidden-instruction injection riding in the
restricted data. The auto-approval gate, previously duplicated between
submitAction and the drainer, is factored into one exported predicate
autoApprovalRule() -- author verdict + user rule + not latched.
setAutoApprovedActionKind refuses to store a rule while latched (it
would never fire) and listPreApprovableActions offers nothing.

Accepted residual: with producers {A, B}, data observed from A may be
written back to B -- the carve-out is set-based, not per-producer. Every
such write is human-approved, and gatekeeper.ts's TODO(someday) already
flags the restricted mode's bluntness.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The fixture gatekeeper's doThing() now tags its poke with an actionKind
and takes a per-action autoApprovable verdict, getAutoApprovableActions
offers the kind, and applyAction succeeds, so tests can drive the real
submit -> auto-approve -> apply round trip.

The post-latch action test flips by design: the actor IS the producer,
so its doThing() now pends (writes-to-self) while a second connection on
the same account is still refused. New auto-approval-policy tests cover
the happy auto-approve path and that a pre-latch rule stops firing once
the workspace reads sensitive data -- the action pends, the rule surface
refuses, and manual approval remains the path through.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…warnings

Gatekeeper-authored warnings addressed to the human approver, for context
the action's own content can't show -- e.g. "this conversation has read
data from other accounts, which could leak into this write."

- New optional `ActionDescription.operatorWarnings: string[]`.
- A warning exists precisely to be read by a human, so autoApprovalRule()
  refuses any action carrying one, even with the author's verdict and a
  matching user rule; the drainer therefore stops at a warned action
  without skipping ahead.
- Both approval surfaces (chat action card and the Activity panel) render
  the warnings as a prominent warning strip ahead of the description, and
  suppress the always-approve affordance on warned actions.
- Unit tests for the predicate and drainer gates; integration test drives
  a warned poke through the real submit -> pend -> manual approve -> drain
  round trip via the test gatekeeper fixture.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Brings in the MCP portal hidden-servers passthrough
(MCP_PORTAL_HIDDEN_SERVER_IDS), which downstream deployments already
pin, so a submodule pointing at this branch is a strict superset of
that pointer rather than a rewind.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Maximo-Guk
Maximo-Guk force-pushed the restricted-data-writes-to-self branch from 83d06ba to fef8519 Compare August 24, 2026 21:06
Maximo-Guk and others added 2 commits August 24, 2026 17:53
An in-flight facet RPC can outlive removeGatekeeper (the same race the
restricted-observation refusal in authorizeObservation guards), so
submitAction could persist a pending action naming a removed connection.
Such a record can never be resolved: approveAction and rejectAction both
dereference it through getGatekeeperFacet, which throws, and an
awaitDecision agent turn then suspends forever.

Require a live gatekeeper record before any write (refused ahead of the
id allocation, so no record is left behind). This also closes the latched
case: restrictedProducerIds deliberately survives removal, so carve-out
set membership alone must not admit a write to a dead connection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The carve-out's prior rationale ("sending the data back where it came
from reveals nothing new") over-claims for the multi-producer and
broad-connection cases: the check is per-connection set membership, not
data provenance, so with producers {A, B} data observed from A may be
written back to B, and one broad connection can reach audiences beyond
where the data was read. State that residual in the
containsRestrictedData JSDoc and at the carve-out itself, naming the
mandatory human approval of every latched action as the interim
mitigation. The existing TODO(someday) policy framework remains the
full fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Maximo-Guk and others added 4 commits August 24, 2026 17:59
A latched workspace never auto-approves (autoApprovalRule refuses,
setAutoApprovedActionKind throws, listPreApprovableActions returns
nothing), but the UI still offered "Always approve" on pending actions
-- which could only error -- and rendered pre-latch rules as applying.

Thread the live GadgetMetadata.containsRestrictedData (already pushed by
the metadata subscription) into ChatInterface and Activity: restricted
workspaces no longer offer the always-approve affordance in either
surface, and the auto-approvals panel annotates each rule as suspended
("won't apply -- actions always require manual approval") with enabling
blocked while disabling stays possible, so a standing grant remains
revocable. Presentation-only: the backend gates are unchanged, and rules
are kept rather than cleared so revocation history stays intact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gatekeeper Changes to a gatekeeper integration kernel Changes to the Workshop kernel workshop/frontend Changes to the Workshop frontend workshop/shared Changes to shared Workshop APIs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants