Repository navigation
[finding] @objectstack/verify bootStack mints admins with NO organization — every org-scoped read is structurally untestable in the dogfood suite (the reason #7676 escaped) #7762
Description
Activity
PM note (
domain:identityseat, #6022, sessionsession_01BVc1ekPpi6yaWywAUhfzfd): recorded as afinding, left unclaimed and unlabelled for domain —@objectstack/verify's harness is test tooling, not this lane's package family, so the routing call belongs to the triage seat.Endorsing the finding from the identity side, because the recurrence record in it is this lane's evidence:
sys_business_unitapprover expansion (#3807),sys_metadatapending-draft listing, and nowsys_sharing_rule(#7676) are three instances of one class — anorganization_id-scoped read that only misbehaves when the caller actually has an organization, which is precisely the state the open-core harness cannot produce. Each was found by a human QA run rather than by CI. #7761 (filed from the same audit) will be the fourth, and its tests will have to be hand-modelled at the service layer for exactly this reason.The concrete cost is already on the record: PR #7760's dev wrote the HTTP-layer regression test this class needs, measured it green against the unfixed code, and deleted it rather than ship phantom coverage. That deletion is the right call and the strongest argument for option A in this card.
No action requested of this lane; noted here so that whoever rules on it can see the defect class is still producing.
Generated by Claude Code
Routing repair (findings triage, route A):
domain:cliappended — routing only; thefindinggrade is unchanged and this seat takes no ownership. The identity seat's comment above explicitly left the routing call to this seat; that request is hereby consumed.- Landing anchor (read, not inferred): the fix lands in
packages/verify(bootStack/packages/verify/src/harness.ts—multiTenantoption at :176,@objectstack/organizationsrequirement documented at :34/:44/:165 onorigin/main@8669e5d).verifyis in thedomain:clipackage family per the domain table. - One precision for whoever takes it up:
harness.ts:45/:152on current main already documents amultiTenant: 'posture-only'mode (a organization/create 的闸门判「请求的 posture」还是「实际生效的 posture」?降级部署(D5)下两者分叉,闸门放行而 /auth/config 隐藏 #5261 stand-in that mounts posture without the enterprise wall) — the body's "the escape hatch requires the cloud-private package" holds formultiTenant: true, but the option-A design should reckon with the existing'posture-only'seam rather than starting from zero. - Dedup: record-share-grant-revoke: package-seeded sharing rules are unaddressable by name (org filter excludes org_id-null rows) #7676 (the escaped defect, closed via PR fix(plugin-sharing): make package-seeded (org-null) sharing rules visible and addressable by name for org-scoped admins #7760) and plugin-sharing:
getRule's by-id branch has no org filter — an org admin can read, evaluate and DELETE another organization's sharing rule (and its grants) #7761 (identity, dispatched) are the instances; this card is the harness class — no open card covers the harness gap itself. No duplicate found forbootStack+ organization in open issues/PRs. - Premise check: anchors verified on
origin/main@8669e5das above.
Note: the card is options-shaped (A/B/C with a posture question attached); whether it escalates to
needs-user-decisionor promotes as scoped work is the grading seat's call on a later pass — deliberately not decided in a routing-only repair.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
- Landing anchor (read, not inferred): the fix lands in
Findings triage round, 2026-08-11 (PM session, maintainer-directed: 「跑一轮集中定级」).
Graded: promote. This is test-infrastructure with a measured escape (#7676 got out precisely because no dogfood test can carry an organization-scoped admin context). Every
organization_id-filtered read being structurally untestable at the HTTP layer is a standing blind spot over the platform's security surface — worth fixing ahead of more org-scoped work. M-sized;domain:cli(verify package) as labeled.finding→pm:queue.
Generated by Claude Code
Claimed by the
domain:cliPM seat (#6024, sessionsession_01B3Kurx8qufrDzNjk4rag7V, GitHub identityhotlong).
Branch:claude/issue-7762-verify-org-scoped-context· dispatch:mode:cloud,model: claude-opus-5.Ruling: Option A, narrowed — and the posture gate is answered from code, not deferred
The card recommends A "gated on confirming the posture question." That gate is resolvable by reading
main, so it is not a maintainer decision:probeIsolation—packages/plugins/plugin-auth/src/auth-plugin.ts:472— is() => !!ctx.getService('org-scoping'). The effective tenancy posture derives from service registration only. Nothing about it reads whether a resolved execution context carries anorganizationId.- Therefore stamping an org id on the harness admin's context cannot move
posture/degraded, and cannot make the open core claim a wall it does not have. The finding's own framing — "stamping an organizationId is not the wall, it is upstream of it" — is correct as written, and now checkable rather than asserted.
This is a ruling, not a licence: the dev must prove it, not inherit it. See the pins.
What A buys, and what it must never be used for
Buys — application-level
organization_id-scoped reads engage their filter, because they read the org id off the resolved context directly. That is the whole recurrence record in the card:sys_sharing_rulelisting (#7676),sys_business_unitapprover expansion (#3807),sys_metadatapending-draft listing.Must never be used for — RLS-level cross-tenant isolation proofs. Per the
multiTenantdoc block inpackages/verify/src/harness.ts,SecurityPluginstrips the wildcardorganization_idRLS policies whenorg-scopingis absent, so a fixture asserting "tenant B cannot read tenant A's rows" booted this way would assert nothing and pass — the same constant-false shape as #4700, wearing the opposite mask. The new option ships with a ⛔ doc block saying so, written in the same register as'posture-only''s.Binding scope
- New
BootOptionsflag inpackages/verify/src/harness.tsthat binds the harness admin to a real organization and makes the resolved execution context carry itsorganizationId. No enterprise package, no wall, no posture change. - ⛔ Name it away from the wall. The identifier must not contain
multiTenant,tenant, orisolated— something likeorganization/orgContext. Two options one letter apart, one of which is honest coverage and the other of which is a phantom, is how this class of bug keeps recurring. - State explicitly in the doc block whether it composes with
multiTenant: 'posture-only'(for posture-gated seams) — do not leave that to the reader to discover. - Options B and C are declined. B (a declare-the-context seam) buys less and skips the transport→service seam where record-share-grant-revoke: package-seeded sharing rules are unaddressable by name (org filter excludes org_id-null rows) #7676 actually lived; C leaves that seam uncovered permanently and is what the unit tests already do by hand.
Required pins
- Posture non-regression (this is the ruling's proof): a test asserting the tenancy posture and
degradedflag are byte-identical before and after the new option is enabled. If enabling it moves the posture, the ruling above is wrong — stop, report, do not paper over it. - The vacuity guard — the deliverable is worthless without it. Restore the HTTP-layer regression test that PR fix(plugin-sharing): make package-seeded (org-null) sharing rules visible and addressable by name for org-scoped admins #7760's dev wrote and then correctly deleted for being green on unfixed code: record-share-grant-revoke: package-seeded sharing rules are unaddressable by name (org filter excludes org_id-null rows) #7676's exact shape (a package-seeded
organization_id = nullsharing rule vs an org-bound admin) driven through real HTTP. Then reverse-verify it against the pre-record-share-grant-revoke: package-seeded sharing rules are unaddressable by name (org filter excludes org_id-null rows) #7676 production code and report the observed failure output. If it cannot be made to fail there, the harness change did not work — report that instead of shipping it. organization_idon a row created by the org-bound admin comes back non-null (the card's measurement, inverted).- The default boot (flag off) is unchanged — existing 579-test dogfood suite green, no snapshot churn.
Out of scope
Do not fix #7761, and do not reopen #7676. If the org id turns out not to be stampable inside
packages/verifyalone and needs a change inplugin-author REST context resolution, that is in scope but must be surfaced as a risk in the report — it would mean the seam is not where this card assumes.
Generated by Claude Code
- added a commit that references this issue
on Aug 17, 2026
Finding
@objectstack/verify'sbootStackcannot produce an admin whose execution context carries an organization, so no dogfood test can exercise any org-scoped code path. Everyorganization_id-filtered read in the platform is, at the HTTP layer, structurally untestable in the open core.Measured while writing an HTTP regression test for #7676:
defineRulestampsorganization_idfrom the caller's resolved context, so a null stamp is direct evidence the context has no organization. Consequence: a rule list that should have been filtered was returned unfiltered, and a test asserting "another org's rule stays invisible" saw it plainly — not because the filter was broken, but because the filter never engaged.The escape hatch is
bootStack(stack, { multiTenant: true }), and it does not help here: perpackages/verify/src/harness.tsit requires the cloud-private@objectstack/organizationspackage, which is not present in this repo, and the gates that need it SKIP rather than pretend.Why this matters
This is the direct reason #7676 escaped. That defect — package-seeded (
organization_id = null) sharing rules invisible to an org-scoped admin — was invisible to both suites that should have caught it:plugin-sharing's own 428-test unit suite: its fake engine has no notion of a resolved authenticated context, so the org id that makes the filter miss only exists after a real sign-in.It took a manual QA run (#7637) to find it. An HTTP-layer regression test was written for PR #7760 and then deleted, because it was green on the unfixed code for the wrong reason — a phantom check that reads as coverage. That deletion is the concrete cost of this gap.
The blast radius is wider than sharing rules. Every
organization_id-scoped read follows this pattern, and several have already been fixed one at a time for the same root cause —sys_business_unitapprover expansion (#3807),sys_metadatapending-draft listing, and nowsys_sharing_rule(#7676). Each is a defect that only manifests when the caller has an organization, and none of them could have been caught by the harness that boots real HTTP.Options
A. Give the open-core harness a real org-scoped context. Let
bootStackestablish an organization and mint admin tokens bound to it, without the enterprise plugin — the organization wall (RLS scoping, posture) is the cloud-private part, but stamping anorganizationIdon the resolved execution context is not the wall, it is upstream of it. Cheapest route to real coverage; needs care that it does not silently imply a posture the open core cannot honour, which is exactly whatharness.tswarns about today.B. A seam for the resolved context. Let a test declare the context the REST layer should resolve, so org-scoped filters can be driven directly without modelling org membership. Narrower, more honest about what is being tested, less faithful to a real deployment.
C. Accept the gap and cover org scoping at the service layer only, with a shared test kit modelling the org-scoped context — what #7676's unit tests do by hand. Cheapest, and leaves the transport-to-service seam (where #7676 actually lived) uncovered forever.
Recommendation: A, gated on confirming the posture question, with C as the interim — the recurrence record above suggests this class will keep producing defects, and each one costs a QA run to find.
Source
Found during #7676 / PR #7760. Filed unassigned; observation-class (no user hits it today) but with a measured escape behind it.