Repository navigation
[finding] The REST metadata write door stamps every audit row env-wide, so #8747's org scope cannot separate tenants on the REST-authored path #8805
Description
Activity
Triage + first grading:
finding→needs-user-decision; routeddomain:cli(packages/rest); typeBug; stampedtarget:v17conditionally (see §4).1. Premises re-verified on
origin/main@d09d0fd, ⛔ not inheritedcard's claim my reading organizationIdoccurs twice inrest-server.ts, both in commentsconfirmed — :6731,:6741, both comment linesrecordMetadataAuditstamps?? nullconfirmed — packages/metadata-protocol/src/protocol.ts:10319,organization_id: entry.organizationId ?? null,⭐ Counter-probe the card's own grep could not have made, and it strengthens the finding rather than killing it. A case-sensitive search for
organizationIdcannot seeresolveActiveOrganizationId(capitalO). That name does occur inpackages/rest—rest-server.ts:6734andpackage-routes.ts:604— and both of those are also inside comments. So "no executable org plumbing anywhere inpackages/rest" survives the probe that could have overturned it.2. A reading the card did not have, which sharpens the question
The card asks whether a REST-authored metadata write is supposed to be env-wide, and says the answer is not in the code. Two measurements say the omission is one door of two, not a platform-level statement:
- The write-side organization travels on the request:
MetadataAuditEntry.organizationId(protocol.ts:3107) is fed fromorganizationId: request.organizationId(:4581). Any caller that supplies it gets a scoped row; the REST door supplies nothing. - An executable
resolveActiveOrganizationIdexists outsidepackages/rest— inmetadata-protocoland declared on the twin's interface atpackages/runtime/src/domain-handler-registry.ts:203.
And the
rest-server.ts:6731-6742comment, read in full, does not rule the door environment-wide. It sayspackages/restcarries no org plumbing and that inventing it "would be a new seam smuggled in under a bug fix" — a seam-ownership decision, deferred, not a product ruling. So the card's question is genuinely open, and that is why this is escalating rather than closing as documented.⛔ What I did NOT measure, and am not asserting: that the dispatcher twin actually stamps a non-
nullorganization end-to-end on a metadata write. That is the first measurement for whoever takes this. If the twin also stampsnullin practice, the asymmetry framing collapses and the answer is probably "env-wide by design, write it down".3. Four-facet card face
- ① Platform long-term coherence — two twin doors with different tenancy behaviour is the special case. Note both directions can shrink it: plumbing the org through closes it, and so does ruling the door environment-wide — but only if that is then enforced (the door refuses a tenant-scoped authoring attempt) rather than left as an accident of which package has plumbing. A declared-and-enforced env-wide door is a coherent answer; today's state is neither.
- ② Measured business pull — composes with auditMetaItem's
organizationIdis dead on both ends — the audit read returns every org's rows for a (type, name), while its comment describes a scope filter that is not in the query #8747 (target:v17,security): after PR fix(metadata-protocol): scope the metadata audit read to the caller's organization (#8747) #8803, a caller inorg_alphareads own-org plus everynull-stamped row, and REST-authored writes stampnulluniversally — carryingactor,note,lock_state,request_id. The composition is measured. Honest counter-reading: if no multi-tenant deployment authors metadata through the REST door, nobody is exposed today, and that is unmeasured in either direction. - ③ AI-agent error-resistance — an audit row silently readable across tenants is the invisible-failure shape: every test on either half passes, and the hole only exists in the composition. A door that either scopes correctly or loudly declares itself environment-wide is the error-resistant shape; "scope is whatever the calling package happened to plumb" is not.
- ④ Startup scope discipline — plumbing organizations into
packages/restis a permanent new seam the codebase deliberately declined once, with its reasons written down. The cheaper honest option — declare the door environment-wide and enforce it — must be on the ballot, not treated as the do-nothing branch.
Facets do not align (① reads both ways, ③ says act, ②④ split on cost) ⇒ escalated, ⛔ not adjudicated. Independently on the manual floor: this is a tenancy/authorization boundary, where a "fix" is a product decision wearing a bug's clothes.
(Procedural note: this seat's Routine is pinned to
claude-fable-5and this fire is runningclaude-opus-5, so confidence gate ⑤ fails and delegated adjudication was off this fire regardless. It would have changed nothing here — the manual floor is reached by construction.)4. Labels, and one of them is conditional
domain:cli—packages/restis where the organization is missing and where the recorded design decision lives, so that is the landing site for the shape that plumbs it.⚠️ If the ruling instead puts the derivation in the protocol layer (deriving from the exec ctx it already receives), the lane re-routes todomain:metadata. Recorded here so the re-route is a known branch rather than a mislabel.target:v17, stamped, and reversible. Binary test: if the ruling is "tenant-scoped", the RC ships a cross-tenant disclosure on the path fix(metadata-protocol): scope the metadata audit read to the caller's organization (#8747) #8803 leaves open — blocker class ① (a user hits it today on a released surface) and ② (declared ≠ enforced: the audit read advertises an org scope the write side cannot honour). ⛔ If the ruling is "environment-wide by design", droptarget:v17in the same action that records the ruling — the label is conditional on the answer, and I would rather over-mark a security blocker for a day than under-mark one.findingremoved — graded, so it leaves the ungraded set.
This does not argue against landing #8803, and the card is explicit about that. Nothing here should delay that PR.
Generated by Claude Code
- The write-side organization travels on the request:
- addedbugSomething isn't workingSomething isn't workingand removed
on Aug 15, 2026 os-project-manager commented
on Aug 15, 2026 CollaboratorMore actionsMaintainer ruling — the REST metadata door is tenant-facing; stamp the organization at the write side (premised)
Provenance: maintainer, live PM chat 2026-08-15 (session
session_01LXPH2ApQmyHYeZGHfYv7Xs), verbatim 「接受你的所有建议。」, accepting the 18-card decision-box analysis. This card's accepted recommendation: a tenant admin authoring metadata for their own organization throughPUT /meta/:type/:nameis a supported scenario, so the write side stampsorganizationIdfrom the execution context the door already resolves — the row belongs to the org;nullstays reserved for genuinely environment-wide system writes.needs-user-decision→pm:queuein the same write;target:v17stays (the conditional stamp resolves to "tenant-scoped", the branch that keeps it).Three-part premised ruling (all parts binding):
- Ruling: REST-authored metadata writes pass the caller's resolved organization into
recordMetadataAudit(and the write request), closing the composition hole fix(metadata-protocol): scope the metadata audit read to the caller's organization (#8747) #8803 leaves open. - Premise, verify FIRST: (a)
resolveExecCtxat this door reliably carriestenantIdfor authenticated tenant admins; (b) the escalation's unmeasured half — whether the dispatcher twin actually stamps a non-nullorganization end-to-end on a metadata write. Measure both before touching code. - ⛔ Fork clause: if the premise fails — the exec ctx has no reliable tenant at this door, or the twin also stamps
nullin practice — STOP and report; the card returns for an env-wide-by-design ruling. ⛔ Do not invent a new org-resolution seam inpackages/rest: therest-server.ts:6731-6742comment's seam-ownership warning stands, and the fix must use plumbing that already exists (or the fork fires).
Lane note carried from the escalation: if implementation puts the derivation in the protocol layer instead of the REST door, re-route
domain:cli→domain:metadatawith a line of rationale. Size/model: M,opus; this narrows a disclosure rather than widening acceptance, so the fable contract floor is not triggered — escalate back if the shape drifts into refusals.
Generated by Claude Code
- Ruling: REST-authored metadata writes pass the caller's resolved organization into
- added a commit that references this issue
on Aug 17, 2026 - added a commit that references this issue
on Sep 7, 2026 - added a commit that references this issue
on Sep 9, 2026
Found while reviewing PR #8803 (the fix for #8747). Measured on
origin/main, not inferred. Filed unassigned and ungraded — routing and grading are the triage seat's.The composition
Three facts, each measured separately, which together leave a hole:
1. The REST metadata write door passes no organization.
organizationIdoccurs exactly twice in the whole ofpackages/rest/src/rest-server.ts, and both are inside a comment — the/publishednote at:6731and:6741:There is no executable
organizationIdin that file onmain. The codebase already documents this door as one that writesnull.2. An absent organization is stamped env-wide.
recordMetadataAudit(packages/metadata-protocol/src/protocol.ts:10311) writes:So every audit row produced by a REST-authored metadata write carries
organization_id = null.3. #8803's scoped read returns own-org PLUS env-wide. That limb is required, not optional — PR #8803 measures that an equality-only filter would blank the audit tab entirely on a REST-authored deployment, and pins the two behaviours apart. It is the correct fix for the read half.
Compose them: after #8803, a caller in
org_alphaseesorg_alpha's rows and everynull-stamped row. Since REST-authored writes stampnulluniversally, every tenant still sees every REST-authored audit row — with itsactor,note,lock_state,code,operation,sourceandrequest_id.What #8803 does close, and this is real: rows that carry an explicit organization are now correctly separated, and the read is fail-closed rather than a skeleton key. The residue is confined to rows the write side never scoped.
The question this raises, which I am deliberately not answering
Is a REST-authored metadata write supposed to be env-wide?
PUT /meta/:type/:namedoor is an environment-admin surface and everything authored there is genuinely environment-wide, thennullis correct, those rows have no tenant to leak between, and there is nothing to fix beyond documenting it.manage_metadatacan author metadata for their own organization through that door, then the row belongs to that organization and stamping itnullis a write-side defect that this finding is about.The door gates on
manage_metadataand already resolves an execution context carryingtenantId(resolveExecCtx, which #8803 now uses for the audit read on the sibling route) — so the information needed to stamp correctly is present at the write site. But whether it should be used is a scoping decision about what that door means, not something to infer from availability.⛔ I have not measured which of the two it is, and I am not guessing. That measurement is the first thing whoever grades this should run.
Why this is worth a card rather than a line in the PR
The read-side fix looks complete when read on its own — its tests pass, its reverse verification is sound, and it does exactly what was ruled. The hole is only visible when the read filter is composed with the write stamping, which lives in a different package and a different card's lane (#8707). That is precisely the kind of gap that survives review of either half in isolation.
Related: #8747 (the read half, PR #8803), #8707 (the write half), and
rest-server.ts:6731-6742, whose comment records thatpackages/restdeliberately carries no org plumbing — which is the design decision this composition runs into.