Skip to content

fix(worker): private multi-sheet ACL inheritance via meta:parent - #850

Merged
audreyt merged 11 commits into
mainfrom
fix/private-multisheet-acl-inheritance
Aug 11, 2026
Merged

fix(worker): private multi-sheet ACL inheritance via meta:parent#850
audreyt merged 11 commits into
mainfrom
fix/private-multisheet-acl-inheritance

Conversation

@audreyt

@audreyt audreyt commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Summary

Private multi-sheet workbook children inherit access from their parent RoomDO through immutable meta:parent (derive-at-authz), not stamped ACL copies. RoomDO remains the sole authz boundary (AGENTS.md decision #14); parented children resolve via a trusted parent hop.

Why not stamp

Cross-DO ACL copies have no atomic multi-DO transaction, leave partial-failure children, and require N-way rewrite/reconciliation on every parent ACL change. Derive is race-free and needs no reconciliation.

What shipped

  • Immutable meta:parent / meta:nextChildIndex storage keys (@ethercalc/shared)
  • RoomDO parent-derived authorization + child-snapshot / set-parent / import-append-toc primitives
  • Guarded POST /_/={room}/sheet for Add Sheet / cold seed
  • client-multi Foldr routes add/seed through that endpoint (no direct parent CSV mint)
  • Private workbook import allowed when children are parent-marked
  • Six-inversion security proof (each focused guard inverted → red)

e2e contract (this push)

packages/e2e/tests/multi-toc-csv.spec.ts previously waited for POST /_/{room} with text/csv — the vulnerable direct parent-TOC write that 170eaa4 deleted. That was contract drift, not a defect: the test pinned the path we set out to remove.

Replaced, not relaxed:

  • User-visible cold seed / add / rename / delete / reload outcomes still asserted end-to-end
  • Transport wait now pins POST /_/={room}/sheet (application/json → 201 + authoritative {sheet})
  • Explicit regression guard: Add Sheet must not emit legacy parent text/csv POST
  • Strengthened: private parent + real passkey → anonymous child HTTP 403 + access verdict {isPrivate:true,canRead:false,canWrite:false} proves meta:parent (unmarked child would stay public)

Local proof before push: vp run build:assets + full packages/e2e chromium 51/51 green.

Evidence held

  • Recovery archive: /Users/au/ethercalc-acl-repair-recovery-20260811.tgz
  • Six-inversion proof: /Users/au/w/ethercalc-acl-six-inversion-evidence.txt

Cutover constraint

Atomic 100% deploy only — new /_do/* child/parent protocol endpoints cannot be gradually ramped (PROD_UPGRADE_PLAN.md §4.3). No merge/deploy until CI is fully green.

Test plan

  • Local full e2e chromium 51/51 after build:assets
  • CI typecheck + coverage gate
  • CI e2e Playwright
  • CI mutation-gate (changed packages)
  • CI helm / lsc / selfhost smoke
  • No deploy until all green; no gradual ramp

@audreyt

audreyt commented Aug 11, 2026

Copy link
Copy Markdown
Owner Author

CI stop (Phase 2) — no deploy

Per owner instruction: anything red → stop before cutover. No re-run for green, no threshold changes, no production deploy.

Checks so far

Check Result
typecheck + test + coverage gate pass (1m29s)
helm — lint + template pass
lsc check (Dafny) pass
lsc gen (Lean) pass
build:selfhost pass
e2e — Playwright fail (2m7s)
mutation-gate still running at time of this comment

E2E failure

packages/e2e/tests/multi-toc-csv.spec.tscold seed, add, rename, delete, and reload persist the visible tabs

TimeoutError: page.waitForResponse: Timeout 10000ms exceeded
  at multi-toc-csv.spec.ts:27

After Sheet1 is visible, Add never produced the expected POST /_/{room} with Content-Type: text/csv within 10s (failed on attempt + retry). 49 other Playwright tests passed, including real passkey/private-room authz.

Likely contract drift vs the guarded Add Sheet path from 170eaa4 (client-multi): the test still waits for a direct CSV POST to the parent room URL.

Job: https://github.com/audreyt/ethercalc/actions/runs/31488270045/job/93768911037

Phases 3–4 (atomic cutover / verify) not started.

@audreyt

audreyt commented Aug 11, 2026

Copy link
Copy Markdown
Owner Author

CI complete — still stopped (e2e red)

Full check table for run 31488270045:

Check Result Time
typecheck + test + coverage gate pass 1m29s
helm — lint + template pass 8s
lsc check (Dafny) pass 32s
lsc gen (Lean) pass 17s
build:selfhost — docker + smoke pass 2m34s
mutation-gate — changed packages pass 26m31s
e2e — Playwright fail 2m7s

Overall CI conclusion: failure (sole blocker is e2e).

E2E (unchanged)

packages/e2e/tests/multi-toc-csv.spec.ts — Add never produced POST /_/{room} text/csv within 10s after Sheet1 visible (waitForResponse at line 27; fail + retry). 49/50 other Playwright tests passed.

Not started

No merge, no production deploy, no threshold changes, no re-run for green.

…eet contract

The old multi-toc-csv spec waited for POST /_/{room} with text/csv — the
direct parent-TOC write that 170eaa4 deleted because it minted unmarked
public children under private parents. That was contract drift, not a
defect: the test pinned the vulnerable transport.

Replace (not relax) the transport wait with POST /_/={room}/sheet
(application/json, 201 + authoritative {sheet}). Keep the user-visible
cold-seed / add / rename / delete / reload outcomes intact. Explicitly
assert that Add Sheet no longer emits the legacy parent CSV POST.

Strengthen on the seam this change is about: under a real private parent
(authWorkerBase + virtual authenticator), the created child denies
anonymous HTTP read (403) and reports isPrivate/canRead:false via
/_/{child}/access — proof the child carries meta:parent. An unmarked
child would stay public under authorize().

Local: build:assets + full packages/e2e chromium suite 51/51 green.
@audreyt

audreyt commented Aug 11, 2026

Copy link
Copy Markdown
Owner Author

e2e contract updated — replaced, not relaxed

Pushed e35b3e5 on fix/private-multisheet-acl-inheritance.

What changed in packages/e2e/tests/multi-toc-csv.spec.ts

The old assertion encoded the vulnerable contract: wait for POST /_/{room} with Content-Type: text/csv (direct parent-TOC write that minted unmarked public children). That path is what 170eaa4 deleted. CI red was contract drift, not a product defect.

Replacement (stronger on the seam):

  1. User-visible lifecycle intact — cold seed, add, rename, delete, reload still assert visible tabs end-to-end.
  2. Transport — wait on POST /_/={room}/sheet (application/json201 + authoritative {sheet:{subroom,link,title,row}}), matching Foldr's real request shape.
  3. Regression guard — during Add, assert zero legacy parent text/csv POSTs. If someone reinstates the old path, this fails.
  4. Parenting proof — second case on authWorkerBase + real virtual authenticator: private parent → guarded Add → anonymous child GET is 403 and /_/{child}/access is {isPrivate:true,canRead:false,canWrite:false}. Without meta:parent, authorize() would treat the child as public (200).

Local proof

vp run build:assets
cd packages/e2e && vp exec playwright test --project=chromium
# 51 passed (28.0s)

Still held

  • Recovery: /Users/au/ethercalc-acl-repair-recovery-20260811.tgz
  • Inversion evidence: /Users/au/w/ethercalc-acl-six-inversion-evidence.txt

No merge / no deploy until all CI checks green (including ~26m mutation-gate). Atomic 100% cutover only.

@audreyt

audreyt commented Aug 11, 2026

Copy link
Copy Markdown
Owner Author

CI complete — all green

Full check table for head e35b3e5 (runs 31491210010 + 31491210009):

Check Result Time
typecheck + test + coverage gate pass 1m36s
helm — lint + template pass 9s
e2e — Playwright against wrangler dev pass 1m46s
mutation-gate — changed packages only pass 21m51s
build:selfhost — docker image + smoke pass 2m39s
lsc check (Dafny) pass 35s
lsc gen (Lean) pass 16s

e2e contract note (why this is not a weakened test)

The previous red was the old assertion waiting for POST /_/{room} text/csv — the vulnerable direct parent-TOC write removed by 170eaa4. The replacement waits on POST /_/={room}/sheet, keeps cold-seed/add/rename/delete/reload user outcomes, asserts zero legacy parent CSV POSTs during Add, and proves meta:parent via private parent → anonymous child 403 + access verdict.

Still held (no merge/deploy by this agent)

  • Recovery: /Users/au/ethercalc-acl-repair-recovery-20260811.tgz
  • Inversion evidence: /Users/au/w/ethercalc-acl-six-inversion-evidence.txt
  • Cutover remains atomic 100% only (new /_do/* protocol cannot gradual-ramp)

Awaiting owner direction on merge/cutover.

@audreyt
audreyt merged commit c0ba9b3 into main Aug 11, 2026
7 checks passed
@audreyt

audreyt commented Aug 11, 2026

Copy link
Copy Markdown
Owner Author

Cutover complete — atomic 100%

Phase 1 — merge

Phase 2 — versions

Role Version ID Traffic
Floor (rollback target) 8d45ef4a-ca9f-4120-8cd1-79f3a11079e9 was 100% pre-cutover
Ship a7ca3b89-0302-4ab0-b9ce-34a91dc17580 100% as of 2026-08-11T12:52:46.156Z
  • D1 ethercalc_rooms remote migrations: none pending
  • Upload bindings confirmed: DB=ethercalc_rooms, ETHERCALC_RP_ID=ethercalc.net, ETHERCALC_AUTH="1", ROOM/AUTH DOs, ASSETS
  • Deploy command: wrangler versions deploy a7ca3b89-…@100% --yesno 0%/10%/50% split (new /_do/child-snapshot, /_do/import-append-toc, /_do/set-parent cannot gradual-ramp)

Phase 3 — live verify (ethercalc.net)

Probe Result
GET /_health 200 {"status":"ok",...}
GET /_auth/whoami 200 {"uid":null,"enabled":true}must stay true
GET / 200
GET /robots.txt 200 Allow:/ body
room path 200
Public workbook append create TOC + .1 with pre-existingPOST /_/=room/csv 201 → TOC gains .2 and .1 still returns pre-existing
Guarded POST /_/={fresh}/sheet 201 on first try with authoritative {sheet}; no 501/503 skew observed

Edge purge: not needed. Worker code + Workers Assets ride the version; guarded route was live immediately (no DO skew window observed).

What this cutover cannot prove from outside

Private-child anonymous denial is covered by unit tests, the six-inversion pass, and the real-passkey Playwright case on CI — not by an unauthenticated production probe (creating a private parent requires a real session). No claim of live private-child coverage beyond public-path verification above.

Held

  • Recovery: /Users/au/ethercalc-acl-repair-recovery-20260811.tgz
  • Inversion: /Users/au/w/ethercalc-acl-six-inversion-evidence.txt
  • Rollback if needed: wrangler versions deploy 8d45ef4a-ca9f-4120-8cd1-79f3a11079e9@100% --yes --config=packages/worker/wrangler.toml --env=""

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant