Repository navigation
add support for single-gadget sharing - #894
maxwellpeterson wants to merge 6 commits into
Conversation
|
Review: 3 findings. Submitted 3 actionable inline comments in a single review. |
b910074 to
d35b994
Compare
|
Since last review: 2 resolved, 1 still open, 0 new. The scope-widening and hook-attribution findings are fixed in the current code. The existing browser observer-configuration finding still applies. No duplicate inline comments were posted. |
Preview:
|
|
Since last review: 1 resolved, 0 still open, 2 new. Submitted 2 actionable inline comments in one review: one P1 and one P2. All three earlier findings are addressed in the current code. |
f4ef6da to
86a76a8
Compare
|
Review: 4 findings. Submitted 1 new P2 inline comment. Three previously published findings still apply:
Verified these with isolated runtime reproductions; no duplicate inline comments were posted. Applied the backend package’s kernel bar and the frontend guide’s rule to test observable contracts and failure paths ( |
|
Review: 1 findings. The existing gadget-admission timeout finding still applies. No new findings or duplicate inline comments. Verified by source inspection that Applied the backend kernel bar and the frontend guide’s requirement to “Test observable contracts, product rules, state transitions, accessibility behavior, race handling, and failure paths” ( |
|
LGTM! |
352ba74 to
d46273b
Compare
|
LGTM! |
|
LGTM! |
There was a problem hiding this comment.
Superseded by the review of
1818815d.
This review, of 20b5b919
💬 Approved with comments — No blocking issues. There is 1 WARNING about the exclusion check using a narrower scope than admission, and 1 WARNING about observer docs that still describe the old verification timing, plus INFO notes.
Findings: 2 warnings · 3 info (1 is an inline comment on the diff)
The decision is advisory: this review is a comment and never approves, blocks or requests changes on the pull request.
🔧 Fix with your agent
Paste the following into your coding agent to investigate and fix these issues.
Fix the following code review issues found in pull request #894.
Before making changes, review each issue and present a brief summary table:
- For each finding, state whether you agree, disagree, or need clarification
- If you disagree (e.g., the fix would require disproportionate changes
for minimal ROI, or the finding is factually incorrect), explain why
- If you need to clarify something before deciding, ask those questions
- Then share your plan for which issues to tackle and in what order
## WARNING (2)
### [Code Quality] `#inAccessVerificationScope` ignores `gadgetProducers`, which `#inScopeGatekeepers` includes. A "use" observer verified against a producer-only connection is treated as out of scope on exclusion: the observation is admitted and their registration is dropped, then re-added on the next open.
- **File:** packages/workshop-backend/src/overseer.ts:5893
### [Documentation] `docs/observers.md` still says every collaborator is re-verified at every workspace `open()`. "Use" collaborators are now verified per gadget at `getGadget()`, and their workspace shell is not verified.
- **File:** docs/observers.md:715
## INFO (3)
### [Performance] `#gadgetReaches()` is recomputed for every excluded observer inside the `#enforceExcludeObservers` loop. Compute it once and pass it in.
- **File:** packages/workshop-backend/src/overseer.ts:5893
### [Documentation] Ungrammatical "contexts what is opened reaches" and "requirements what is opened reaches". Suggest "reached by what is opened".
- **File:** docs/observers.md:965
### [Documentation] `plans/restricted-data-sharing.md` still references `#useScopeGatekeeperIds` and `computeEffectiveRoles`. Update those references or mark the passages as historical.
- **File:** plans/restricted-data-sharing.md:10
📝 Documentation: ⚠️ 1 warning · 2 info
Observer docs still describe verification as happening at workspace open for every collaborator. There is also a grammar slip and a stale plan doc.
Recommendation: 📝 Minor Comments
- [WARNING]
docs/observers.mdstill says every collaborator is re-verified at every workspaceopen(). "Use" collaborators are now verified per gadget atgetGadget(), and their workspace shell is not verified. (docs/observers.md:715) - [INFO] Ungrammatical "contexts what is opened reaches" and "requirements what is opened reaches". Suggest "reached by what is opened". (
docs/observers.md:965) - [INFO]
plans/restricted-data-sharing.mdstill references#useScopeGatekeeperIdsandcomputeEffectiveRoles. Update those references or mark the passages as historical. (plans/restricted-data-sharing.md:10)
View full review
- Verification timing:
docs/observers.md:150-153and714-717say every collaborator is re-verified against every in-scope gatekeeper at everyopen(), and that nobody can be in the workspace without passing. With this change, "build" collaborators are verified at workspace open. "Use" collaborators open the workspace shell unverified and are verified per gadget atgetGadget(). - Grammar:
docs/observers.md:965and981contain "contexts what is opened reaches" and "requirements what is opened reaches". - Stale plan doc:
plans/restricted-data-sharing.mdstill refers to#useScopeGatekeeperIdsandcomputeEffectiveRoles, which this change replaces.
📋 Code Quality: ⚠️ 1 warning
One scope mismatch between the exclude-observers check and admission. The rest is coherent.
Recommendation: 📝 Minor Comments
- [WARNING]
#inAccessVerificationScopeignoresgadgetProducers, which#inScopeGatekeepersincludes. A "use" observer verified against a producer-only connection is treated as out of scope on exclusion: the observation is admitted and their registration is dropped, then re-added on the next open. (packages/workshop-backend/src/overseer.ts:5893)
View full review
Verification-scope mismatch (overseer.ts:5890 vs 9908)
#inScopeGatekeepers builds a gadget grant's scope from two sources:
reach.gatekeeperIdsobserverLedger.gadgetProducers(reach.gadgetIds), the connections through which a reached gadget recorded requirements, even if it no longer binds them.
#inAccessVerificationScope is documented as "whether they could have been verified against it", but it only consults #gadgetReaches(). A "use" collaborator verified against a producer-only connection is therefore treated as out of scope when named in that connection's excludeObservers. The observation is admitted, and their registration on that connection is dropped. Their next gadget open then re-verifies them and registers them again, because admission still counts the producer.
What this means in practice:
- The two functions now disagree on what "in scope" means.
- New data from the connection probably cannot reach a gadget the collaborator holds if no included gadget reaches it.
- The exclusion path still loses its fail-closed property for producer connections.
Either derive both functions from one helper, or state in the comment that exclusion only concerns forward reach and add a test for it.
⚡ Performance: 💬 1 info
One low-priority note about reach recomputation on the observation path.
Recommendation: 💬 Optional Nits
- [INFO]
#gadgetReaches()is recomputed for every excluded observer inside the#enforceExcludeObserversloop. Compute it once and pass it in. (packages/workshop-backend/src/overseer.ts:5893)
View full review
#enforceExcludeObservers calls #inAccessVerificationScope once per named observer. Each call runs #gadgetReaches(), a full transitive walk over every permanent gadget with SQLite reads. Computing it once before the loop would avoid N identical walks per observation. Optional.
✅ All other checks passed
🔒 Security: ✅ LGTM
Checked and found sound:
- Sharer-must-hold-build conferral in
computeEffectiveAccess, including share links through their creator. #requireCanShare, which blocks sharing by "use"-only holders.- Legacy "all gadgets" links, which add no new access.
- Grants that name provisional or missing gadgets confer nothing.
- Restarts on any narrowing in
removeCollaboratorandrevokeShareLink. - Per-gadget admission in
getGadget/authorizeGadget: access is re-read live, and a refused gadget gets the same "No such gadget" error as a missing one. - Per-gadget session counting.
- Filtered listings for "use" collaborators.
- Widening detection with spawner quarantine.
On the frontend:
- Share URLs are built with
URL/searchParams, and the key stays in the fragment. - UI gating is only a hint; the server enforces.
📖 AGENTS.md: ✅ LGTM
No agent-guide changes needed.
Reviewed commit: 20b5b919
| if (access.role === "build") return true; | ||
| if (!gatekeeperVendorId(this.storage.gatekeepers.get(gatekeeperId))) return true; | ||
| return this.#accountRequiringUseScope().has(gatekeeperId); | ||
| for (let [gadgetId, reach] of this.#gadgetReaches()) { |
There was a problem hiding this comment.
#inAccessVerificationScope ignores gadgetProducers, which #inScopeGatekeepers includes. A "use" observer verified against a producer-only connection is treated as out of scope on exclusion: the observation is admitted and their registration is dropped, then re-added on the next open.
Admission (#inScopeGatekeepers, line 9912) puts every connection in a gadget grant's scope through which a reached gadget recorded requirements (gadgetProducers). This check only looks at #gadgetReaches(). A "use" collaborator who was verified against such a producer connection and is named in its excludeObservers is therefore classified out of scope: the observation is admitted and their registration is removed, and their next open re-registers them. Either derive both scopes from one helper, or document that exclusion intentionally considers only forward reach and add a test.
|
LGTM! |
There was a problem hiding this comment.
💬 Approved with comments — No blocking or warning-level issues. The earlier warnings on the exclusion scope and on the observer docs are fixed; two INFO notes remain, one about a missing test and one about the cost of the reach computation.
Findings: 2 info
The decision is advisory: this review is a comment and never approves, blocks or requests changes on the pull request.
📋 Code Quality: 💬 1 info
The earlier warning on the exclusion scope is fixed. One INFO note about a missing test.
Recommendation: 💬 Optional Nits
- [INFO] Exclusion looks only at what a gadget reaches now, while admission also counts historical producers. The difference is documented in a comment but has no test for a connection that is only a historical producer. (
packages/workshop-backend/src/overseer.ts:5835)
View full review
Earlier warning, fixed. #inAccessVerificationScope no longer exists. #enforceExcludeObservers (packages/workshop-backend/src/overseer.ts:5835-5882) works out which gadgets reach a connection from #gadgetReaches(). Its comment explains why it differs from admission (#inScopeGatekeepers): a historical producer (a connection a gadget recorded requirements through but no longer binds) does not deliver new observations, so it does not block an exclusion. observer-ledger.test.ts adds a test where a historical producer's new read is let through and only that registration is dropped.
Still open (INFO). observer-exclude-scope.test.ts does not cover a connection that is only a historical producer of a gadget the observer holds. One test there would pin this deliberate difference, so a later change to #inScopeGatekeepers cannot quietly change how exclusion behaves.
Checked and found sound:
- The
computeEffectiveAccessfixed point, the rule that a sharer must hold build, and#reRootKeptUsers. - Per-gadget session counting.
- Widening detection with spawner quarantine.
sameObserverScopenow requires an exact match.- The
observerRequirementPrefixkey now ends in:, so gadget ids like 1 and 12 cannot collide. - The frontend admission flow, which disposes stubs and aborts superseded prompts.
⚡ Performance: 💬 1 info
The earlier note about recomputing reach per observer is fixed. One INFO note about the reach computation spanning the whole workspace.
Recommendation: 💬 Optional Nits
- [INFO]
#gadgetReaches()walks every gadget's reach in the workspace on the observation path and twice per bind, hook enable or merge. Fine at current sizes; consider caching if workspaces grow. (packages/workshop-backend/src/overseer.ts)
View full review
Earlier note, fixed. #enforceExcludeObservers now fills reachingGadgets once, lazily, with ??=.
INFO. #gadgetReaches() walks the reach of every permanent gadget. Each node in that walk costs a storage read. It runs:
- through
gadgetsReaching()on an observation that has a "use" observer; - twice in
#restartIfGadgetScopesWidenedon every bind, hook enable and merge.
gadgetProducers adds O(connections × reached gadgets) listKeys probes on each verification pass. This is fine at typical workspace sizes. If large workspaces become common, consider caching the reach map per synchronous turn, or computing reach only for gadgets that have a live session.
✅ All other checks passed
🔒 Security: ✅ LGTM
Checked and found sound:
- Grant conferral: a grant, including a share link, confers access only while its sharer or creator holds build.
computeEffectiveAccessis a least fixed point, so a cycle of grants that the owner does not reach confers nothing. - Who can share:
#requireCanShareguardsaddCollaborator,createShareLinkandnewShareLinkKey. Grants that name a missing or provisional gadget confer nothing. - Gadget admission:
authorizeGadgetandgetGadgetre-read access live. They refuse an excluded gadget with the same "No such gadget" error as a missing one. - Spawner queue: it checks
assertGatekeeperUsable, so a spawner held until a restart starts nothing. - Observation audience:
ObserverLedger.#audiencefails closed for "use" observers on chat and gadget contexts. - Frontend: share URLs keep the key in the URL fragment, and UI gating is only a hint because the server enforces access.
📖 AGENTS.md: ✅ LGTM
The agent guides need no changes for this pull request.
📝 Documentation: ✅ LGTM
Fixed since the last review:
- Verification timing:
docs/observers.md(around lines 362 and 717) now says that "build" collaborators are re-verified at workspaceopen()and "use" collaborators atgetGadget(). - Grammar: the "what is opened reaches" phrasing is gone.
- Plan:
plans/restricted-data-sharing.mdnow opens with a "Historical plan" notice, and its anchors section is renamed to say it is historical, so the stale#useScopeGatekeeperIdsandcomputeEffectiveRolesreferences are framed correctly.
Reviewed commit: 1818815d
Eval resultsVerdict: ⚪ Unchanged. No task moved beyond what these runs can tell apart from noise.
Failed checks
|
🔬 Eval runs reviewPerformanceAgainst main, change-calendar fell from 100% to 80% and chess from 100% to 60%, but neither exceeds noise ( ⚪ VERDICT: NO REGRESSION FROM THIS PRThe sharing and observer-admission changes do not explain the generated-code mistakes, and the unchanged comparison verdict plus mixed costs and step counts establish no PR-attributable improvement or regression. TriageFailure modes
Tool errors
Prompt cache
What to do
|
This PR deprecates the workspace-wide "use" sharing role and replaces it with gadget-specific sharing grants.
Existing users with the "use" role will retain their current access (subject to the new delegation rules discussed below), and continue to automatically gain access to new gadgets added to the workspace. Adding new users with the "use" role will no longer be possible, and existing share links with the "use" role will stop granting new users access. In the language of the sharing graph, “use” edges continue to exist, but new “use” edges cannot be created except via
keepUsersduring existing "use" role revocation.In place of the "use" role, every gadget in a workspace will become individually sharable. This is a fairly simple extension of the existing sharing graph model. New, gadget-scoped “use:<gadget_id>” edges are created by direct shares and gadget-scoped share links.
When a gadget is shared with someone, that user is verified against observations attributed to that gadget, as well as any workspace-wide observations from any connection the gadget has. Once all gatekeepers migrate to the API introduced in #881, verification will be narrowed to just gadget-scoped observations (with a few small caveats) in new workspaces going forward.
This PR splits the current sharing modal into separate sharing modals for workspace collaborators (the "build" role) and individual gadget sharing. The modal itself doesn't change much beyond simplifying away "build" vs. "use" indicators for workspaces without legacy "use" roles.
This PR also changes how grants are conferred, requiring that a user hold the "build" role in order to confer grants to others. Consider the following scenario: Alice owns a workspace. Alice invites Bob with the "build" role, and Bob invites Carol with the "build" role. Alice revokes Bob's access and declines to keep Carol. Both Bob and Carol lose all access. Alice then shares gadget A with Bob.
Under the old model, Carol regains access to gadget A through the old "build" grant from Bob. Carol's access being resurrected in a different form is unintuitive to me, but the bigger problem is how to present Carol in the UI. Carol only has access to gadget A, but she doesn't have a grant to gadget A. If Alice later shared gadget B with Bob, Carol would also gain access to gadget B. It doesn't make sense to show Carol in the shared user list for gadget A, since her access and revocation logic are different from everyone else with a direct grant to gadget A. It also doesn't make sense to show Carol in the workspace collaborator list, since she only has use access to gadget A. Carol effectively has the deprecated "use" role, predicated on gadgets that Bob has access to instead of all gadgets in the workspace.
Under the new model, Carol's access doesn't change when Alice shares gadget A with Bob (note that Carol would still regain "build" access if Alice restored Bob's "build" access). Any workspace collaborator can add Carol as a peer collaborator or grant her access to a specific gadget. There is no in between state. The only "in-betweeners" are users with the legacy "use" role, which show up in the workspace collaborator list under their own section.
Maybe this logic change should be its own PR, but it has a significant impact on how the new UI works and I feel confident in it so I decided to include it here. The main risk of the new model is a future where we allow single-gadget users to reshare those gadgets. Even in this world, I don't think we would bring back the old model.
Instead, we could adopt this model: A grant confers access only while the sharer’s effective access covers the grant’s entire scope. Otherwise it confers nothing. When computing effective access, each fully supported grant contributes its entire scope to the recipient, and unsupported grants contribute nothing rather than being narrowed. This preserves the existing cheap revoke/restore mechanism while preventing a revoked "build" role from coming back to life as a gadget grant later on (the Carol situation described above). Spooky 👻