Skip to content

[finding] The delegated-admin gate resolves an EMPTY subtree on a stock objectstack dev boot — seeded business units are organization-less while every session carries an active organization, so every in-scope delegated write is refused #21057

Description

@objectstack-fleet

Blocked-by: #15193

Filing gate: ① a reproducible product defect with a named landing site — packages/plugins/plugin-security/src/delegated-admin-gate.ts. reach: the public REST data API, reproduced on four fresh objectstack dev --seed-admin showcase boots (runner + independent verifier). Reader: triage first touch, then the lane owning plugin-security. Dedupe: semantic search "delegated admin subtree business unit organization_id null outside the delegated subtree" (open + closed) → 8 hits, none this defect; nearest are the same gate's cross-organization repairs #19775 / #19819 / #19860 (closed) and the ADR-0131 epic #15194 (open, no NULL organization_id).

QA-source: #21056 · access-security.crud-permission-matrix · acceptance[0]

What happens

ADR-0090 D12 delegated administration does not work on a stock dev boot. A delegate holding showcase_field_ops_delegate (adminScope Field Operations + subtree, manageAssignments, assignablePermissionSets: [showcase_contributor, showcase_manager]) is refused every in-subtree assignment. The gate fails closed, so this is over-refusal, not exposure.

Reproduction

  1. Fresh boot: OS_PORT=<p> pnpm -C examples/app-showcase exec objectstack dev --seed-admin -p <p> -d file:<fresh>.db; admin admin@objectos.ai / admin123.
  2. Admin invites + signs up a delegate D and a target T (POST /api/v1/auth/organization/invite-member, then POST /api/v1/auth/sign-up/email).
  3. Admin: POST /api/v1/data/sys_user_permission_set {"user_id":D,"permission_set_id":<showcase_field_ops_delegate>} → 201; POST /api/v1/data/sys_business_unit_member {"business_unit_id":"bu_field_ops","user_id":D} → 201.
  4. D: POST /api/v1/data/sys_user_position {"user_id":T,"position":"contributor","business_unit_id":"bu_west_coast"} (bu_west_coast is a child of bu_field_ops).
    • Expected: 2xx.
    • Actual: 403 PERMISSION_DENIED "delegated 'insert' on sys_user_position rejected — business unit 'bu_west_coast' is outside the delegated subtree (scope from 'showcase_field_ops_delegate')".
    • Control: admin posting the identical payload → 201.
  5. Discriminating experiment: admin PATCHes organization_id onto the seeded bu_field_ops / bu_west_coast / bu_east_coast → D's same call now → 201. An org-stamped "Field Operations" tree created through REST also works.

Mechanism

  • The seeded sys_business_unit rows have organization_id: null by design — the seed loader never stamps the fallback organization on sys_* seeds (packages/metadata-protocol/src/seed-loader.ts).
  • delegated-admin-gate.ts callerOrganizationId() returns context.organizationId ?? context.tenantId without consulting the tenancy posture. Its docblock says the value is "Undefined for a single-posture caller", but the dev boot's default-organization bootstrap gives every session an activeOrganizationId, so it is always defined (boot banner: Tenancy: single).
  • resolveSubtree then reads the scope root through resolveOwnOrganizationRow(...).own, which only matches rows stamped with that organization (per-organization-catalog.ts); resolveSubtreeById also drops children whose organization_id !== organizationId. The org-less seeded tree is filtered out, the subtree is empty, and every write fails closed.
  • The rest of the plugin reads the opposite way: per-organization-catalog.ts calls org-less rows "the CORRECT shape" under the single posture, and the enforcement loader dbLoaderFor in security-plugin.ts uses own ?? organizationLessResidue, with a comment that resolveOwnOrganizationRow is written for seeders. DelegatedAdminGate is constructed with { ql, resolveSets, logger } only and receives no posture.

Why the pin is green

packages/qa/dogfood/test/showcase-permission-zoo.dogfood.test.ts (13/13) boots the shared stack without orgContext, so packages/verify/src/harness.ts sets autoDefaultOrganization: false; the context carries no organization and the gate takes its by-name branch. The pin never models the real dev boot.

Landing site and acceptance

Related symptom, same class, not verified here: every stock boot warns [sharing-rule] share_new_inquiries_with_field_ops expands to NO recipients … not organization-stamped.

Triage note (not a verdict): a delegate's PATCH/DELETE on an assignment ANOTHER user created is also refused by the platform ownership floor (record_access_denied) even after the units are stamped; the floor does not yield to adminScope.manageAssignments, so a fixed gate may still not let a delegate re-staff others' assignments.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — bug · priority:p2 · domain:services · area:access · pm:queue. Direction: the seed stamps the organization on seeded business units. ⛔ The gate does not widen

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-01T04:57Z. ⛔ Not a claim, ⛔ not a dispatch.

    Why p2. The gate fails closed, so this is over-refusal on a stock dev boot, not exposure. But ADR-0090 D12's delegated administration cannot be exercised at all in the showcase, which is the platform's reference flow.

    Direction:

    • Seeded business units carry the organization_id the seeding organization has, at the seed path that creates them.
    • ⛔ No change to the gate's organization match. Treating a NULL organization as "any" would widen a security boundary.
    • Census: if stored dev databases already hold organization-less units, the claim names the migration or the boot fixer and keeps it scoped to seeded rows.
    • Pin: on a fresh --seed-admin boot, a delegate's in-subtree assignment is accepted, and an out-of-subtree one is still refused (the control).

    Generated by Claude Code

  2. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: re-grade — priority:p2 → priority:p1. A red row in the P0 release sweep; the direction is unchanged

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-01T06:05Z. ⛔ Not a claim, ⛔ not a dispatch.

    Correction to this seat's first grade (5925045171). This card's QA source is access-security.crud-permission-matrix acceptance[0], and that is a P0 checklist item, red in #21056. Under the release-priority rule, a red row in the P0 release sweep is p1. The first grade weighed only "fails closed" and missed the rule.

    The direction (the seed stamps the organization; ⛔ the gate does not widen), the routing and the state are unchanged.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · 2026-10-01T07:42Z
    Session: session_01DiCSbmJrkzNhuEAier4VoJ
    Account: os-bill (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-21057-seeded-bu-organization
    Worktree: objectstack-issue-21057
    Domain: domain:services
    Seat: domain:services#2 (seat post #21118)
    File surface: the expected landing is the seed path that writes the seeded sys_business_unit rows. Triage's direction 5925045171 is that the seed stamps the organization. Today packages/metadata-protocol/src/seed-loader.ts keeps every sys_/cloud_/ai_ seed off its single-organization fallback (the per-tenant tagging step), so that file is the probable producer. If the dev measures the real producer elsewhere (the default-organization bootstrap, or the showcase seed under examples/app-showcase/src/data/seed/), the fix lands at the producer and the report names it. Also in scope: a pin under packages/qa/dogfood/test/ (a new file, run with orgContext: true) and a changeset. ⛔ No change to the organization match in packages/plugins/plugin-security/src/delegated-admin-gate.ts (triage direction). ⛔ No edit to security-plugin.ts (#21063 holds it). Stop on breach; explain in the report.
    Cross-lane: packages/metadata-protocol is domain:engine's family. Triage routed the whole card here, and this claim declares that surface and dispatches it vertically.
    Container & model: M, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate; default tier, because the seed-scope choice is a design judgment)
    Clause-②: no
    Thread-read: 5925733644
    Serial constraints cleared: I diffed the pushed branches of every open pm:dispatched card (29 cards, 26 branches) against origin/main. None touches seed-loader.ts, delegated-admin-gate.ts, per-organization-catalog.ts or the showcase seed. The metadata-protocol files that are in flight are different files: #21086 (PR #21115: protocol.ts, metadata-redaction.ts), #21002 (protocol.ts) and #20870 (protocol.ts, runtime-authoring-gate.ts, sys-metadata-repository.ts). #21052 (PR #21104) holds examples/app-showcase/src/security/permission-sets.ts and two showcase dogfood files; the pin here is a new file, so the two are disjoint. #21063 holds security-plugin.ts, which this claim does not touch.

    Clause-② no: no published contract's accept set widens and no public surface grows. The seeded rows gain the organization key that a normal write carries. Landing: delegated administration sits on the access boundary, so the PR stays draft until the seat's review decides the landing path.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Maintainer ruling, recorded · domain:services seat 2 (#21118) · session_01DiCSbmJrkzNhuEAier4VoJ · 2026-10-01T08:03Z · ⛔ Not a claim (the claim is 5927038194).

    • The question: the card is domain:services, but triage's direction (5925045171) puts the probable landing in packages/metadata-protocol/src/seed-loader.ts, which is domain:engine's family under the "domain = the landing package's domain" rule. The seat offered two paths: continue under this claim, or release the card to the engine lane through pm:retriage.
    • The ruling, verbatim: 「已经在开发的就继续开发」. Who: the maintainer. Where: this seat's Claude Code session session_01DiCSbmJrkzNhuEAier4VoJ, 2026-10-01.
    • What follows: the card stays claimed here and the dev continues. The cross-lane surface is declared in the claim. The engine seat gets a cross-seat note on its post ([PM seat] domain:engine · seat 2 — 🟢 os-tesla · session_01Bw3y2DWhT9RPnrmDsNqEVG #20966), and the seat's review confirms the measured landing.

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 21057,
    "status": "needs_decision",
    "branch": "claude/issue-21057-seeded-bu-organization",
    "pr": null,
    "session": "session_01DiCSbmJrkzNhuEAier4VoJ — mode:subagent, so the dispatching seat's session id",
    "premise_still_valid": true,
    "summary": "The defect is real and was reproduced on a stock objectstack dev --seed-admin showcase boot at origin/main 53ed3d1: the delegate's in-subtree assignment (bu_west_coast) answers 403 PERMISSION_DENIED "outside the delegated subtree", and the out-of-subtree control (bu_hq_finance) also answers 403. Once the admin PATCHes organization_id onto the three Field-Ops units, the in-subtree call answers 201 and the out-of-subtree one stays 403, so stamping is sufficient and the gate needs no change. I stopped before writing code because of Zone 2 #3. The direction this card was routed with (the seed path stamps seeded business units) is exactly ADR-0131 C1's business-unit leg. ADR-0131 D3 (Accepted 2026-09-04) decides it for business units specifically: "the seed loader's exemption of sys_ / cloud_ / ai_ seeds from stamping ('intentionally global') is withdrawn — there are no platform-global seeds left; a seeded sys_business_unit is the organization's business unit". D14 then stages it: "Everything else is the v18 line … ⛔ No 17.x card narrows or removes an arm, adds a name column beside an id column, or ships a half of this record." The ADR status says "Execution is paused under #15193", and #15193 is OPEN ("⛔ No ADR-0131 card is dispatched, claimed, or assigned — by any seat"; maintainer 「要 v18 才开发」). The tree is on 17.5.0. The sibling consumer with the same root cause (#14547, sharing) was deliberately left standing in 17.x, and its fix was staged to v18 C1 (plugin-sharing/src/business-unit-graph.ts orgScope docblock; plugin-sharing CHANGELOG ab47816). The fork is in open_questions. No PR. The branch is pushed and empty at 53ed3d1.",
    "tests": "No code was written, so no test or gate is owed (dispatch-gates --repo objectstack-ai/objectstack --commands, EXIT=2: "this branch changes nothing against 'origin/main' (merge base 53ed3d1) — nothing to derive"). Measurements, all at 53ed3d1, through a turbo build of the dogfood + cli + showcase closure under os-verify-lock (VERDICT command-exit 0, 63/63 tasks). [boot 1, fresh file DB, pnpm -C examples/app-showcase exec objectstack dev --seed-admin] The banner reads Tenancy: single and Seeds: com.example.showcase 132 rows. The DB read shows sys_business_unit bu_acme/bu_field_ops/bu_west_coast/bu_east_coast/bu_hq_finance with created_at 2026-10-01T07:58:06.864Z..06.869Z and organization_id null (5/5). The admin sys_user was created at 07:58:08.001Z and the Default Organization org_mup8s7vmxmqnsu36 at 07:58:08.099Z, about 1.2 s AFTER the seed. The business seed showcase_inquiry is null on 3/3 too, so on a first boot no object can take the fallback and the sys_ regex is not the cause. [repro, HTTP] admin invite+sign-up D and T 200/200; grant showcase_field_ops_delegate 201 (that set row is itself org-less: [ps_mup8s87i8gw77ky0, null]); sys_business_unit_member bu_field_ops 201 (stamped org_mup8s7vmxmqnsu36); D POST sys_user_position bu_west_coast -> 403 {"code":"PERMISSION_DENIED","error":"... business unit 'bu_west_coast' is outside the delegated subtree (scope from 'showcase_field_ops_delegate')."}; bu_hq_finance -> 403 same shape. [boot 2, same DB restarted] The replay re-stamped the business seed (showcase_inquiry nulls went 3 -> 0) and left sys_business_unit null (5/5, updated_at unchanged). This is the /^(sys_|cloud_|ai_)/ cut at seed-loader.ts:1096. The in-subtree call still answers 403. [discriminator, boot 2] admin PATCH organization_id onto bu_field_ops/bu_west_coast/bu_east_coast 200x3, then D in-subtree -> 201 and D out-of-subtree (bu_hq_finance) -> 403. Both servers were started by me and stopped by recorded PID (listeners 23971 and 26617 verified dead). No ablation was run because no pin was written.",
    "mcp_calls": "0 — no MCP GitHub tool used (reads went through gh api GET: issues/21057, issues/21057/comments, issues/15193)",
    "api_writes": "1 — the os-dev-report comment on #21057 via scripts/pm/post-stamped.mjs (fleet-write relay -> POST /repos//issues/21057/comments). git push of the empty branch is not a REST write. No pr_create and no label-write were spent, because no PR exists.",
    "gates": "none owed — empty diff (dispatch-gates EXIT=2, nothing to derive at merge base 53ed3d1)",
    "line_budget": "n/a — no diff",
    "files_changed": [],
    "deviations": [
    "Base is 53ed3d1, not the 9c8b65a the dispatch named: origin/main had moved when the worktree was created. git diff --stat 9c8b65a 53ed3d1 over every measured surface (seed-loader.ts, seed-tenancy-backfill.ts, runtime app-plugin.ts, plugin-auth ensure-default-organization.ts, delegated-admin-gate.ts, per-organization-catalog.ts, objectql/src/tenancy, showcase seed, ADR-0131) is empty.",
    "No PR and no pin: I stopped under Zone 2 #3 ("If it does, stop and report the fork with the governing text"). The pr_create and label-write budget is unspent.",
    "Zone 2 #1 is partly falsified: on a FIRST boot the producer is not the seed loader's sys_ regex. No organization exists at seed time (1.2 s gap), so every seed is org-less, and the sys_organization-insert handoff (runtime app-plugin.ts registerSeedTenancyHandoff -> backfillSeedTenancy) repairs only autonumber-split NON-platform objects. On every later boot the regex at seed-loader.ts:1096 is the producer.",
    "Process finding for the PM: per #15193's rule, a card whose fix is an ADR-0131 C1 derivative should carry pm:blocked + Blocked-by: #15193 rather than pm:dispatched. This dispatch itself sits on that line."
    ],
    "open_questions": [
    {
    "question": "The fork: this card was routed with "seeded business units carry the organization at the seed path". That is ADR-0131 C1's business-unit leg, which D14 stages to v18 and #15193 (open) gates. Which line does #21057 land on? Governing text: ADR-0131 D3 ("the seed loader's exemption of sys_ / cloud_ / ai_ seeds from stamping … is withdrawn … a seeded sys_business_unit is the organization's business unit (D9 derives the owner; #14547 is this defect seen from the sharing side)"); D14 ("⛔ No 17.x card … ships a half of this record"); D10 ("The migration is a manual, operator-run ceremony — never an automatic boot step"); #15193 ("⛔ No ADR-0131 card is dispatched, claimed, or assigned"); objectql/src/tenancy/system-write-organization.ts ("⛔ Do NOT restore the coupling by re-cutting the seed paths per object, and ⛔ do not re-stamp anything to match"); platform-object-tenancy.ts (sys_business_unit is unlisted, so it reads unclassified, "⛔ Do not promote an entry to tenant-scoped to shorten the list").",
    "options": [
    "A — Hold for v18 C1, the #14547 precedent. Re-label #21057 pm:blocked with Blocked-by: #15193, so it is cut as a C1 symptom. The P0 row access-security.crud-permission-matrix acceptance[0] is carried as known-v18 (the checklist owner's call). Optional A+: keep the 17.x symptom loud and correctly attributed. The gate refusal (or a once-per-process warn) would say that the scope anchor resolved to no business unit of the caller's organization, only an org-less one, instead of "outside the delegated subtree". This is the twin of SharingRuleService.warnOnEmptyUnitExpansion and does not touch the organization match. Cost: delegated admin stays unusable on stock dev boots until v18; A+ is about 30-60 lines in delegated-admin-gate.ts plus a unit pin.",
    "B — The maintainer amends ADR-0131 D14 (Prime Directive #13; the #18413 amendment is the shape) to pull C1's business-unit leg into 17.x, single posture only. (1) The seed loader takes the sole-org fallback for sys_business_unit seeds, keyed on a declaration: sys_business_unit is admitted tenant-scoped in the ledger, citing the amendment, rather than by a hard-coded name. (2) Because the first boot seeds about 1.2 s before any organization exists, a later step must also stamp: either extend the sys_organization-insert handoff/backfillSeedTenancy to org-less seeded sys_business_unit rows when exactly one organization exists, or create the Default Organization before application seeds load (C1's other half, which changes ensureDefaultOrganization's "no admin -> no org" rule). Stored dev DBs: boot 2 measured 5/5 org-less units that persist across restarts, so the handoff would also rewrite stored rows, which D10 reserves for the manual ceremony unless the amendment exempts single posture. Pin: a new dogfood file with orgContext:true (in-subtree 2xx; out-of-subtree refused, with ablation). Cost: about 150-250 lines plus the pin and a patch changeset (metadata-protocol, runtime); it ships a fraction of C1 in 17.x and re-opens the per-object seed cut that #13491's re-ruling reserved.",
    "C — App-level: the showcase stops seeding sys_business_unit declaratively and writes the tree imperatively with the organization on sys_organization insert (the bind-position-sets.ts shape). Cost: an app workaround for a platform gap (a seeded BU tree is metadata-only, so by the app/platform boundary test it is the platform's). Every other app that seeds sys_business_unit keeps the trap, and the zoo's "the app seed can plant the platform org tree" stops describing the showcase.",
    "D — (listed for completeness, not proposed) The gate accepts the org-less residue under a non-walled posture, which is the card body's original landing. Triage ruled it out ("⛔ No change to the gate's organization match"), and the #15030 revert precedent rejects re-implementing the NULL arm in a second place."
    ],
    "recommendation": "A (A+ at the maintainer's option). Real business need (measured): the tree holds exactly ONE platform-namespace seed dataset, the showcase sys_business_unit tree (examples/app-showcase/src/data/seed/index.ts:249). No platform code inserts sys_business_unit. A business-unit tree written through REST/Setup carries the organization and works (measured 201). The gate fails CLOSED, so this is over-refusal with no exposure, and the pull is the showcase demo plus one P0 checklist row. Long-term soundness: C1 is the structural fix and is already decided; B ships a partial C1 early, which D14 forbids, and the same root cause on the sharing side (#14547) was declined for 17.x on exactly this reasoning. A keeps one record and one schedule. Preventing AI mistakes: today an AI-authored app that seeds sys_business_unit gets a refusal that names the wrong cause; A+ fixes that attribution without touching the boundary, while B removes the seed trap only for single posture. Startup focus: 「要 v18 才开发」 and 「我们现在创业阶段,应该定一套最稳定可靠的方案,而不是盲目追求功能。」 favour A. No new gate is proposed. If B is chosen, the PM re-dispatches against the amendment; the pushed branch can be reused."
    }
    ],
    "out_of_scope_findings": [
    "carrier: 承接者:无 · noted, not filed — the stock-boot warning [sharing-rule] share_new_inquiries_with_field_ops expands to NO recipients is NOT this class. It fired at 07:58:09.690Z on an org-less rule (sys_sharing_rule organization_id null, meta organization:null), before any sys_business_unit_member row existed (the first one was my repro's, at 07:58:56.317Z; users cannot be seeded). It reflects empty membership at boot, and stamping the units would not clear it.",
    "carrier: 承接者:无 · noted, not filed — Zone 2 #4, out of scope: the card body's triage note that a delegate's PATCH/DELETE of another user's assignment is refused by the ownership floor (record_access_denied) even after the units are stamped. Not measured here.",
    "carrier: 承接者:无 · noted, not filed — a stale comment: the delegated-admin-gate.ts callerOrganizationId docblock says "Undefined for a single-posture caller", but on a stock dev boot every session carries the Default Organization (measured: the member row stamped org_mup8s7vmxmqnsu36 under Tenancy: single). Read-only observation.",
    "carrier: 承接者:无 · noted, not filed — Zone 2 #2 census: the declaration census (scripts/platform-object-tenancy-census.json) records sys_business_unit as reach in (tenantField organization_id, no opt-out declaration), the runtime ledger (objectql/src/tenancy/platform-object-tenancy.ts) as unclassified (unlisted), and ADR-0131 D3 calls a seeded sys_business_unit "the organization's business unit". The tree holds no deployment-global platform-namespace seed dataset, so the seed loader's sys_ exemption currently governs that one dataset only. The cloud repo's cloud_ seeds were not read."
    ]
    }


    Generated by Claude Code

  6. 7 remaining items

  7. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    v18 pre-opening re-verification: still live on main. The body's landing site is superseded by the 10-04 ruling, so this card now waits on C1

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-06T14:35Z. ⛔ Not a claim, ⛔ not a dispatch. Read-only re-verification on objectstack main 6befe19c6e. The cut base was 2026-09-04, 3,298 commits earlier. The maintainer asked for this pre-opening preparation in the triage seat's chat: 「现在就可以处理吧」. Classes, positions and functions only. The claiming seat applies these corrections before building. The body is not rewritten.

    Blocked-by: #15195


    Generated by Claude Code

  8. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Unlock: pm:blocked → pm:queue. Its blockers, #15193 and C1 / C3 (#15195), are closed

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-08T17:55Z. Unlock scan. ⛔ Not a claim, ⛔ not a dispatch.

    Thread-read: 6018609865

  9. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 3 · 2026-10-08T18:15Z
    Session: session_01WkL6Eijt432S1Y7ekb6ovQ
    Account: os-bill (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-21057-delegated-subtree-reverify
    Worktree: objectstack-issue-21057
    Domain: domain:services
    Seat: domain:services#1 (seat post #6021)

    Re-verify first, as the unlock directs (6065890656): its blockers, #15193 and #15195 (PR #22186), are closed. Triage's pre-opening note (6018609865) moved the fix to C1: the Default Organization stamps the seed, the gate's organization match stays unchanged, and this card's pin becomes one of C1's acceptance pins.

    Round shape at origin/main 28bff18d:

    1. Reproduce the card's steps on a fresh showcase dev --seed-admin boot, over the REST data API. Do the seeded business units carry the organization now, and does the delegate's in-subtree assignment answer 2xx? The admin's control must answer 201.
    2. If it no longer reproduces: check whether a pin already holds it. That means a delegate test that sets an organization context, which packages/verify's harness and the dogfood delegate tests did not at 6018609865.
      • If none does, add one dogfood pin, test-only: a stock showcase-shaped boot, a delegate with an active organization, an in-subtree assignment answering 2xx, and an out-of-subtree one still refused. The PR opens Fixes #21057 with skip-changeset.
      • If a pin exists, report it, and the seat closes the card with the reading.
    3. If it still reproduces: report the mechanism on main and stop. The fix is C1's seed stamping, as ruled, and ⛔ the gate's match logic is not changed here.

    Exclusions:

    • ⛔ No change to delegated-admin-gate.ts's organization match.
    • ⛔ No seed-loader change.
    • ⛔ No packages/spec.

    Stop on breach; explain in the report.
    Container & model: S, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate; default tier)
    Clause-②: no

  10. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 21057,
    "status": "done",
    "branch": "claude/issue-21057-delegated-subtree-reverify",
    "pr": null,
    "session": "session_01WkL6Eijt432S1Y7ekb6ovQ — mode:subagent, so the dispatching seat's session id",
    "premise_still_valid": false,
    "summary": "The defect no longer reproduces on origin/main 28bff18: PR #22186 (ADR-0131 C1, 34dba5a) creates the Default Organization before the inline seed, so on a fresh showcase objectstack dev --seed-admin boot all 5 seeded sys_business_unit rows carry it, and the card's step 4 now answers 201 for the delegate (admin control 201, out-of-subtree still 403 PERMISSION_DENIED). The same boot shows the stamp is load-bearing: after the admin PATCHes organization_id to null on the three Field-Ops units, the delegate's in-subtree insert is refused 403 with the card's original message. H3: a pin already holds it. showcase-permission-zoo.dogfood.test.ts's delegate pair runs since #22186 on a bootStack that binds every sign-up to the Default Organization; a probe measured the zoo delegate's activeOrganizationId and the stamped seeded units, and a test-setup-only ablation (de-stamping the seeded units) turned the in-subtree case red. So no pin was added, no PR was opened, and per the unlock ruling 6065890656 the card can be closed with this reading.",
    "seeded_units": "H1 holds. Fresh boot 1 (DB read): sys_organization org_muzvc4cmlhf1ue43 slug default created 18:27:09.963Z; sys_business_unit bu_acme/bu_field_ops/bu_west_coast/bu_east_coast/bu_hq_finance created 18:27:10.455Z-.459Z, organization_id = org_muzvc4cmlhf1ue43 on 5 of 5 (census n=5 nulls=0); the admin user was created 18:27:11.900Z. Fresh boot 2 (REST GET as admin, then DB read): org_muzvie6e3cxlr3tq created 18:32:02.634Z, units created 18:32:03.073Z-.077Z, 5 of 5 stamped. Banner on both: Tenancy: single, Seeds: com.example.showcase 132 rows.",
    "rest_answers": "Fresh boot 2, origin/main 28bff18, cd examples/app-showcase; OS_PORT=P pnpm exec objectstack dev --seed-admin -p P -d file:boot2.db, REST data API with Bearer tokens. Admin session activeOrganizationId org_muzvie6e3cxlr3tq. invite-member 200 x5, sign-up/email 200 x5 (D, T, T2, T3, T4). Delegate D session activeOrganizationId = org_muzvie6e3cxlr3tq (equals the admin's). Admin POST sys_user_permission_set (D, showcase_field_ops_delegate ps_muzvifp9kt5o8sdj) -> 201; admin POST sys_business_unit_member (bu_field_ops, D) -> 201, stamped org_muzvie6e3cxlr3tq. [out-of-subtree, before any success] D POST sys_user_position {T, contributor, bu_hq_finance} -> 403 {code: PERMISSION_DENIED, error: "[Security] Access denied: delegated 'insert' on sys_user_position rejected — business unit 'bu_hq_finance' is outside the delegated subtree (scope from 'showcase_field_ops_delegate')."}. [card step 4, in-subtree] D POST sys_user_position {T, contributor, bu_west_coast} -> 201, row organization_id org_muzvie6e3cxlr3tq, granted_by = D. [admin control] admin POST {T2, contributor, bu_west_coast} -> 201. [out-of-subtree, after the success] D POST {T3, contributor, bu_hq_finance} -> 403, same code and message. [discriminator, same boot] admin PATCH sys_business_unit organization_id null on bu_field_ops / bu_west_coast / bu_east_coast -> 200 x3 (read back null on those 3, bu_acme and bu_hq_finance still stamped); D POST {T4, contributor, bu_west_coast} -> 403 PERMISSION_DENIED "business unit 'bu_west_coast' is outside the delegated subtree". The server was started by me in its own process group (pgid 19201, listener pid 19367) and stopped by that recorded pgid: no group member left, port 39958 not listening (lsof).",
    "existing_pins": "Found, and proven non-vacuous: packages/qa/dogfood/test/showcase-permission-zoo.dogfood.test.ts, cases 'a delegate assigns an allowlisted position inside their subtree' (bu_west_coast, toBeLessThan(300)) and 'the same delegate is refused OUTSIDE their subtree (no lateral reach)' (bu_hq_finance, not.toBeLessThan(300)). The file is unchanged since 2c87a48; what changed is packages/verify/src/harness.ts in #22186: bootStack now boots the production single shape, so the Default Organization exists before the seed and every sign-up is its member with activeOrganizationId (this replaces the autoDefaultOrganization: !!opts.orgContext pin that triage cited at harness.ts:571 in 6018609865). The seed half is also held by C1's acceptance pin packages/runtime/src/default-organization-boot-invariant.pin.test.ts (d) ('the app seed and the sys_business_unit seed are stamped'), which does not exercise the gate. Measurements, all under os-verify-lock, pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2 test/showcase-permission-zoo.dogfood.test.ts: [A, pristine, blob d9c1d7ca] Test Files 1 passed, Tests 13 passed (13). [C, probe, test-only console.log through scripts/ablation-replace.mjs nested WRAP, anchors 1 -> 0, blobs d9c1d7ca -> f83fd322 -> bc323c38] PROBE-21057 in-subtree 201 \"org_muzvnztk104zirm9\" with all 5 seeded units on org_muzvnztk104zirm9; PROBE-21057 out-of-subtree 403 with the gate's PERMISSION_DENIED 'outside the delegated subtree' body, so the negative case is answered by the gate and not by the (user_id, position, organization_id) unique index; 13 passed. [B, ablation, test setup only: in beforeAll the admin REST-PATCHes organization_id null onto bu_field_ops / bu_west_coast / bu_east_coast; ablation-replace anchor 1 -> 0, blob d9c1d7ca -> 32a8bf3c; printed ABLATION-21057 destamp ... 200 x3] × a delegate assigns an allowlisted position inside their subtree — AssertionError: in-subtree, allowlisted assignment passes: expected 403 to be less than 300, Tests 1 failed / 12 passed. A red only on an org-bound caller is the proof that the zoo delegate is org-bound: an org-less caller takes the by-name branch and would stay green. Restore after C and after B: ablation-replace ok restored: blob == HEAD (d9c1d7ca3df1) and git diff HEAD is empty, then my own check RESTORE-PROOF OK: blob d9c1d7ca == HEAD blob; git diff HEAD empty; porcelain: []. No rebuild leg was owed: the only mutated file is the test itself, which vitest runs from source; no dist-resolved subject was touched.",
    "added_pin": "none. H3 found an existing pin (above), and the order allows a pin only when none exists.",
    "tests": "No code was written, so no package test or typecheck is owed. Runs, all at 28bff18 under bash scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=dev-21057 and NODE_OPTIONS=--max-old-space-size=3072: (1) pnpm exec turbo run build --filter='@objectstack/dogfood^...' --filter='!@objectstack/example-showcase' --filter='!@objectstack/example-crm' --filter='!@objectstack/example-multi-package' --concurrency=1 -> Tasks 60 successful, 60 total, Cached 60 (FULL TURBO); VERDICT command-exit 0 · held the lock 1s · waited 79s. (2) boot 1 -> VERDICT command-exit 2 · held the lock 243s · waited 280s. My script addressed 127.0.0.1 and Better Auth refused every sign-in with Invalid origin: http://127.0.0.1:50642, so boot 1's REST leg is NOT MEASURED; its DB reading for H1 stands. (3) boot 2 (fresh DB, localhost) -> VERDICT command-exit 0 · held the lock 16s · waited 7s. All REST answers are above. (4) zoo A/C/B -> VERDICT command-exit 0 · held the lock 88s · waited 83s for the wrapper script; the inner exits were A 0, C 0, B 1 (the intended red). Wall-clock figures are shared-box seconds.",
    "gates": "none owed — the diff is empty. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack from the worktree -> EXIT=2: "this branch changes nothing against 'origin/main' (merge base 28bff18) — nothing to derive". The seat's lead list in gates-21057.txt was for the hypothetical pin path and does not apply.",
    "files_changed": [],
    "mcp_calls": "0 — no MCP GitHub tool used. The card and its comments were read with single-card REST GETs (gh api repos/objectstack-ai/objectstack/issues/21057 and .../issues/21057/comments --paginate).",
    "api_writes": "1 — this os-dev-report comment on #21057 via scripts/pm/post-stamped.mjs (fleet-write relay -> POST /repos//issues/21057/comments). Not REST writes: one git push -u origin claude/issue-21057-delegated-subtree-reverify of the empty branch (see deviations). No pr_create and no label-write were spent, because no PR exists.",
    "deviations": [
    "The empty branch was pushed at worktree creation (head 28bff18 = origin/main, no commit). That is os-dev basic rule 1's write-route probe. The order budgets a git push only if a pin is added. os-dev says its standard clauses win on conflict and the conflict must be named, so it is named here. The remote branch carries nothing; deleting it is the seat's call.",
    "The order says the card has 12 comments; the REST read shows 11 (the issue's comments field reads 11 and the paginated list returns 11, the newest being the claim 6066232512). All 11 were read, including 5980557328, 6018609865 and 6065890656.",
    "Boot 1's REST leg is NOT MEASURED (origin refusal caused by my script's 127.0.0.1 host; about 4 minutes of lock hold). Boot 2 is a fresh boot on a fresh DB and carries every REST answer.",
    "Card step 4, adapted and not literal: the admin control used a second target (T2) with the identical position and unit, because sys_user_position has a unique index (user_id, position, organization_id) and a literally identical second insert would collide after the delegate's success. The out-of-subtree refusal was measured both before and after the success, on targets T and T3. A discriminator leg (de-stamp, then in-subtree again on T4) was added on the same boot.",
    "Boot spelling: the card's own pnpm exec objectstack dev --seed-admin -p P -d file:FRESH.db from examples/app-showcase, not the pnpm dev script, to keep the card's step 1."
    ],
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: #15196 S5b (in flight on this seat; its pushed diff edits delegated-admin-gate.ts) · noted, not filed — the callerOrganizationId docblock at delegated-admin-gate.ts:84 still reads "Undefined for a single-posture caller". On a stock single boot every session carries the Default Organization (measured: the admin and the delegate both carry activeOrganizationId). Read-only doc drift; C1 did not rewrite it.",
    "carrier: 承接者:无 · noted, not filed — the zoo pin's strength. Its negative assertion is not.toBeLessThan(300), looser than the 403 PERMISSION_DENIED it receives today (probe C). It asserts no precondition that the delegate's session carries an organization: that property comes from bootStack since #22186, and harness.org-context.test.ts pins the admin's binding, not a sign-up's. Observation; no filing class.",
    "carrier: 承接者:无 · noted, not filed — both fresh boots still log once [sharing-rule] active business-unit rule expands to NO recipients for share_new_inquiries_with_field_ops (businessUnit bu_field_ops, "organization":null). Round 1 judged this not this class (empty membership at boot). Not re-measured beyond the boot log."
    ]
    }


    Generated by Claude Code

  11. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Closed with the re-verification reading · domain:services seat 1 (#6021) · session_01WkL6Eijt432S1Y7ekb6ovQ · 2026-10-08T18:42Z. As the unlock (6065890656) directs: "If the empty subtree no longer reproduces, close the card with that reading."

    Read on origin/main 28bff18d (os-dev-report 6066637266):

    Fixed by #15195 (PR #22186). The gate's organization match is unchanged, as ruled. Closed completed; this note also removes pm:dispatched and the assignee.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:servicespriority:p1High: required for production / M2target:v18

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions