B7b: every access resolver answers with one org-aware membership model - #2673
Conversation
…red org-aware membership (B7b) getUserDriveAccess, isUserDriveMember, isDriveOwnerOrAdmin, getUserDrivePermissions, getUserAccessiblePagesInDrive(+WithDetails), getBatchPagePermissions, getUsersWhoCanViewPage, getDriveIdsForUser (and through isUserDriveMember hasAppDriveMembership/hasScopedDriveMembership), plus the lib gates checkDriveAccess, checkDriveAccessForRoles, resolveGranterAccess, getPageIfCanShare, getMemberCustomRoleId, isUserMemberOfAnyEventDrive and resolveDriveMembership now read membership only through loadEffectiveDriveMembership / resolveEffectiveDriveMemberships, the one IO edge over B7's pure resolveEffectiveDriveMembership. getDriveIdsForUser lists exactly what listAccessibleDrives lists (decideListedDriveRole). ORG-4 audit dedupe: org-admin-access-audit.ts writes authz.access.granted at most once per (user, PRIVATE drive) per 15-minute UTC window, via a guarded ON CONFLICT DO NOTHING claim on the (key, windowStart) primary key of rate_limit_buckets (no migration; the Admin PG ingest grant is INSERT-only), with an in-process memo in front and fail-open on a claim-store error. Two fixes the consistency matrix surfaced, both live on master: - checkDriveAccessForRoles read pending rows (a pending ADMIN invitation managed roles). - resolvePagePermissionRow let a custom role's drive-wide view open a PRIVATE page in batch permissions and the page-viewer fan-out; every other resolver already refused that. Tests: org-drive-sibling-resolvers.integration.test.ts (consistency matrix, dark-flag equivalence against a frozen de98839 copy, pending invitations, agent/app identities, granter caps), org-admin-access-audit.test.ts; the Northwind fixture is shared with the B7 suite. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011VHYwVCUxjqUhx9Z5yaxwn
…paths and processes (B7b) Five resolver calls in one 15-minute UTC window (page view, AI tool check, drive room join, drive route, batch check), then search's drive gate and per-hit checks, then a second process with an empty memo, all write one authz.access.granted row; 09:14:59.999 is the same window and 09:15:00 writes a second. An audience computation (getUsersWhoCanViewPage) writes none. The claim itself is a Postgres unique key: five concurrent claims, one winner. claimOrgAdminAuditWindow is exported for that last test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011VHYwVCUxjqUhx9Z5yaxwn
…B7b) - reassignLedOrgDrives deletes the former lead's OWNER row on every drive it reassigns, inside the reassignment transaction (a rollback keeps it). - leaveOrganization deletes the leaver's OWNER rows on all of the org's drives (led now, or led once and already reassigned), reported as revoked.formerLeadOwnerRows; personal drives and other orgs' drives keep theirs. - updateDriveLastAccessed writes the owner self-heal row only for a personal drive; on an org drive the lead reaches it through drives.ownerId and a row would outlive a reassignment. Data check (the query in leave.integration.test.ts ownerRowsOnOrgDrives, unfiltered): 0 rows on a freshly migrated database; the e2e Northwind seed creates no org drive, and every route that sets drives.orgId is 404 while ORGS_ENABLED is false, so no environment can hold one today. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011VHYwVCUxjqUhx9Z5yaxwn
…t, OAuth grants included (B7b) - revokeOrgDriveGrants (leave.ts) is the one per-drive revocation: agent memberships granted, drive and page share links created, MCP key drive rows, and now every OAuth token family whose scopes name one of the drives (an explicit OAuth drive role lives in the scope list and is never re-checked, so the family is revoked with reason org_access_revoked). leaveOrganization, and through it removeMember and account deletion, run it over all the org's drives. - changeMemberRole runs revokeForDemotion (demotion.ts) in the same transaction. Pure planDemotionRevocation compares effective access before and after through resolveEffectiveDriveMembership: drives the lower role cannot open get the full revocation; drives where ADMIN became MEMBER lose drive share links, page share links the member could not share (memberCouldSharePage mirrors getUserAccessLevel), and agent grants above the member cap (recapAgentMembershipsGrantedBy, now transaction-aware with a known cap). Inheriting key scopes and grants on such drives stay: they follow the person. Tests: org-member-revocation.integration.test.ts (removal: MCP key scopes and an OAuth grant on org drives stop resolving, own-drive ones survive; demotion: Finance fully revoked, Product loses only admin-only artifacts, an invited ADMIN row keeps Omar's; promotion revokes nothing) and demotion.test.ts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011VHYwVCUxjqUhx9Z5yaxwn
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@codex review |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
…m (B7b) CI Unit Tests: drive-members-enumeration.seam.test.ts flagged a drive_members read in organizations/demotion.ts. The read moves to loadAcceptedRowsInDrives in permissions/org-drive-membership.ts, which accepts the role change's transaction. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011VHYwVCUxjqUhx9Z5yaxwn
2witstudios
left a comment
There was a problem hiding this comment.
[independent-review] agent:ow-irv-2673
Spec review of #2673 (B7b) at head 2b0c4bf7da3e19dd7326537ccb50c44b3341dfa9. I read Spec drc7x34u, leaf gdk2ok99 (all sections), Sequence Spec, Retro 0, and the full diff. I also verified on a throwaway pg17 (127.0.0.1:5641, session TZ America/Chicago, freshly migrated), since stopped.
Verdict: changes requested (posted as COMMENT: GitHub refuses REQUEST_CHANGES on a PR by the same account). One P1 (CI is red on this head) and two P2s. The access model itself holds up: the extended matrix agrees everywhere, the frozen legacy copy is mechanical, and query counts are constant.
Per-ID verdicts
| ID | Claim | Verdict |
|---|---|---|
| ORG-4 | (partial) in 16 titles (matrix, agent/app identities, dedupe unit ×5 and integration ×2, demotion unit ×4, removal/demotion integration ×2) |
partial, correctly marked in it titles. I agree it must stay partial: the apps/web inline gates (B7c) and the owner-only actions closed to the org Owner leave "full access on every org-owned drive" unproven. But describe('ORG-4 audit dedupe (integration)') names ORG-4 plainly (P2-1 below). |
| DRV-5 | (partial) matrix + an implicit Open member holds the drive default role… (org-drive-sibling-resolvers.integration.test.ts) |
partial, correct. The sidebar and picker are Wave F. |
| DRV-6 | (partial) matrix |
partial, correct. There is no join or approve flow here. |
| DRV-7 | (partial) resolveGranterAccess follows the shared membership… |
partial, correct |
| X-6 | (partial) matrix |
partial, correct. Two negatives are tested: Dana, from another org, is denied Northwind's OPEN Product, and Chris, a guest, gets no listing of a second drive through a page share. The policy, wallet and automation clauses belong to other lanes. |
| ORG-6 | (partial) ×3 new (leave.integration.test.ts) |
partial, correct. The titles are the ones the leaf mandated. |
| DRV-9 | not claimed | correct |
- The allowlist is unchanged.
- No migration.
- No real Spec ID appears in a gate self-test.
- Every partial marker is well formed.
CI on this head (run 35238300575)
ci / Spec ID coverage: FAILED, log lines 246 to 313:ORG-4 allowlisted partial: …ORG-6 MISSINGcovered 9/87, allowlisted-missing 77, MISSING 1FAIL: no passing test names these IDs: ORG-6
- On #2669's run (base) ORG-6 was
covered(10/87, MISSING 0). The regression is a cascade of the P1: libtest:coverageexited 1, so later results never reached the gate. - Lint & TypeScript, and E2E: green.
P1
P1-1: ci / Unit Tests fails on the lib seam guard (inline at packages/lib/src/organizations/demotion.ts:130).
src/__tests__/seams/drive-members-enumeration.seam.test.ts > no NEW file outside the membership seams reads drive_membersfails withpackages/lib/src/organizations/demotion.ts:130 .from(driveMembers).- Result:
Test Files 1 failed | 617 passed, and@pagespace/lib#test:coverage exited (1)(unit log ~7885 and 12214). - The PR's local unit command (
src/permissions src/services src/organizations src/__tests__/acceptedAt-gate.test.ts …) never ransrc/__tests__/seams. - Fix: move the row read into a services/permissions function (the seam's rule). Only pre-existing moved code may use the allowlist.
P2
P2-1: a plain ORG-4 claim through a describe title (inline at org-admin-access-audit.integration.test.ts:42).
hitsFromTestOutcomes(scripts/check-spec-coverage.ts ~425-433) counts an ID named in an ancestor title as covered once every test under that describe passes.nameCarriesIdseesORG-4 audit dedupeas plain.- Once the lib integration results reach the gate (after P1 is fixed), ORG-4 becomes STALE-ALLOW and the gate fails. Worse, the file claims ORG-4 in full, which this PR says it does not prove.
- Fix: rename the describe (e.g.
org-admin audit dedupe (integration)) or mark itORG-4 (partial).
P2-2: the dedupe claim is consumed before an audit write that can fail, and is never released (inline at org-admin-access-audit.ts:93).
writeisaudit(), which firessecurityAudit.logEvent(...)and only logs a rejection ([Audit] audit write failed).repository.appendEventcan reject, for example when the Admin PG ingest is unreachable while the main DB, which holds the claim, is up.- In that case the (user, PRIVATE drive) claim row already landed, and every later access in the 15-minute window is suppressed. So one failed write loses the chain record for the whole window, including accesses after the audit store recovers.
- The module's own contract says "Over-auditing a PRIVATE drive is acceptable; losing the record of an access is not."
- Fix: let the writer return the append promise, and on rejection delete the claim row (and drop the memo entry) so the next access retries. Add a unit test with a rejecting writer.
Rest of the brief, verified
(a) Consistency matrix. The lane's suites reproduce at 2b0c4bf7d: sibling-resolvers 6/6, audit 2/2, revocation 3/3, leave 12/12, B7 resolvers 8/8.
- I extended the matrix fixture (git-excluded probe, not committed) with shapes the lane did not have:
- a trashed PRIVATE org drive that the org Admin reaches only through org power;
- an OPEN drive whose default role names a private page explicitly, plus drive-wide view;
- a pending ADMIN invitation on an OPEN drive for an org MEMBER;
- a RESTRICTED drive where a row's custom role has drive-wide view, next to a private page;
- explicit canView=false page permissions, for an implicit OPEN member and for an org Admin;
- an expired page share.
- Every resolver agreed with getDriveAccess and getUserAccessLevel for shapes 1 to 4, and for the org Admin's deny in shape 5. The remaining disagreements are not caused by this PR (P3 notes below).
- Mutation 1 (by line index, with a no-op control):
drive-member-service.ts:96, where checkDriveAccess loads membership as{ id: drive.id }so the shared loader treats it as a personal drive. The matrix went RED:1 failed | 5 passed. Control: 6/6.
(b) Routing.
- All 9 named siblings and the 7 extra lib gates call
loadEffectiveDriveMembershiporresolveEffectiveDriveMemberships. Every call site passes a full drive row, or a select that includes orgId and orgVisibility. - The lib functions left reading
drive_membersordrives.ownerIdare:- listings or audiences:
getDriveMemberUserIds,getDriveRecipientUserIds,getAllMemberUserIdsForEvent,app-shell-service,usersShareDrive,listAccessibleDrives(B7); - mutations: share-link accept upserts,
org-membership-sync,deletion,leave,demotion.
- listings or audiences:
- All of these are named for B7c or are not access decisions.
(c) Dark flag.
- I diffed the 15 functions in
pre-b7b-sibling-resolvers.tsagainstgit show de988390ewith whitespace normalised. They are identical except for theLEGACY_VIEWER_BATCH_SIZEconstant name, so the frozen copy is mechanical and not self-graded. - By code reading, the only other behaviour changes are for a nonexistent drive id:
isDriveOwnerOrAdmin,isUserDriveMemberandgetUserAccessiblePagesInDriveno longer query membership, which the FK makes unreachable. - The two declared diffs:
- the private-page guard in
resolvePagePermissionRow; - the acceptedAt gate in
checkDriveAccessForRoles, by point-guard ruling.
- the private-page guard in
(d) Former-lead OWNER row.
reassignLedOrgDrivesdeletes the row per drive inside the transaction. The rollback test proves it is in the same transaction.leaveOrganizationdeletes it on every org drive.updateDriveLastAccessedis guarded byorgId === null.- Data check on my freshly migrated pg17:
drive_memberswith role OWNER joined to drives with orgId not null returns 0. There are 0 org drives.
(e) Dedupe on rate_limit_buckets. The choice is sound apart from P2-2.
- Key namespace. Every
checkDistributedRateLimitorreset…caller in apps and packages uses a fixed literal prefix (oauth-token:,magic_link:,authfail:,webhook-seen:, …). None is caller-controlled or starts withaudit:, so nothing can pre-claim or reset an audit key. - Types.
window_startandexpires_atare timestamptz. The window is epoch-ms arithmetic, which is UTC. - Sweep. It deletes
expires_at < now(), and expiresAt is the window end, so a live dedupe row is never swept early. After the window ends the key's windowStart differs, so a sweep cannot cause a duplicate. - Growth. At most one row per (admin, PRIVATE drive, 15 minutes), bounded by the existing cron sweep.
- Concurrency. Covered by the integration test: 5 racers with 1 winner, and a fresh module graph as a second process.
- Mutation 2 was not on this file. See (f).
(f) Removal and demotion.
- Removal runs the leave cascade. It revokes mcp_token_drives rows and OAuth families whose scopes name org drives; the integration test resolves the key and the OAuth token before and after.
- Demotion revokes everything on access-gone drives, and only drive links, unshareable page links and the agent recap on OPEN drives. Inheriting scopes stay. Omar's invited ADMIN row is untouched.
- Mutation 2 (by line index, with control):
demotion.ts:102, removing memberCouldSharePage's private-page guard.demotion.test.tswent RED:1 failed | 4 passed. Control: 5/5.git statuswas clean after both mutations.
(g) acceptedAt.
- The shared loader,
getDriveIdsForUserWithOrgs, both batch joins and the demotion row read all filter acceptedAt. - The pending-invite test covers every routed function, dark and enabled. My extension adds a pending ADMIN invitation on an OPEN drive, which resolves as the implicit MEMBER.
(h) N+1. I spied on pool.query with orgs enabled:
| Call | Queries |
|---|---|
getBatchPagePermissions, 40 pages |
4 |
getBatchPagePermissions, 120 pages across 3 org drives |
4 |
getUsersWhoCanViewPage, 1 candidate |
4 |
getUsersWhoCanViewPage, 13 candidates |
4 |
getDriveIdsForUser |
6 |
…WithDetails |
7 |
No N+1. revokeForDemotion loops per capped drive inside the transaction, which is bounded by the org's drives and is a mutation, not a listing.
(i) ORG-4 stays partial. Yes, for the reasons in the table.
Rules.
- The decision logic is pure (
planDemotionRevocation,memberCouldSharePage,createOrgAdminAccessAuditorwith injected IO). - Permissions go through
permissions/. - No
any. ORGS_ENABLEDis a code constant.- No new
now(). - IN lists are chunked at 500.
- No new @pagespace/lib subpath is imported by apps.
- Scope matches the leaf.
P3 (no thread needed)
- The PR body says
getDriveIdsForUserreturns "exactly" thelistAccessibleDrives({includeTrash})set. Not quite.listAccessibleDrivesignorespagePermissions.expiresAt, so a user whose only link to a drive is an expired page share gets it listed, whilegetDriveIdsForUser(correctly) omits it.- Reproduced on pg17:
getDriveIdsForUser: pat answered [], listAccessibleDrives ["…"]. - The bug is pre-existing in
listAccessibleDriveson master (it lists a drive name through an expired share). Worth a master ticket; not this lane.
- Pre-existing, dark and enabled alike: a drive member with an explicit
canView=falsepage permission still gets that page fromgetUserAccessiblePagesInDrive(+WithDetails), whilegetUserAccessLeveldenies it.- Shown for Nina (implicit Product member) and for Dana on a personal drive. The legacy copy is identical.
- The tree leaks the title of an explicitly revoked page. This is for master, not this lane.
getMemberCustomRoleIddoes not skip the drive's lead. On an OPEN org drive, a lead who is an org MEMBER now gets the default role's id (null before). All 3 callers check owner first, so this is latent only.MembershipDrive.orgIdis optional, and undefined means a personal drive.- A future caller that selects only
{ id, ownerId }silently skips the org model. Mutation 1 shows exactly that. - Consider making
orgIdandorgVisibilityrequired.
- A future caller that selects only
- The new ORG-6 tests in leave.integration.test.ts keep that file's
if (!dbAvailable) return;pattern, so they pass without doing anything when no DB is available. CI has a DB; the pattern is pre-existing. - The audit writes in the name of the wrong person on some paths. When
recapAgentMembershipsGrantedByresolves the granter on another actor's action, it audits org-admin access by the granter, who accessed nothing. This errs toward over-auditing.
|
CI fix at d1e6edd: Unit Tests failed on |
…; expired shares list no drive (B7b) Review 5237996587: - P2: the dedupe claim was consumed before a fire-and-forget audit write. The write is now awaited (same structured log + securityAudit.logEvent as audit()), and a failed write removes the in-process memo entry and deletes the claim row, so the next access in the window writes the record. record() still never rejects. - P2: the dedupe describe title made a plain ORG-4 claim; retitled 'ORG-4 (partial) audit dedupe'. - Point-guard: listAccessibleDrives (dark and org bodies) ignored pagePermissions.expiresAt, so an expired page share still listed its drive while getDriveIdsForUser and every access resolver refused it. Both bodies now filter expired shares; master has the same unfiltered query. Tests: unit 'a failed audit write releases the claim...', integration 'a failed audit write releases the window...' (securityAudit.logEvent rejected once), matrix fixture gains Lu's expired share on Marcus Notes plus 'an expired page share lists no drive, dark or enabled'. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011VHYwVCUxjqUhx9Z5yaxwn
2witstudios
left a comment
There was a problem hiding this comment.
[independent-review] agent:ow-irv-2673b
Re-review of #2673 (B7b) at 7d094f204841cbdc5d64264c1add45d5de23ed0a, covering the delta 2b0c4bf7d..7d094f204 (d1e6edd, 7d094f2) against review 5237996587.
I read the leaf gdk2ok99 (all sections), Spec drc7x34u, reviews 5235984739, 5236485572 and 5237996587, and the three thread replies. All my evidence comes from a throwaway homebrew pg17 (127.0.0.1:5673, session TZ America/Chicago, freshly migrated), since stopped. I changed no code; probes were git-excluded and removed, and git status was clean after every mutant.
Verdict: all three findings of 5237996587 are FIXED. One new P2 (inline), 0 P1.
Previous findings
| Finding | Status | Evidence |
|---|---|---|
| P1: demotion read failed the drive-members seam guard | FIXED | See (1) below |
| P2-1: plain ORG-4 through the describe title | FIXED | See (2) below |
| P2-2: dedupe claim consumed before a write that can fail | FIXED | See (3) below |
| Orchestrator add: expired page share listed its drive | FIXED | See (4) below |
(1) The demotion read now goes through the permissions seam, not the allowlist.
demotion.ts:129callsloadAcceptedRowsInDrives(tx, …)inpermissions/org-drive-membership.ts. It runs the same select as before, with the sameacceptedAtfilter, on the same transaction; an empty id list yields an empty map. The seam test file is untouched in the delta.- My plant: a new file
src/organizations/zz-review-plant.tswithdb.select().from(driveMembers). The seam test reported1 failed | 2 passed, namingzz-review-plant.ts:6. After deleting the plant:3 passed. - CI on this head (Test Suite run 35242406812):
- Unit Tests is green, and lib reports
Test Files 618 passed | 5 skipped(was1 failed | 617). - Spec ID coverage is green:
ORG-6 covered,covered 10/87, allowlisted-missing 77, MISSING 0,spec-coverage: OK. - Security Tests run 35242406569 is green.
- Unit Tests is green, and lib reports
(2) The describe title is fixed and ORG-4 is not counted as covered.
- The describe reads exactly
ORG-4 (partial) audit dedupe (integration). - Spec job:
ORG-4 allowlisted partial: …, listingorg-admin-access-audit.integration.test.tsamong the partial files. ORG-4 is still onscripts/spec-coverage-allowlist.txt:68, not counted as covered.
(3) The claim is now released when the audit write fails. write is awaited. On rejection the auditor drops the memo entry and deletes the claim row (key, windowStart). record() still never rejects. My own probe on pg17, beyond the lane's test:
securityAudit.logEventthrows synchronously twice in a row: first on the drive route (getDriveAccess), then on a page view. After each failure the claim row is gone and there are 0 audit rows.- A third access at 10:59:59 in the same window writes exactly 1 row, and a fourth writes none.
- The claim row reads
windowStart 10:45:00ZandexpiresAt 11:00:00Z, so the window end is correct on a Chicago session. - The lane's suites: audit integration 3/3 and unit 6/6.
The rate_limit_buckets choice still holds.
- Key namespace. I re-checked every
check/reset…DistributedRateLimitcaller in apps and packages. All use a fixed literal prefix (export:user:,passkey_auth:,signup:,integration:,page-webhook*:,email:,oauth*:, …). None can produceaudit:, and only this module writesaudit:org-admin-private-drive:keys. - No early sweep. The sweep is
expires_at < now()on timestamptz, which does not depend on the session TZ.expiresAtis the window end. - No unbounded growth. There is at most one row per (user, PRIVATE drive, 15 minutes), and a failed write deletes rather than adds.
(4) An expired share no longer lists its drive. Both listAccessibleDrives bodies filter expiresAt IS NULL OR expiresAt > now. Probe on the Chicago session, with shares expired 2h ago and 60s ago, live for another 2h, never expiring, and one expired on a personal drive:
- Dark:
listAccessibleDrives=getDriveIdsForUser= {personal, research}. - Enabled: both = {personal}; a page share alone lists no org drive.
- Mutants, with the control green:
- Line 120, dark body filter removed: RED. It lists
handbook, financeand the expired personal drive, which is exactly master's behaviour. - Line 217, org body filter removed: RED. It lists the expired personal drive.
- Line 120, dark body filter removed: RED. It lists
- Master differs:
origin/masterdrive-service.ts:110still has the unfilteredpermissionDrivesquery. It needs its own master lane, as the PR body says.
(5) Consistency matrix.
- I copied the lane's matrix and added my own people:
- Ola: org ADMIN with an accepted invite MEMBER row on PRIVATE Finance.
- Pia: org MEMBER with no row and only a page share (view+edit) on Finance's private page.
getUserDriveAccessis true; she is not listed. - Rex: removed from the org, still holding a stale
source:'org'row on OPEN Product and an invite ADMIN row on Research.
- With 16 people × 6 drives, every resolver agrees, and the non-vacuity asserts hold.
- Mutants, by line index with an anchor guard and the control green; neither function was mutated by the lane or the previous reviewer:
permissions.ts:414:isDriveOwnerOrAdminreads the rawdrive_membersrow. RED, includingisDriveOwnerOrAdmin: ola on Finance answered false, canonical true.permissions.ts:453:getUserAccessiblePagesInDrivereads the raw row. RED.
- A fourth shape of my own fails the matrix: P2 below.
(6) Dark flag. The lane's dark-equivalence test passes at head (exactly 4 diffs against the frozen de988390e copy).
- Both declared fixes are now on master via #2672 (merged 2026-09-18,
6d5d088c8): theresolvePagePermissionRowprivate-page guard and thecheckDriveAccessForRolesacceptedAt gate. - The expired-share filter is a third dark-visible change and is not on master. It is declared in the "Fixes" list, but the callers section still says "While dark, every caller sees exactly today's result, except the two fixes above" (P3).
- The delta's audit and demotion code runs only on org power or org role changes.
(7) Partial markers. Every Spec ID in an added test title is (partial): ORG-4 ×19, ORG-6 ×3, DRV-5 ×2, DRV-6, DRV-7 and X-6. The only plain mentions are in code comments. ORG-4 must stay partial until B7c, because the apps/web inline gates and the owner-only actions closed to the org Owner are unproven.
P2 (new, inline at permissions.ts:1048)
A role entry that denies view still grants edit in batch permissions.
resolvePagePermissionRowreturns{ ...resolved, canDelete: false }even whenresolved.canViewis false.getUserAccessLevel(line 326 here) returns null in that case.- The shape: Product's default role holds a per-page entry
{canView:false, canEdit:true}. With orgs on, the matrix goes RED for nina, kai, eve, marcus, tomas and pia:getBatchPagePermissions … answered [false,true,false,false], canonical [false,false,false,false]. - Every implicit OPEN member holds the default role, so enabling orgs spreads this to the whole org.
- #2672 closed exactly this on master (
return resolved.canView ? { ...resolved, canDelete: false } : null;, with the testgrants nothing when the custom role entry denies view, whatever else it sets). This PR carries only the private-page half of that hunk. - With master's line applied at 1048, my extended matrix goes green, and the lane's suite (including dark equivalence at exactly 4 diffs) plus
resolve-page-permission-row.test.tspass:22 passed. - While dark this matches the
pu/org-walletsbase, so it is not a regression. But it breaks the leaf's "every resolver agrees", andpermissions.tsconflicts between master andpu/org-wallets. Taking this side of the conflict would silently undo #2672.
P3 (no thread needed)
- A lost claim can suppress audits after a failed write. A process that loses the claim while another process's write is in flight and then fails memoizes the loss, and writes nothing more in that window. Probe: process B lost during A's failing write, and B's re-access in the same window left 0 rows and 0 claim rows. Only a later access in another process (A did) restores the record. This is a narrow race (a failing write overlapping a concurrent access in another process), not the deterministic loss P2-2 was. A short TTL on memoized losses would close it.
- The PR body's dark-flag sentence is stale. It says "the two fixes above"; there are three dark-visible changes, and only the expired-share one is still missing from master.
- The merge with master will conflict in
permissions.ts,drive-role-service.tsanddrive-service.ts, all touched by #2672. Resolve toward master's lines.git merge-tree origin/master origin/pu/org-walletsalready conflicts there, so this belongs to the integration branch, not this lane.
Suites at head, my runs: sibling-resolvers 7/7, B7 resolvers 8/8, audit integration 3/3 and unit 6/6, revocation 3/3, demotion 5/5, drive-service 40/40, seams 3 files.
… in the batch resolver; a lost audit claim is trusted only briefly (B7b) - resolvePagePermissionRow takes master's #2672 line: resolved.canView gates the custom-role result, so {canView:false, canEdit:true} no longer grants edit through getBatchPagePermissions while getUserAccessLevel returns null. The matrix fixture gives Product's default role that entry on a Board deck page; the dark-equivalence test names it as the third intended difference. - The ORG-4 auditor memoizes a lost claim for ORG_ADMIN_AUDIT_LOSS_TTL_MS (30s), not the whole window: a loss observed while the winner's write is in flight, and then fails, no longer suppresses this process's audit for the rest of the window. Wins stay memoized for the window. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WeQmMxjvSjnNikMbyMpgmD
2witstudios
left a comment
There was a problem hiding this comment.
[independent-review] agent:ow-irv-2673b
Re-review of #2673 (B7b) at ef594e12767e55d8d9693dd27b98d816c9eeca09, covering the delta 7d094f204..ef594e127 (one commit) against my review 5262822286 and the point-guard rulings.
Evidence comes from a throwaway homebrew pg17 (127.0.0.1:5673, session TZ America/Chicago, freshly migrated), since stopped and deleted. I changed no code; probes were git-excluded and removed, and git status was clean after every mutant.
Verdict: every ruled item is FIXED. No new findings: 0 P1, 0 P2, 0 P3.
| Finding (5262822286) | Ruling | Status | Evidence |
|---|---|---|---|
P2: a role entry with canView:false still grants edit in resolvePagePermissionRow |
fix on #2673 with #2672's line, plus the shape in the matrix | FIXED | See (1) |
| P3-1: a memoized claim loss suppresses later audits | short TTL on losses, with the probe as a test | FIXED | See (2) |
| P3-2: stale dark-flag sentence in the PR body | correct the body | FIXED | See (3) |
P3-3: master vs pu/org-wallets conflicts |
point-guard's master-sync PR | out of scope here | not reviewed |
(1) P2 is fixed.
permissions.ts:1048is nowreturn resolved.canView ? { ...resolved, canDelete: false } : null;, byte-identical to master's #2672 line.- The matrix fixture gives Product's default role a Board-deck entry
{canView:false, canEdit:true}. The enabled matrix asserts DENY for nina, kai and marcus through the default role. - Dark equivalence now expects exactly five differences. The new one is Marcus's edit-without-view on the Board deck, which master also no longer grants.
resolve-page-permission-row.test.tscarries #2672's unit case.- My mutant, by line index with the control green (22/22): line 1048 back to
return { ...resolved, canDelete: false };turns three tests RED:org-drive-sibling-resolvers.integration.test.ts:313(dark equivalence);:341(the enabled consistency matrix);resolve-page-permission-row.test.ts:132.
- My extended matrix (Ola, Pia and Rex from my last review) on the new fixture: 16 people × 6 drives, every resolver agrees.
(2) P3-1 is fixed. settled is now a Map from key to the time the settlement stays trusted. A win is trusted until the window's end; a loss only for ORG_ADMIN_AUDIT_LOSS_TTL_MS (30s), after which the next access asks the claim store again.
- My probe, through the real resolvers (
getUserAccessLevel, two module graphs):- A wins and its
logEventhangs; B loses. - A's write fails, and the claim is released.
- B at TTL−1s writes 0 rows.
- B at TTL+1s writes exactly 1.
- Further B and A accesses in the window write none, and the next window writes a second row.
- A wins and its
- Mutants, with the control green:
- A loss trusted for the whole window (
current.expiresAtfor both outcomes): RED, in 2 unit tests and the new integration test (:198). - A loss never trusted (TTL 0): RED, in the unit test that checks the claim store is not asked again inside the TTL (
:132).
- A loss trusted for the whole window (
- Load stays bounded: at most one claim-store round trip per (user, PRIVATE drive) per 30s per process for a loser, and none for a winner inside its window. The unit test asserts 11 claims across 10 lapsed re-checks.
- The residual is inherent: a loser whose only access falls inside the TTL, with no later access anywhere, cannot know the winner failed. That is now bounded to 30s instead of 15 minutes.
(3) P3-2 is fixed. The body now names four dark-visible fixes: pending invitations, the private-page drive-wide guard, the view-denying role entry and expired shares. It says dark equivalence has exactly five intended differences, and that the expired-share fix is covered by its own test because listAccessibleDrives is not a routed sibling. The first three are on master through #2672; the expired-share filter is not.
Other checks.
- Every Spec ID in the new test titles is
(partial); the only plain mentions are in code comments. ORG-4 stays partial until B7c. - The allowlist is unchanged, and there is no migration.
Suites and CI.
- My runs at head: sibling-resolvers 7/7, B7 resolvers 8/8, audit integration 4/4 and unit 8/8, revocation 3/3, demotion 5/5, drive-service 40/40, batch-page-permissions 22/22, resolve-page-permission-row 15/15, seams 3 files. Total: 12 files, 145/145.
- CI on this head:
- Test Suite run 35558047032 is green: Unit Tests (lib
618 passed | 5 skipped), Lint & TypeScript, E2E, and Spec ID coverage (ORG-4 allowlisted,ORG-6 covered,covered 10/87 … MISSING 0,spec-coverage: OK). - Security Tests run 35558046814 is green.
- Test Suite run 35558047032 is green: Unit Tests (lib
I have not resolved any threads.
B7b: every access resolver answers with one org-aware membership model
Leaf
gdk2ok994er5or61l6crpklc(Phase 1 taskyn7155euqcungw4qoht18jio). Basepu/org-walletsatde988390e(#2669, B7); merge-base equals that tip. Wave F (orgs on in staging) waits on this PR, and on B7c for the apps/web inline gates.What changed
1. One membership model for every sibling resolver.
resolveEffectiveDriveMemberships(permissions/org-drive-membership.ts) is the one IO edge over B7's pureresolveEffectiveDriveMembership. It handles one or many (user, drive) pairs. While dark, or for personal drives, it runs no query. When enabled it adds one org-roles query and one default-roles query per batch.loadEffectiveDriveMembershipis now a single-pair call into it.getUserDriveAccess,isUserDriveMember,isDriveOwnerOrAdmin,getUserDrivePermissions,getUserAccessiblePagesInDrive,getUserAccessiblePagesInDriveWithDetails,getBatchPagePermissions,getUsersWhoCanViewPage,getDriveIdsForUser.hasAppDriveMembershipandhasScopedDriveMembershipfollow throughisUserDriveMember.checkDriveAccess,checkDriveAccessForRoles,resolveGranterAccess,getPageIfCanShare,getMemberCustomRoleId,isUserMemberOfAnyEventDrive,resolveDriveMembership(agent workspaces).withEffectiveMembershipswaps each row's joineddrive_membersrow for the effective membership. It resolves each (user, drive) pair once. It also loads the permissions of a default custom role the query did not join.getDriveIdsForUser. While enabled it returns exactly the drive set oflistAccessibleDrives({ includeTrash: true }), decided bydecideListedDriveRole. A RESTRICTED or PRIVATE org drive is included only once joined, and never through a page share alone. Its callers (mentions search, calendar, sessions, principal drive ids) aggregate only over drives the user has listed.drive_agent_membersrows.2. Former lead's OWNER row.
reassignLedOrgDrivesdeletes the former lead's OWNER row on every drive it reassigns, in the reassignment transaction.leaveOrganizationdeletes the leaver's OWNER rows on all of the org's drives, reported asrevoked.formerLeadOwnerRows.updateDriveLastAccessedwrites the self-heal row only for a personal drive.drive_memberswithrole = 'OWNER'joined todriveswithorgId IS NOT NULL) returns 0 rows on a freshly migrated database. The e2e Northwind seed creates no org drive, and every route that setsdrives.orgId(api/drivesPOST,api/drives/[driveId]/org) returns 404 whileORGS_ENABLEDis false. So no environment can hold such a row today. I did not run it against production.3. ORG-4 audit dedupe.
permissions/org-admin-access-audit.tswritesauthz.access.grantedat most once per (user, PRIVATE org drive) per 15-minute UTC window.rate_limit_buckets, keyed byaudit:org-admin-private-drive:<user>:<drive>plus the window start truncated to the 15-minute UTC boundary. That pair is the table's primary key, and the insert usesON CONFLICT DO NOTHING. Only the caller whose insert lands writes the event.getUserAccessLevel), AI tool checks, drive room joins, search's drive gate and per-hit checks, and batch checks. Audience computations (getUsersWhoCanViewPage) passaudit: false: nobody accessed anything.4. Removal and demotion.
removeMemberalready runs the leave cascade (2ba9e10). That cascade is nowrevokeOrgDriveGrants(tx, userId, driveIds): agent grants, drive and page share links, MCP key drive rows, and OAuth grants.org_access_revoked.changeMemberRolerunsrevokeForDemotionin the role-change transaction. PureplanDemotionRevocationcompares effective access before and after, per org drive:Access gone (a drive org power alone opened): the full
revokeOrgDriveGrants.ADMIN became MEMBER (an OPEN drive): only what a Member could not create:
memberCouldSharePagemirrorsgetUserAccessLevel);recapAgentMembershipsGrantedBy, now transaction-aware with a known cap).Inheriting key and OAuth scopes stay, because they follow the person.
Otherwise: nothing. This covers a row still backing the access, a drive they lead, Owner to Admin, and promotion.
Fixes the consistency matrix and review surfaced. All four change behaviour while orgs are dark. Master already has the last three (#2672); only the expired-share fix is missing from master.
listAccessibleDrives(both the dark and org bodies) ignoredpagePermissions.expiresAt. An expired page share still listed its drive in the picker,/api/drivesand MCPlist_drives, whilegetDriveIdsForUserand every access resolver refused it. Both bodies now filter expired shares. Master has the same unfiltered query (drive-service.tspermissionDrives), so it differs there too. Matrix fixture: Lu's expired share on Marcus Notes; testan expired page share lists no drive, dark or enabled.checkDriveAccessForRolesread pending rows, so a pending ADMIN invitation could manage roles. It now uses the shared loader, which gates onacceptedAt. The point-guard ruled: routed functions apply the gate, with a pending-invite test on each. A separate lane,fix-accepted-invite-gate, fixes master.resolvePagePermissionRow(behindgetBatchPagePermissionsandgetUsersWhoCanViewPage: search, sidebar badges, inbox,permissions/batch, channel fan-out) let a custom role'sdriveWidePermissions.canViewopen a PRIVATE page.getUserAccessLevel, the app-token resolver and the agent resolver all refuse that.resolve-page-permission-row.test.tshad pinned the leaking behaviour; that test is changed here.resolvePagePermissionRowalso returned a custom-role entry whosecanViewis false, so{canView:false, canEdit:true}granted edit throughgetBatchPagePermissionswhilegetUserAccessLevelreturned null. It now takes master's fix(permissions): pending drive invites grant nothing; a custom role's drive-wide grant never opens a private page #2672 line (return resolved.canView ? { ...resolved, canDelete: false } : null;) and its unit testgrants nothing when the custom role entry denies view, whatever else it sets. Matrix fixture: Product's default role hides a Board deck page while settingcanEditon it.Requirement IDs and tests
ORG-4 (partial) DRV-5 (partial) DRV-6 (partial) X-6 (partial) the consistency matrix…·ORG-4 (partial) agent and app identities never gain org-derived access…(org-drive-sibling-resolvers.integration) ·ORG-4 (partial) five resolver calls in one window write one audit row…·ORG-4 (partial) the claim is a unique key in Postgres…(org-admin-access-audit.integration) · 4×ORG-4 (partial)(org-admin-access-audit.test) · removal and demotion 2× (org-member-revocation.integration) · 4× (demotion.test)DRV-5 (partial) an implicit Open member holds the drive default role in every custom-role reader…DRV-7 (partial) resolveGranterAccess follows the shared membership…getDriveIdsForUsernow equals the accessible-drives listing (matrix asserts it); the picker grouping UI is Wave FORG-6 (partial) a lead who leaves the org keeps no OWNER row on its drives·ORG-6 (partial) reassigning a lead's org drives … removes their OWNER row…·ORG-6 (partial) updateDriveLastAccessed never writes an OWNER self-heal row for an org drive…(leave.integration)while ORGS_ENABLED is false every routed sibling returns exactly the pre-B7b result, except the three named fixes…·a pending invitation counts for nothing in any routed resolver, dark or enabledORG-4 is still
(partial), so the allowlist is unchanged.isOwner, e.g. rename or restore drive) stay closed to the org Owner.(partial)until B7c lands.Dark-flag equivalence.
__tests__/fixtures/pre-b7b-sibling-resolvers.tsis a frozen copy of all 16 routed function bodies atde988390e, extracted mechanically. Only the names are prefixedlegacy.ORGS_ENABLEDfalse. That is more than 1000 comparisons, and the test asserts that count.checkDriveAccessForRolesfor Tomás's two pending invitations;getBatchPagePermissionsandgetUsersWhoCanViewPage;getBatchPagePermissions.listAccessibleDrivesis not a routed sibling. Its own test,an expired page share lists no drive, dark or enabled, covers it dark.Mutation checks (line index at
2b0c4bf7dunless noted; each with a no-op control GREEN;git statusclean after)permissions.ts:433isUserDriveMemberreadsdrive_membersdirectly instead of the shared loaderpermissions.ts:1097withEffectiveMembershipalways returns the joined rowspermissions.ts:1047private-page guard inresolvePagePermissionRowremovedpermissions.ts:156if (listed !== null)removed ingetDriveIdsForUserWithOrgspermissions.ts:50if (ORGS_ENABLED)gate removeddrive-role-service.ts:122back todb.query.driveMembers.findFirstwithout the acceptedAt gateorg-admin-access-audit.ts:93if (won) write(access)→write(access)org-admin-access-audit.ts:105claim always winsorg-admin-access-audit.ts:44no window truncationorg-drive-membership.ts:138dropoptions.audit &&drive-service.ts:656drop&& drive.orgId === nullORG-6 (partial) updateDriveLastAccessed…leave.ts:118reassign deletes the OWNER row of'__nobody__'ORG-6 (partial) reassigning…leave.ts:119reassign deletes roleMEMBERinstead ofOWNERORG-6 (partial) reassigning…leave.ts:285leave deletes roleMEMBERinstead ofOWNERORG-6 (partial) a lead who leaves…, O-8 guest survives leaveleave.ts:189OAuth family revocation skippedleave.ts:225scopesNameDrivematches any parseable scopemembership.ts:147demotion revocation never runsdemotion.ts:73access-gone drives not revokeddemotion.ts:74ADMIN→MEMBER drives not cappeddemotion.ts:165page-link check skippeddemotion.ts:98explicitcanShareignoreddemotion.ts:129(d1e6edd) accepted rows → empty mapdemotion.ts:129(d1e6edd) plantedtx.select().from(driveMembers)org-admin-access-audit.ts:116(7d094f2) claim release removedorg-admin-access-audit.ts:114(7d094f2) memo entry kept after a failed writedrive-service.ts:120(7d094f2) expiry filter removed, dark bodydrive-service.ts:217(7d094f2) expiry filter removed, org bodypermissions.ts:1048(ef594e1) back to the ungatedreturn { ...resolved, canDelete: false };[false,true,false,false]for Eve, Kai, Nina, Tomás and Marcus), dark equivalence,resolve-page-permission-row.test.tsorg-admin-access-audit.ts:123(ef594e1) a lost claim memoized for the whole window (current.expiresAt)Review follow-ups
d1e6edd9e: CI Unit Tests caught the seam guard ondemotion.ts'sdrive_membersread. It now goes throughloadAcceptedRowsInDrivesin the permissions seam. Seam test: 3 passed (3).7d094f204: review 5237996587 P2s (audit write/claim release; theORG-4 (partial) audit dedupedescribe title) and the expired-share listing fix.drive-service.test.ts+acceptedAt-gate.test.ts: 64 passed (64).ef594e127: review 5262822286. P2:resolvePagePermissionRowgates the custom-role result oncanView(fix(permissions): pending drive invites grant nothing; a custom role's drive-wide grant never opens a private page #2672's line), with the Board deck shape in the matrix fixture. P3-1: a lost audit claim is memoized forORG_ADMIN_AUDIT_LOSS_TTL_MS(30s) instead of the whole window, so a process that lost while the winner's write was in flight, and then failed, writes the record on a later access in the same window. Wins stay memoized for the window. P3-2: this body's dark-flag sentences.ef594e127(throwaway homebrew postgresql@17 on 127.0.0.1:5499, session TZ America/Chicago, stopped and deleted after):org-drive-sibling-resolvers.integration.test.ts7 passed (7) ·org-admin-access-audit.integration.test.ts4 passed (4) ·org-admin-access-audit.test.ts8 passed (8) ·resolve-page-permission-row.test.ts15 passed (15) ·bunx tsc --noEmit -p tsconfig.json(packages/lib) 0 errors, with a probe error caught ·bunx eslinton the two touched sources: 0 errors.Commands and results (local; build slot respected, CI is the gate)
Throwaway homebrew postgresql@17 on 127.0.0.1:5498 (session TZ America/Chicago), migrated with
bun run --filter @pagespace/db db:migrate. The worktree source harness (vitest.wt*.config.ts, a@pagespace/dbsource alias) is git-excluded.DATABASE_URL=postgresql://user@127.0.0.1:5498/pagespace_test bunx vitest run --config vitest.wt-int.config.ts <file>(packages/lib), at2b0c4bf7d:org-drive-sibling-resolvers.integration.test.ts: 7 passed (7) at7d094f204org-admin-access-audit.integration.test.ts: 3 passed (3) at7d094f204org-admin-access-audit.test.ts(unit): 6 passed (6) at7d094f204org-member-revocation.integration.test.ts: 3 passed (3)leave.integration.test.ts: 12 passed (12)org-services.integration.test.ts: 21 passed (21)org-drive-resolvers.integration.test.ts(B7, now on the shared fixture): 8 passed (8)org-membership-sync.integration.test.ts: 10 passed (10)page-viewers.integration.test.ts: 9 passed (9)bunx vitest run --config vitest.wt.config.ts src/permissions src/services src/organizations src/__tests__/acceptedAt-gate.test.ts src/agent-workspaces src/auth --exclude '**/*.integration.test.ts'gives 245 files, 5792 passed | 1 todo.bunx vitest run --config vitest.wt.config.ts src/app/api/orgs "src/app/api/drives/[driveId]/roles" "src/app/api/drives/[driveId]/members" src/app/api/calendar src/app/api/agent-workspaces "src/app/api/drives/[driveId]/agents" "src/app/api/pages/[pageId]/permissions" src/app/api/__tests__/security-audit-coverage.test.ts src/lib/dev-preview/__tests__/manage-decision.test.tsgives 41 files, 755 passed.bunx tsc -p tsconfig.wt.json(packages/lib, tests and fixtures included) reports 0 errors. A probe with an injected type error was caught, so the check is live.bunx eslinton every touched lib source file: 0 errors.__tests__are knip-ignored; every new export is used in its own file or by lib code), and E2E.vitest.config.tsexcludes (unit run and coverage), so they run in ci.yml's libtest:integrationstep.Callers of changed functions and what each now sees
Unless stated, the effects apply only while
ORGS_ENABLEDis on. While dark, every caller sees exactly today's result, except the four fixes above (pending invitations, the private-page drive-wide guard, the view-denying role entry, and expired shares inlistAccessibleDrives).getBatchPagePermissions:api/search,api/sidebar/badges,api/inbox,api/permissions/batch,api/agent-workspaces/conversations,api/storage/infoandlib/storage/storage-info-core,lib/commands/available-commands,lib/auth/principal-permissions, libtags/tag-service. Cross-drive aggregates remain per-page filtered.getUsersWhoCanViewPage:api/channels/[pageId]/messages(channel fan-out). It writes no audit.getUserAccessiblePagesInDrive(+WithDetails):api/pages/tree,lib/ai/core/page-tree-context,lib/ai/tools/actor-permissions,lib/auth/principal-permissions, libapp-permissions(inheriting tokens).getUserDriveAccess: realtimeindex.ts(drive room joins),app-logs/app-log-handler; webapi/drives/[driveId]/assignees,api/mentions/search,api/storage/info,lib/ai/core/agent-awareness,page-tree-context,actor-permissions,principal-permissions; libdrive-search-service(search gate),drive-agent-service. (hooks/usePageContentSocketonly names it in a comment.)isUserDriveMember:api/commands/{resolve,suggest},api/drives/[driveId]/apps/[tokenId],api/workflows/agents,lib/ai/core/command-resolver; AI toolsactivity,calendar-read,calendar-write,skill,trigger;principal-permissions,lib/commands/{available-commands,command-catalog-loader}, zoom/page-webhook/calendar trigger executors,workflow-executor; libapp-permissions(hasAppDriveMembership,hasScopedDriveMembership),file-access,share-link-service,calendar-event-drive-service,tag-service.isDriveOwnerOrAdmin: backups routes (backups/[backupId]/pages,drives/[driveId]/backups/**,schedule),api/commandsand[commandId],drives/[driveId]/history,lib/ai/tools/command-tools,principal-permissions,lib/dev-preview/manage-decision,services/api/{backup-export-service,drive-backup-service,page-service}; librollback-permissions,share-link-service(drive share links),calendar-event-drive-service,drive-agent-service,drive-envs/local-env-gate.getUserDrivePermissions: processorservices/{authorization,rbac}; webapi/ai/page-agents/multi-drive(+sandbox-eligibility-by-drive),api/auth/key,api/upload/{complete,presign},lib/agent-workspaces/principal-code-exec-access,principal-permissions; libsandbox/can-run-code,validated-service-token. An implicit member's drive-wide edit follows the default role.checkDriveAccess:api/drives/[driveId]/agents/**,apps/**,members/**,api/integrations/zoom/triggers,api/workflows/**,lib/ai/tools/{actor-permissions,agent-tools,member-tools},lib/repositories/drive-invite-repository.checkDriveAccessForRoles:api/drives/[driveId]/roles/**,lib/ai/tools/role-management-tools. A pending invitation no longer manages roles (dark too).resolveDriveMembership: realtimeindex.tsanddev-preview/preview-runtime; webapi/agent-workspaces/conversations,lib/agent-workspaces/agent-workspaces-runtime,lib/ai/tools/session-tools-runtime,lib/dev-preview/preview-runtime; libagent-workspace-access,sandbox/preview/{preview-access,dev-preview-status}.isUserMemberOfAnyEventDrive:api/calendar/events/[eventId]/**,lib/ai/tools/calendar-read-tools.getMemberCustomRoleId:lib/auth/oauth-grant-authority,principal-permissions, libdrive-service(validateDriveScopeAccess). An implicit member's own custom role is the default role.getPageIfCanShare(viagrantPagePermissionandrevokePagePermission):api/pages/[pageId]/permissions, librevocation-kick. An org Admin can grant or revoke page permissions on org drives.resolveGranterAccess(viaaddAgentToDriveandrecapAgentMembershipsGrantedBy):api/ai/page-agents/[agentId]/drives,api/drives/[driveId]/agents,api/drives/[driveId]/members/[userId]. Org power grants agents up to ADMIN; an implicit member is capped to MEMBER with the default role.getDriveIdsForUser:api/auth/key,api/mentions/search,lib/ai/tools/{calendar-read-tools,session-tools-runtime,session-tools},principal-permissions. It returns the accessible-drives set (see above).updateDriveLastAccessed:api/drives/[driveId]/access. On an org drive it no longer writes an OWNER row (orgs dark: no org drives exist).leaveOrganization,leaveAllOrganizations,reassignLedOrgDrives,removeMember,changeMemberRole:api/orgs/[orgId]/members/[userId], librepositories/account-repository(account deletion). They revoke OAuth families that name the org's drives, and OWNER rows. Demotion revokes as described./api/drives, MCP routes, OAuth). Inheriting MCP and OAuth scopes follow the person. An explicit key role is unchanged.Not in this PR
kxgdqsjqyrlyvcytnhlbdllz): apps/web inlinedrive_members/drives.ownerIdgates. They fail closed for org power but honour a stalesource='org'row:drives/[driveId]/{trash,permissions-tree,pages,members/invite},pages/{tree,bulk-copy,bulk-move},account/handle-drive;services/api/{page-reorder-service,permission-management-service},lib/repositories/drive-invite-repository.findAdminMembership,lib/auth/revoke-adapters;messages/threads(fetchChannelsWithLastMessage),lib/users/visibility,commandsandcommand-toolsgetMemberDriveIds;admin/global-prompt,users/messageable,sidebar/badgesandinboxpre-filters (their decision isgetBatchPagePermissions, routed here).drives.ownerIdonly, correct for a lead and closed to the org Owner):drives/[driveId]/restore,trash/drives/[driveId],ai/page-agents/create,drive-toolsrename_drive,drive-repository.findByIdAndOwner.getDriveRecipientUserIdsandgetDriveMemberUserIds(drive broadcast recipients),app-shell-service.fetchShellDriveIds,usersShareDrive(DM eligibility, deliberately without the acceptedAt gate). While orgs are on, an implicit member gets no drive-level broadcast or shell entry (fail closed), and a stale org row still receives drive broadcasts until the sync removes it. These belong with B7c.ORGS_ENABLED=falseon the integration branch. On master, the acceptedAt gate is fixed by lanefix-accepted-invite-gate; the private-page leak is reported in #org-wallets for a master ruling.🤖 Generated with Claude Code
https://claude.ai/code/session_011VHYwVCUxjqUhx9Z5yaxwn