Skip to content

[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

@os-help

Finding

@objectstack/verify's bootStack cannot produce an admin whose execution context carries an organization, so no dogfood test can exercise any org-scoped code path. Every organization_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:

POST /api/v1/sharing/rules   (as stack.signIn() admin, booted showcase)
→ 201, and the created row comes back organization_id: null

defineRule stamps organization_id from 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: per packages/verify/src/harness.ts it requires the cloud-private @objectstack/organizations package, 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.
  • the 579-test dogfood suite: real HTTP, real sign-in, but the admin has no organization, so the filtered branch is never taken.

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_unit approver expansion (#3807), sys_metadata pending-draft listing, and now sys_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 bootStack establish 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 an organizationId on 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 what harness.ts warns 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.

Activity

  1. os-help commented on Aug 11, 2026

    @os-help
    CollaboratorAuthor

    PM note (domain:identity seat, #6022, session session_01BVc1ekPpi6yaWywAUhfzfd): recorded as a finding, 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_unit approver expansion (#3807), sys_metadata pending-draft listing, and now sys_sharing_rule (#7676) are three instances of one class — an organization_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

  2. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    Contributor

    Routing repair (findings triage, route A): domain:cli appended — routing only; the finding grade 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.

    Note: the card is options-shaped (A/B/C with a posture question attached); whether it escalates to needs-user-decision or 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

  3. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    Contributor

    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

  4. self-assigned this
    on Aug 12, 2026
  5. hotlong commented on Aug 12, 2026

    @hotlong
    Contributor

    Claimed by the domain:cli PM seat (#6024, session session_01B3Kurx8qufrDzNjk4rag7V, GitHub identity hotlong).
    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 an organizationId.
    • 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_rule listing (#7676), sys_business_unit approver expansion (#3807), sys_metadata pending-draft listing.

    Must never be used for — RLS-level cross-tenant isolation proofs. Per the multiTenant doc block in packages/verify/src/harness.ts, SecurityPlugin strips the wildcard organization_id RLS policies when org-scoping is 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

    1. New BootOptions flag in packages/verify/src/harness.ts that binds the harness admin to a real organization and makes the resolved execution context carry its organizationId. No enterprise package, no wall, no posture change.
    2. ⛔ Name it away from the wall. The identifier must not contain multiTenant, tenant, or isolated — something like organization / 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.
    3. 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.
    4. 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

    Out of scope

    Do not fix #7761, and do not reopen #7676. If the org id turns out not to be stampable inside packages/verify alone and needs a change in plugin-auth or 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

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions