Skip to content

Fix two observer-verification gaps - #306

Draft
Maximo-Guk wants to merge 4 commits into
mainfrom
observer-verification-fixes
Draft

Fix two observer-verification gaps#306
Maximo-Guk wants to merge 4 commits into
mainfrom
observer-verification-fixes

Conversation

@Maximo-Guk

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

Copy link
Copy Markdown
Member

WIP

@github-actions github-actions Bot added the kernel Changes to the Workshop kernel label Aug 23, 2026
@Maximo-Guk
Maximo-Guk marked this pull request as draft August 23, 2026 04:14
@github-actions

Copy link
Copy Markdown

Preview: pr306-observer-veri-775f79b7

https://pr306-observer-veri-775f79b7-router.cloudflare-os-previews.workers.dev

Dashboard · deleted when this PR closes

Maximo-Guk and others added 3 commits August 23, 2026 11:18
Observer verification only ever ran in open(). receiveExternalMessage
authorized on the effective role alone, so a build collaborator could drive
the agent -- and read its replies -- without having been verified against
anything the workspace has read. A collaborator added directly, who never
opened the workspace in a browser, was never verified at all; one whose
verification had failed in the browser kept working through this path.

The fix lands as authorizeCollaborator, a single role + observer-verification
gate on the Overseer: resolve the effective role, deny below the caller's
`requireRole` floor before verification runs (so a "use" caller gets the plain
denial rather than being verified for access this path can never grant), then
run the same ensureObserver check open() applies. receiveExternalMessage
routes through it non-interactively and tells an unverified caller to open the
workspace instead. open() still runs the same steps inline; migrating it onto
the gate is left to the share-key redemption rework that has to restructure
that path anyway.

Independent of the restricted-data work that follows: it applies to any
workspace with observer-verified gatekeepers, and is reachable only on
deployments that bind an external message gateway.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ensureObserver loads the observer record, awaits verifier RPCs (and possibly
the configuration modal, which parks on user input indefinitely), then persists
the record. Input gates don't cover those awaits, so two concurrent opens for
one profile raced: two first opens each minted their own observerId,
registering both with the gatekeepers while the last-written record forgot the
other id existed, and a later open's final put could overwrite state a
concurrent open had just written.

A per-profile promise chain now serializes the whole body, following the
existing #preparingChatMessages pattern. blockConcurrencyWhile is not usable
here: it would freeze the entire DO for an unbounded modal wait. Distinct
profiles stay concurrent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Exercises the authorizeCollaborator gate end-to-end through the real
ExternalMessageGateway entrypoint: an unverified build collaborator is refused
until they open the workspace (which verifies them), and a "use" collaborator
is denied by role before verification ever runs.

The fixture worker grows a control surface for submitting external messages
(a service binding to the Workshop's gateway entrypoint, plus an Overseer
namespace binding used only to derive the workspace id behind a gadgetKey),
and its sessions become real: readThing()/doThing() drive observations and
actions through the same ApprovalQueue funnel a shipping gatekeeper uses.

Also hardens the harness against local-dev leakage: worker configs declare an
empty required-secrets list so a developer's .dev.vars (say CF_AI_GATEWAY_*)
can't change suite behavior -- these tests depend on no test user having an
AI model.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Maximo-Guk
Maximo-Guk force-pushed the observer-verification-fixes branch from 0f52f50 to 6779242 Compare August 23, 2026 16:33
A first-time ensureObserver registers its freshly minted observerId with
gatekeepers (addObserver) before the observer record -- and with it the
byObserverId reverse index -- is persisted, and the window in between spans
awaits (sibling verifier RPCs, even the unbounded configuration modal). A
gatekeeper that already accepted the registration may name that id in an
observation's excludeObservers; #enforceExcludeObservers resolved it via
byObserverId, found nothing, and read it as "not an active observer ->
ignore" -- the observation proceeded, and the collaborator was admitted
moments later with the data already in chat history.

Track such ids in an in-memory #pendingObserverIds map for the duration of
the registration (set on mint, deleted in a finally that also covers the
step-6 put, so there is no gap where neither the map nor the index resolves
the id), and have #enforceExcludeObservers fail closed on them with a
distinct "collaborator currently being verified" message. In-memory is the
right scope: a DO restart kills the in-flight open, and its gatekeeper-side
registration then references an id no record will ever carry, so ignoring
it is correct. Re-verification is unaffected -- it reads the observerId
from the persisted record, which the index already resolves.

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

Labels

kernel Changes to the Workshop kernel

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant