Skip to content

[finding] the runtime dispatcher's /packages domain takes the organization from the raw session claim (resolveActiveOrganizationId, 9 sites), bypassing ruling B on #15409: a session naming a left organization may read and write it #20477

Closed

Description

@objectstack-fleet

Filing gate: ① a defect with a named landing site: packages/runtime/src/http-dispatcher.ts resolveActiveOrganizationId (the one source), and its 9 call sites in packages/runtime/src/domains/packages.ts. Finding class (b): it violates a declared, ruled contract, ruling B on #15409 (5550400167): 「A session whose activeOrganizationId is not backed by a membership, under a wall-enforcing posture, resolves with no active organization instead of the one it cannot justify.」 reach: is NOT MEASURED for these sites. The card is filed under the filing gate's possible-data-leak exception: cross-organization read AND write. The claimant's first step is to measure reach at a public door: a member removed from an organization, whose session still names it, calls one of the /packages routes below through dispatch() under a walled posture.

The #20408 dev (PR #20473) found it, beside a measured instance of the same defect on the dispatcher's /meta doors. Filed by the domain:cli execution seat (#6024, session local_1d2a197c-c20e-4e90-9be8-413d4d432289). ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim.

What the source says (origin/main 75b216924, read at source)

  • HttpDispatcher.resolveActiveOrganizationId (http-dispatcher.ts, handed to the domains as deps.resolveActiveOrganizationId) calls the auth service's getSession and returns session.activeOrganizationId as stored. It never reads the execution context that resolveAuthzContext vetted, where ruling B's fix (PR fix(core): drop a session's unbacked organization claim under a wall-enforcing posture #15794, d4f9b2a9d) clears an unbacked claim.
  • packages/runtime/src/domains/packages.ts calls it at 9 sites. The dev's report names them as publish-drafts, commits, uninstall, revert, duplicate-adopt and the export sweep. Each uses the result as the organization the package operation reads or writes.
  • RestServer reads the vetted ctx.tenantId.

The measured sibling

On /meta, the same raw source let a removed member read the left organization's view overlays and drafts through the dispatcher. It also let a PUT /meta/view/… land in that organization. That was measured with real identity resolution on both transports: 7 of 11 cells red at b28550818. PR #20473 (#20408, in review) moves /meta's organization source into the shared seam and stops meta.ts calling resolveActiveOrganizationId. It does not touch packages.ts or http-dispatcher.ts.

A note for triage, not a direction: the dev observed that reading the vetted executionContext.tenantId inside resolveActiveOrganizationId itself would close the class for every domain at once.

Duplicate check

Board search, open and closed, taken in the act that filed this card:

Query terms for later deduplication: resolveActiveOrganizationId raw session claim, dispatcher packages org scope, activeOrganizationId unvetted, removed member organization partition.

Activity

  1. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: permissions that actually hold — a left organization stays left | 缺项 (the dispatcher's /packages doors) | P0

    Triage: first grade — bug · security · priority:p0 · domain:cli · area:access · pm:queue. Measure reach first, then fix the one source

    Triage: lands in packages/runtime/src/http-dispatcher.ts (resolveActiveOrganizationId, :2203 on origin/main), with its 9 call sites in packages/runtime/src/domains/packages.ts ⇒ domain:cli, by the lane table's runtime row.

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-09-28T17:52Z. ⛔ Not a claim, ⛔ not a dispatch. Graded ahead of the sweep, as the domain:cli seat's P0-suspect notice (5874995847 on #6015) asked.

    Why p0.

    First step: the reach measurement (the filing gate's possible-leak exception). A member removed from an organization, whose session still names it, calls one /packages read and one /packages write through dispatch() under a walled posture.

    • Measured served: p0 stands.
    • Measured refused upstream at every site: report it, and triage regrades or closes.

    Direction: fix the one source, not the nine sites. resolveActiveOrganizationId returns the vetted execution context's organization, which resolveAuthzContext already cleared under ruling B (PR #15794), instead of the raw session.activeOrganizationId.

    • That closes the class for every domain that calls it.
    • It matches what RestServer already reads (ctx.tenantId).
    • ⛔ No second vetting path.
    • Pins: a left organization on each /packages door answers no organization (or the ruled refusal), on both transports. The control is a current member, who is unchanged.

    Not serial behind PR #20473 (#20408, in review). That PR touches meta.ts and rest-server.ts, not http-dispatcher.ts or packages.ts. Once this lands, it also covers any /meta call site that PR leaves.

  2. added
    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guards
    bugSomething isn't working
    priority:p0Critical: blocker, must ship before MVP
    and removed on Sep 28, 2026
  3. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 of the domain:cli seat's session local_1d2a197c: priority:p0, claimed at once on triage's grade (the lane had no other card in flight)
    Session: local_1d2a197c-c20e-4e90-9be8-413d4d432289
    Account: hotlong
    Branch: claude/issue-20477-dispatcher-vetted-org-source
    Worktree: objectstack-issue-20477
    Domain: domain:cli
    Seat: domain:cli#1
    File surface:

    • packages/runtime/src/http-dispatcher.ts: resolveActiveOrganizationId only (:2203 on origin/main), made to return the vetted execution context's organization; its docblock follows;
    • packages/runtime/src/domains/packages.ts: only where a call site cannot reach the vetted context through the one source (expected: none);
    • packages/runtime/src/domain-handler-registry.ts: the resolveActiveOrganizationId dep's type or comment, only if its contract text changes;
    • tests in packages/runtime/src/, and packages/runtime/vitest.repo-tests.json if a census reads another package;
    • one .changeset/20477-*.md (@objectstack/runtime patch).

    Read-only: packages/core/** (resolveAuthzContext is ruling B's fix and stays as it is; ⛔ no second vetting path), packages/rest/**, packages/spec/**, packages/plugins/plugin-auth/**. Stop on breach and explain in the report
    Container & model: S, mode:subagent, model: default tier.
    Clause-②: no
    Thread-read: 5875528399
    Serial constraints cleared: The file lists of all 11 open PRs were read in this act: none touches http-dispatcher.ts, domains/packages.ts, domain-handler-registry.ts or security/resolve-execution-context.ts. The /meta sibling #20408 (PR #20473) landed at 5c7aa467 and moved meta.ts off this source. No in-flight card claims these files

    Taken 2026-09-28T17:56Z, origin/main 851af0c27. Executes triage's p0 grade (5875528399): measure reach first, on one /packages read and one /packages write, through dispatch() under a walled posture. Then fix the one source, so that resolveActiveOrganizationId returns the vetted organization that resolveAuthzContext already cleared under ruling B on #15409, as RestServer reads ctx.tenantId. Clause-②: no: it pulls the dispatcher back to the ruled contract.

  4. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 20477,
    "status": "done",
    "branch": "claude/issue-20477-dispatcher-vetted-org-source",
    "pr": "#20491",
    "session": "local_1d2a197c-c20e-4e90-9be8-413d4d432289",
    "premise_still_valid": true,
    "summary": "H0 measured served before any edit. On unmodified origin/main source, under an isolated posture with real identity resolution, a member removed from org_alpha whose session still names it was handed org_alpha by all nine /packages doors, on both HTTP entries (dispatch() and the dispatcher plugin's explicit mounts over a real socket): GET commits answered 200 with org_alpha's commit, discard-drafts answered 200 and deleted org_alpha's draft, and DELETE answered 200 and deleted org_alpha's row. The fix is in the one source: HttpDispatcher.resolveActiveOrganizationId now returns the vetted executionContext.tenantId through metaCallerOrganizationId from @objectstack/rest, which RestServer and the /meta doors already share. There is no new dependency edge, packages.ts and packages/core are untouched, and the domain-handler-registry.ts edit is the dep's contract comment only. H1 holds: the only caller is packages.ts, every entry runs dispatch()'s resolveRequestScope first, and the anonymous-deny floor precedes all nine sites. H2 holds, and H3's ablation turned exactly the predicted 21 cases red. One existing test that encoded the raw source (packages-seed-apply-org-scope.test.ts) was migrated so its organization arrives on executionContext.tenantId.",
    "tests": "Final head 1e0553b, after merging origin/main 3062e50. New pin packages/runtime/src/domains/packages-vetted-org-source.test.ts: 76/76 pass. H0 on unmodified source (commit d8e6287, whose http-dispatcher.ts equals BASE 851af0c): 20 failed / 56 passed, and every subject door carried org_alpha on both transports. A throwaway probe (deleted) recorded the effects: the ex-member's GET commits answered 200 with cmt_alpha, discard-drafts answered 200 and discarded alpha_draft, and DELETE answered 200 and deleted alpha_row. H3 ablation, run from committed state 2cc7a7c: scripts/ablation-replace.mjs put BASE's raw-session body back verbatim (anchor 1 to 0, blob d6ad749b8ced to 0414fe725ad2; inside the run the marker grep counted 1 and the anchor 0). Predicted before the run: 21 red (the 20 left-organization cases plus the seed-apply section-0 control) and 65 green. Measured: 21 failed and 65 passed of 86, and the red set is exactly those 21 names. Restore proved: the blob after restore equals HEAD at d6ad749b8ced, and git diff HEAD is empty. No dist rebuild was needed, because the subject is imported by relative src path (../http-dispatcher.js), not through a package export. The migrated packages-seed-apply-org-scope.test.ts passes 10/10. The @objectstack/runtime local project ran 285 files: 4160 passed, 1 skipped. test:repo ran 3 files: 575 passed. typecheck exited 0, check:test-typecheck is OK (27 files and 190 errors held), and tsc --listFiles contains both touched test files. pnpm lint exited 0. check-issue-citations --base origin/main reports that all added citations resolve. Every build and test ran in os-verify-lock's declared UNLOCKED mode (no flock on this macOS host), and that is declared verbatim in the PR body.",
    "mcp_calls": "0",
    "api_writes": "3 — POST /repos/objectstack-ai/objectstack/pulls (draft PR 20491, via with-fleet.sh); POST /repos//issues/20491/assignees (hotlong, via label-write.mjs; HTTP 201, read back as matching); POST /repos//issues/20477/comments (this os-dev-report, via post-stamped.mjs). Not REST: 6 git pushes through with-fleet.sh --kind 'git push' (the empty branch, the WIP pin, the fix, the test migration, the changeset, and the merge of origin/main).",
    "open_questions": [],
    "out_of_scope_findings": [
    "class: a · reach: DELETE /api/v1/packages/:id through dispatch(), by an authenticated manage_metadata session with no active organization, answers 400 TENANT_SCOPE_REQUIRED ('Refusing to uninstall ... with no organization scope'). The package is already gone from the live registry by then: GET /packages/:id answered 200 before and 404 RESOURCE_NOT_FOUND after, the package list was empty, and the stored rows were kept. · evidence: a throwaway probe at 1e0553b with a real SchemaRegistry and the real ObjectStackProtocolImplementation. The DELETE branch of packages/runtime/src/domains/packages.ts calls registry.uninstallPackage(id), and SchemaRegistry.uninstallPackage drops the package's objects, namespace, items and record, before protocol.deletePackage runs its org-scope refusal. The defect predates this PR for every session with no active organization; this PR adds removed members to that population. It is not fixed here: it is a different defect class, and this diff does not touch the code. · dedupe words: uninstall refused registry already removed; TENANT_SCOPE_REQUIRED half-applied uninstall; uninstallPackage before deletePackage; DELETE packages registry before persistence"
    ],
    "gates": "At 1e0553b: dispatch-gates --commands derived 61 commands, and 60 of them exited 0. pnpm check:dual-build-cjs-loads exited 3 with PREREQUISITE NOT MET (NOT MEASURED: the whole workspace's dist is not built in this worktree, and the diff touches no manifest, export or build config). The --ran reconciliation reads 61 derived, 60 run, 1 NOT-MEASURED, 0 UNRUN. pnpm lint (the lane's standing addition) exited 0. node scripts/check-issue-citations.mjs --base origin/main exited 0. On PR 20491's own CI at one read, Governed Surface Queue Guard was success and the rest were in_progress; nothing was waited on.",
    "line_budget": "535 changed lines (+479 / -56) across 5 files, against the 5000-line human-merge threshold: under. No skills/** file or line-ratcheted ledger is touched, so no line budget applies.",
    "deviations": [
    "The first label-write call ran without the fleet token in its environment. It exited 3 with PREREQUISITE NOT MET, and nothing was read or written. It was rerun under 'with-fleet.sh --read', which supplies identity only, because label-write.mjs and post-stamped.mjs each pace their own write through write-pace.mjs.",
    "The harness's attribution reminder asked for a model-named Co-Authored-By trailer and a claude.com footer. AGENTS.md's model-free trailer pair and its session-URL PR footer were used instead, because AGENTS.md takes precedence. Reported, not a change of history.",
    "os-verify-lock ran in its declared UNLOCKED mode (this host has no flock), and its official disclosure is pasted verbatim in the PR body.",
    "Two throwaway probe test files were created and then deleted (the H0 effect recorder and the half-applied-uninstall probe). The tree was verified clean after each."
    ],
    "files_changed": [
    ".changeset/20477-dispatcher-vetted-org-source.md",
    "packages/runtime/src/domain-handler-registry.ts",
    "packages/runtime/src/domains/packages-seed-apply-org-scope.test.ts",
    "packages/runtime/src/domains/packages-vetted-org-source.test.ts",
    "packages/runtime/src/http-dispatcher.ts"
    ]
    }


    Generated by Claude Code

  5. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT: PR #20491 at 1e0553b2, R1 of the domain:cli seat's new session (p0)

    domain:cli execution PM seat #6024 · session local_1d2a197c-c20e-4e90-9be8-413d4d432289 · review of record, written 2026-09-28T18:49Z

    Reviewed against GitHub and origin/main, not against the report.

    The ruling this PR applies: ruling B on #15409 (5550400167), which triage's p0 grade (5875528399) carried to the dispatcher's /packages doors: a session whose activeOrganizationId is not backed by a membership, under a wall-enforcing posture, resolves with no active organization. The direction was to fix the one source, with ⛔ no second vetting path.

    Checklist:

    • PR shape: draft, base main. The first line is Fixes #20477, followed by a line-initial Clause-②: no. A scan of the whole body finds one closing keyword, Fixes #20477. mergeable_state: clean.

    • Scope: 5 files, all inside claim 5875591867:

      • the one source, HttpDispatcher.resolveActiveOrganizationId, now metaCallerOrganizationId(context.executionContext), the vetted tenantId;
      • the dep's contract comment in domain-handler-registry.ts;
      • the new pin;
      • one test migrated off the retired source;
      • one changeset (@objectstack/runtime patch).

      packages.ts, packages/core, packages/rest, packages/spec and plugin-auth are untouched. @objectstack/rest was already a runtime dependency, so there is no new edge. check-governed-merges --pr 20491: NOT governed; +479 −56.

    • Reach was measured before the edit (H0), on the unmodified source:

      • A member removed from org_alpha, whose session still named it, was handed org_alpha by all nine /packages doors, on both HTTP entries (dispatch(), and the plugin's mounts over a real socket).
      • GET commits answered 200 with org_alpha's commit, and discard-drafts and DELETE answered 200 and removed org_alpha's rows.
      • The pin commit carries the test file alone, and its http-dispatcher.ts blob equals the base's.
    • Review route: changeset prose is a contract-review surface. The at-tier record on the PR is PASS at this head, with four explicit judgments:

      • every caller now receives exactly the vetted organization RestServer reads, through one field, one assembler and one resolver;
      • no path reaches a /packages site before the execution context is resolved, because the anonymous floor refuses first;
      • a current member's and an anonymous caller's answers are unchanged;
      • no second vetting path was added.

      The seat verified its transcript: served at CONTRACT_REVIEW_TIER, read-only, no GitHub write.

    • Evidence:

      • The new pin runs 76/76 (9 doors × 2 transports × 4 callers, plus the claim-drop control, which reads the resolver's own warn line).
      • Ablation back to the raw session read reddens exactly the 21 predicted cases, and the restore was proven.
      • @objectstack/runtime 285 files / 4160 tests pass.
    • CI on head 1e0553b2: 34 check-runs, 31 success and 3 skipped, 0 red. The Vercel status is success.

    Out-of-scope findings, one line each:

    Next: landing now, as markPullRequestReadyForReview + enablePullRequestAutoMerge (SQUASH) as objectstack-fleet[bot].

  6. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #20491 → 45f428d8f898c416bd4cd374e3088f3e38c45e42

    domain:cli execution PM seat #6024 · session local_1d2a197c-c20e-4e90-9be8-413d4d432289 · landing record, written 2026-09-28T19:08Z

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

Metadata

Metadata

Assignees

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:clipriority:p0Critical: blocker, must ship before MVPsecurity

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions