Skip to content

migrate the context gatekeeper to kernel-owned observer tracking - #896

Draft
maxwellpeterson wants to merge 7 commits into
mpeterson/single-gadget-sharingfrom
mpeterson/context-observer-tracking
Draft

maxwellpeterson wants to merge 7 commits into
mpeterson/single-gadget-sharingfrom
mpeterson/context-observer-tracking

Conversation

@maxwellpeterson

@maxwellpeterson maxwellpeterson commented Oct 26, 2026 •

Copy link
Copy Markdown
Member

Stacked on #894 (and #881). Draft until those land.

Why

#881 moved observer bookkeeping into the overseer, but no gatekeeper uses it yet. This migrates the Context Library first.

Changes

Context gatekeeper (packages/gatekeeper-context)

  • ContextGatekeeper.describe() sets supportsObserverTracking. Every read goes through authorizeTrackedObservation() and reports the collections whose contents it revealed as subresources, one collection ID each. The binding only ever reads its own sharing domain, so the ID needs nothing else. Disclosure boundaries are unchanged:
    • a search reports only the collections it returned results from;
    • a collection listing or document read reports that collection;
    • the top-level collection listing reports [], because its titles and counts are catalog metadata.
  • ContextGatekeeper.verifyObserverAccess(observers, subresources) asks each observer's ContextVerifier.hasCollectionsAccess() about the collections. It allows only observers whose account can read every one now; a ragged answer allows nothing. The verifier itself is unchanged from main, and nothing is cached, so revocations apply at the next check.
  • getHistoricalSubresources() exports every observedCollection: marker the gatekeeper-kit tracker left in the binding's storage. Pending markers are included, since their read may have been disclosed. The export is read-only, so the Workshop can ask again if the import doesn't commit.
  • addObserver() throws. That refusal is one of the two ways the Workshop learns the gatekeeper has upgraded. removeObserver() retires the old tracker's registrations through the kit's ObserverTracker.removeObserver(), which is what retireLegacyObserverRegistrations asks for.
  • The kit strategy, the ObservationGate wiring and CONTEXT_OBSERVER_DENIED are removed. The Workshop's own denial is used, and it never names the collection.

Integration tests (separate commit)

  • New context-observer-upgrade.test.ts. It boots gatekeeper-context as built at main fe3ce9fc3 (legacy tracking), and two workspaces read a private collection each. It then upgrades the Worker in place with its storage kept, and checks both ways the Workshop can learn of the upgrade:
    • Admission: with no read since the upgrade, a collaborator's open hits the throwing addObserver(). They are refused with the Workshop's denial, which doesn't name the collection, and the action log shows "Imported earlier reads". Once the collection is everyone-viewable they are admitted, and the connection keeps reading as a tracked connection.
    • Read: the owner's top-level collection listing goes through authorizeTrackedObservation(), which switches that connection. The listing reports nothing, so only the imported record can refuse the collaborator, and it does.
  • If getHistoricalSubresources() returns [], both refusals fail; each was checked locally on its own.
  • build-upgrade-base.ts now builds a list of bases (Google plus Context). The redeploy-and-wait logic moved from google-fake.ts into harness.ts as redeployWorker(); Google's upgrade suite uses it unchanged.
  • context-sharing.test.ts uses shared fixtures (context-fixtures.ts). It now also checks that a refused collaborator is admitted once the collection becomes readable to them.

Docs: the Context row and §10 opener in docs/observers.md, the observer section of plans/context-collection-sharing.md, the integration-testing upgrade notes, and the write-gatekeeper skill, which now points at Context as the first shipping example.

Behavior notes

  • Collection reads recorded before the upgrade aren't attributed to any chat or gadget, so the kernel imports them into the workspace context. Gadgets that shared the connection stay coupled through that history; reads after the switch are attributed per chat or gadget.
  • Deploy order: this needs the add per-chat and per-gadget observation tracking #881 kernel. Against an older Workshop, authorizeTrackedObservation() doesn't exist, so reads fail closed instead of being disclosed unchecked. Tracking is never downgraded, so rolling Context back to a legacy build after a workspace has switched makes the kernel refuse that connection's legacy reads.

Verification

  • pnpm --filter @gadgets/gatekeeper-context test:run: 94 workerd + 95 node tests pass.
  • pnpm --filter @gadgets/integration-tests test:run: 46 files, 216 tests pass (plus 1 expected fail), including the new upgrade suite and the Google upgrade suite.
  • pnpm lint passes.

@github-actions github-actions Bot added the gatekeeper Changes to a gatekeeper integration label Oct 26, 2026
@ask-bonk

ask-bonk Bot commented Oct 26, 2026

Copy link
Copy Markdown

LGTM!

github run

@github-actions

Copy link
Copy Markdown

Preview: pr896-mpeterson-con-f3f115f7

https://pr896-mpeterson-con-f3f115f7-router.cloudflare-os-previews.workers.dev

Dashboard · deleted when this PR closes

@maxwellpeterson
maxwellpeterson force-pushed the mpeterson/single-gadget-sharing branch from f4ef6da to 86a76a8 Compare October 28, 2026 19:45
@maxwellpeterson
maxwellpeterson force-pushed the mpeterson/context-observer-tracking branch from 39113d9 to d174765 Compare October 28, 2026 21:36
@ask-bonk

ask-bonk Bot commented Oct 28, 2026

Copy link
Copy Markdown

LGTM!

github run

@github-actions

Copy link
Copy Markdown

Eval results

Verdict: ⚪ Unchanged. No task moved beyond what these runs can tell apart from noise.

Task Score Δ score Fisher test Cache hits Avg min Avg steps
change-calendar (gpt-6-luna) 90% → 70% −20 pp p = 0.58 95%
0 pp
4.3 → 4.5 29.3 → 19.1
chess (gpt-6-luna) 100% → 90% −10 pp p = 1.00 98%
0 pp
9.3 → 9.6 61.3 → 55.5
compaction (claude-sonnet-5-5) 100% 0 pp p = 1.00 90%
0 pp
1.8 → 2.3 43.0
compaction (gpt-6-luna) 100% 0 pp p = 1.00 92% → 93%
+1 pp
2.1 → 1.6 44.0 → 43.0
incident-desk (gpt-6-luna) 100% 0 pp p = 1.00 97% → 96%
−1 pp
4.8 → 5.0 42.2 → 35.8
ticket-triage-agent (gpt-6-luna) 100% 0 pp p = 1.00 96% → 97%
+1 pp
0.1 → 0.2 1.9 → 2.1
worker-logs (gpt-6-luna) 90% 0 pp p = 1.00 95%
0 pp
3.6 → 4.2 23.9 → 23.1
Failed checks
Task Check Failed
change-calendar (gpt-6-luna) t1 agent.timedOut 0/10 → 2/10
change-calendar (gpt-6-luna) t1 schedules-and-lists-windows 0/10 → 1/10
change-calendar (gpt-6-luna) t1 rejects-invalid-windows-without-changing-anything 0/10 → 1/10
change-calendar (gpt-6-luna) t1 overlap-is-per-service-and-touching-is-allowed 0/10 → 1/10
change-calendar (gpt-6-luna) t4 earliest-available-applies-every-rule 1/10 → 0/7
change-calendar (gpt-6-luna) t4 earliest-available-matches-the-reference 1/10 → 0/7
change-calendar (gpt-6-luna) t4 earliest-available-slots-are-bookable 1/10 → 0/7
chess (gpt-6-luna) t1 agrees-with-the-oracle-on-the-hard-positions 0/10 → 1/10
chess (gpt-6-luna) t1 agrees-with-the-oracle-on-perft-positions 0/10 → 1/10
chess (gpt-6-luna) t1 agrees-with-the-oracle-through-random-games 0/10 → 1/10
worker-logs (gpt-6-luna) t1 ingests-resets-and-summarises-per-worker 1/10
worker-logs (gpt-6-luna) t1 hourly-buckets-cover-every-hour-including-empty-ones 1/10
worker-logs (gpt-6-luna) t1 ranges-are-half-open-and-filter-by-worker 1/10

Run · trajectories and raw results

@github-actions github-actions Bot deleted a comment from ask-bonk Bot Oct 28, 2026
@ask-bonk

ask-bonk Bot commented Oct 28, 2026

Copy link
Copy Markdown

🔬 Eval runs review

Performance

Against main, change-calendar passed 9/10 → 7/10 and chess 10/10 → 9/10; worker-logs stayed at 9/10 and every other task/model row passed, with comparison.json judging all pass-rate and prompt-cache movements within noise. Mean costs fell for calendar ($0.0252 → $0.0167) and chess ($0.0604 → $0.0528), but early failures shortened the work and the differences remain within broad run-to-run spreads. Two seven-minute calendar stalls lost the most runs; chess remained the step-heavy task (61.3 → 55.5 steps, 9.3 → 9.6 minutes), with failed exact-match edits rising from 6 → 20.

⚪ VERDICT: NO REGRESSION FROM THIS PR

The comparison is unchanged, and the candidate failures occur before any Context read, with no demonstrated connection to the diff’s migration of Context observation authorization into the kernel.

Triage

Failure modes

  • Activation stalls · change-calendar 2/10 · harness bug · this PR: no — trials 4 and 10, turn 1, hit the 420-second activation deadline after successful editFile(client.js) and writeFile calls respectively, without a final reply or tool error; the stalled component is not exposed, but neither trace reaches the changed Context authorization path.
  • Crossed overlap arguments · change-calendar 1/10 · model error · this PR: no — trial 2, turn 1, writeFile(server.js) passes candidate start, existing end, existing start and candidate end to overlaps(), making every existing same-service window conflict, even when disjoint; the agent then declares completion without exercising the RPC. Main’s one failure was different: turn 4’s slot finder skipped valid same-day evening slots.
  • Kings move like queens · chess 1/10 · model error · this PR: no — trial 5, turn 1, writeFile(server.js) routes kings through the sliding-piece loop without a one-square limit, producing illegal moves and duplicate castling destinations; subsequent editFile calls address king captures, not this defect, before the final reply claims full rules support. Main passed all 10 runs.
  • Invented SQL tagged-template API · worker-logs 1/10 · model error · this PR: no — trial 5, turn 1, three editFile(server.js) calls replace ordinary SQL calls with tagged templates such as sql.exec`...` , after which the agent claims completion without executing them; ingestion and queries fail with SQL syntax errors. Main also lost 1/10, but from oversized parameter batches rather than this API mistake.

Tool errors

  • editFile: No matching text was found · chess 6 → 20, incident-desk 5 → 1, worker-logs 1 → 3, change-calendar 0 → 1 · model error — agents quote text that differs from the file; candidate chess trial 1 repeatedly guesses backslash escaping before using the exact text shown by grep.
  • editFile: Validation failed · incident-desk 1 → 7, change-calendar 3 → 2, chess 3 → 2, worker-logs 2 → 3 · model error — calls omit required arguments, especially filename; candidate incident-desk trial 4 supplies it on the next attempt, while main also contains malformed calls missing replacement.
  • editFile: Multiple matches were found · incident-desk 5 → 5, change-calendar 0 → 1 · model error — agents select repeated SQL fragments or shared statements despite the parameter’s exactly-one-match requirement; adding surrounding method/query text resolves the errors.
  • editFile: CHESS has no file named "CHESS" · chess 1 → 2 · model error — the binding name is supplied as filename instead of client.js; candidate trial 7 corrects that argument immediately.
  • executeCode: This step already made code changes · worker-logs 0 → 1 · tool design — candidate trial 9, turn 3, batches a README edit with execution; the tool description omits this step boundary, although the error explains recovery and the next-step retry succeeds.

What to do

  • Optional follow-up in packages/workshop-backend/src/agent.ts: tell agents to smoke-test newly generated RPCs with executeCode in a separate model step after file edits, never in the same batch; these evals do not justify changing the observer migration.

github run

@maxwellpeterson
maxwellpeterson force-pushed the mpeterson/single-gadget-sharing branch from 352ba74 to d46273b Compare October 30, 2026 19:36
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant