Repository navigation
[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
Activity
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsTriage: 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 widenTriage 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_idthe 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-adminboot, a delegate's in-subtree assignment is accepted, and an out-of-subtree one is still refused (the control).
Generated by Claude Code
- Seeded business units carry the
- addedarea:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3
on Oct 1, 2026 objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsTriage: re-grade —
priority:p2→priority:p1. A red row in the P0 release sweep; the direction is unchangedTriage 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 isaccess-security.crud-permission-matrixacceptance[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
- addedpriority:p1High: required for production / M2High: required for production / M2and removedpriority:p2Medium: important, M3Medium: important, M3
on Oct 1, 2026 objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 · 2026-10-01T07:42Z
Session:session_01DiCSbmJrkzNhuEAier4VoJ
Account:os-bill(the seat's linked user asGET /useranswers 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 seededsys_business_unitrows. Triage's direction5925045171is that the seed stamps the organization. Todaypackages/metadata-protocol/src/seed-loader.tskeeps everysys_/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 underexamples/app-showcase/src/data/seed/), the fix lands at the producer and the report names it. Also in scope: a pin underpackages/qa/dogfood/test/(a new file, run withorgContext: true) and a changeset. ⛔ No change to the organization match inpackages/plugins/plugin-security/src/delegated-admin-gate.ts(triage direction). ⛔ No edit tosecurity-plugin.ts(#21063 holds it). Stop on breach; explain in the report.
Cross-lane:packages/metadata-protocolisdomain: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 openpm:dispatchedcard (29 cards, 26 branches) againstorigin/main. None touchesseed-loader.ts,delegated-admin-gate.ts,per-organization-catalog.tsor the showcase seed. Themetadata-protocolfiles 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) holdsexamples/app-showcase/src/security/permission-sets.tsand two showcase dogfood files; the pin here is a new file, so the two are disjoint. #21063 holdssecurity-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
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsMaintainer ruling, recorded ·
domain:servicesseat 2 (#21118) ·session_01DiCSbmJrkzNhuEAier4VoJ· 2026-10-01T08:03Z · ⛔ Not a claim (the claim is5927038194).- The question: the card is
domain:services, but triage's direction (5925045171) puts the probable landing inpackages/metadata-protocol/src/seed-loader.ts, which isdomain: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 throughpm: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
- The question: the card is
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsos-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 stockobjectstack dev --seed-adminshowcase 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 ofsys_/cloud_/ai_seeds from stamping ('intentionally global') is withdrawn — there are no platform-global seeds left; a seededsys_business_unitis 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.tsorgScope docblock; plugin-sharing CHANGELOGab47816). 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 readsTenancy: singleandSeeds: 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 throughgh apiGET: 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: #15193rather 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 ofsys_/cloud_/ai_seeds from stamping … is withdrawn … a seededsys_business_unitis 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 readsunclassified, "⛔ Do not promote an entry totenant-scopedto shorten the list").",
"options": [
"A — Hold for v18 C1, the #14547 precedent. Re-label #21057 pm:blocked withBlocked-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 admittedtenant-scopedin 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 recipientsis 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 asingle-posture caller", but on a stock dev boot every session carries the Default Organization (measured: the member row stamped org_mup8s7vmxmqnsu36 underTenancy: 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 reachin(tenantField organization_id, no opt-out declaration), the runtime ledger (objectql/src/tenancy/platform-object-tenancy.ts) asunclassified(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
7 remaining items
objectstack-fleet commented
on Oct 6, 2026 ContributorAuthorMore actionsv18 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 C1Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-06T14:35Z. ⛔ Not a claim, ⛔ not a dispatch. Read-only re-verification on objectstackmain6befe19c6e. 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
- Holds on
main:callerOrganizationIdreturns the organization with no posture check (delegated-admin-gate.ts:93; the stale docblock is at:84);resolveSubtree/resolveSubtreeByIdare unchanged (:1091,:1118);- seeded business units are still organization-less (
seed-loader.ts:1190;examples/app-showcase/src/data/seed/index.ts:248-258); - the pin stays green because no delegate dogfood test sets
orgContext(packages/verify/src/harness.ts:571).
- Since filing: only a strings-only change to the gate (
e3ad4922ed). It reproduced again on 10-04. - Scope, per the thread: triage ruled the gate's organization match unchanged and the seed stamped. The dev's option D was rejected, and the maintainer ruled on 10-04 to wait for v18.
- So the fix is C1's: the Default Organization stamps the seed.
- The body's "Landing site and acceptance" (the gate, accepting org-less rows) no longer applies.
- Why the new
Blocked-by: naming only [gate] the v18 development line is not open — ADR-0131 execution cards are blocked on this card #15193, this card would return to the queue beside feat(objectql,plugin-auth): the Default Organization is load-bearing undersingle; an unstamped write is derived there and refused everywhere else (ADR-0131 D3/D9/D11) #15195 at the opening, with both touchingseed-loader.ts. It now waits on feat(objectql,plugin-auth): the Default Organization is load-bearing undersingle; an unstamped write is derived there and refused everywhere else (ADR-0131 D3/D9/D11) #15195, and its pin becomes one of C1's acceptance pins. - Keep: when C3 moves
resolveOwnOrganizationRow, the gate's match logic is unchanged.
Generated by Claude Code
- Holds on
objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsUnlock:
pm:blocked→pm:queue. Its blockers, #15193 and C1 / C3 (#15195), are closedTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-08T17:55Z. Unlock scan. ⛔ Not a claim, ⛔ not a dispatch.Thread-read: 6018609865
- [gate] the v18 development line is not open — ADR-0131 execution cards are blocked on this card #15193 is closed, and feat(objectql,plugin-auth): the Default Organization is load-bearing under
single; an unstamped write is derived there and refused everywhere else (ADR-0131 D3/D9/D11) #15195 closedcompletedat 2026-10-08T16:58Z (PR feat(plugin-auth,objectql,metadata-protocol,runtime)!: undersinglethe Default Organization exists before the seeds and the listener; an unowned seed row or system write is derived there or refused (ADR-0131 C1) #22186,34dba5ae1e). - Re-verify on
mainfirst. My note6018609865found it live before the v18 opening, and C3 may have changed the seeded-unit path. If the empty subtree no longer reproduces, close the card with that reading. The grade is unchanged (p1,target:v18).
- [gate] the v18 development line is not open — ADR-0131 execution cards are blocked on this card #15193 is closed, and feat(objectql,plugin-auth): the Default Organization is load-bearing under
objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsClaim: PM loop round 3 · 2026-10-08T18:15Z
Session:session_01WkL6Eijt432S1Y7ekb6ovQ
Account:os-bill(the seat's linked user asGET /useranswers 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/main28bff18d:- Reproduce the card's steps on a fresh showcase
dev --seed-adminboot, 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. - 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 at6018609865.- 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 #21057withskip-changeset. - If a pin exists, report it, and the seat closes the card with the reading.
- 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
- If it still reproduces: report the mechanism on
mainand 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- A re-verification and, at most, a test-only pin. No accept-set moves.
Responsibility:this repository's own code: seeded business units carried no organization, so the delegated-admin gate resolved an empty subtree (fixed by C1 if the re-verification holds) | none: no delegate test set an organization context, so the gate's pin stayed green | ADR-0090 D12 delegated administrators on stock boots
Thread-read: 6065890656
Serial constraints cleared: - No open PR touches
delegated-admin-gate.tsor the seed loader. - feat(core,objectql,plugin-security,plugin-sharing): the catalog is read from the registry; assignment tables reference it by name (ADR-0131 D2/D3/D4) #15196 S5b (this seat, in flight) reads
delegated-admin-gate.ts's grant pre-images. This round writes no source file.
- Reproduce the card's steps on a fresh showcase
objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsos-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 showcaseobjectstack dev --seed-adminboot 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 ispackages/verify/src/harness.tsin #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 theautoDefaultOrganization: !!opts.orgContextpin that triage cited at harness.ts:571 in 6018609865). The seed half is also held by C1's acceptance pinpackages/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 403with 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; printedABLATION-21057 destamp ... 200x3]× 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-replaceok restored: blob == HEAD (d9c1d7ca3df1) and git diff HEAD is empty, then my own checkRESTORE-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 underbash scripts/pm/os-verify-lock.shwith 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 withInvalid 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 83sfor 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/objectstackfrom 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/21057and.../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: onegit push -u origin claude/issue-21057-delegated-subtree-reverifyof 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'scommentsfield 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 ownpnpm exec objectstack dev --seed-admin -p P -d file:FRESH.dbfrom examples/app-showcase, not thepnpm devscript, 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 — thecallerOrganizationIddocblock at delegated-admin-gate.ts:84 still reads "Undefined for asingle-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 isnot.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, andharness.org-context.test.tspins 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 recipientsfor 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
objectstack-fleet commented
on Oct 8, 2026 ContributorAuthorMore actionsClosed with the re-verification reading ·
domain:servicesseat 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/main28bff18d(os-dev-report6066637266):- The seed is stamped. A fresh showcase
objectstack dev --seed-adminboot was read twice. All 5 seededsys_business_unitrows carry the Default Organization. PR feat(plugin-auth,objectql,metadata-protocol,runtime)!: undersinglethe Default Organization exists before the seeds and the listener; an unowned seed row or system write is derived there or refused (ADR-0131 C1) #22186 (ADR-0131 C1,34dba5ae1e) creates that organization before the inline seed. - The card's step 4 now passes:
- the delegate's in-subtree
sys_user_positioninsert answers201; - the admin's control answers
201; - an out-of-subtree insert is still refused
403 PERMISSION_DENIEDby the gate, measured before and after the success.
- the delegate's in-subtree
- The stamp is load-bearing: on the same boot, nulling the three Field-Ops units' organization brings back the original
403. - A pin already holds it:
packages/qa/dogfood/test/showcase-permission-zoo.dogfood.test.ts's delegate pair, in-subtree and out-of-subtree.- Since feat(plugin-auth,objectql,metadata-protocol,runtime)!: under
singlethe Default Organization exists before the seeds and the listener; an unowned seed row or system write is derived there or refused (ADR-0131 C1) #22186, its boot binds every sign-up to the Default Organization. It went 13 of 13 green. - A test-setup-only ablation that de-stamps the seeded units turns the in-subtree case red.
- C1's own pin (
default-organization-boot-invariant.pin.test.ts(d)) holds the seed half. So no pin was added and no PR was opened.
- Since feat(plugin-auth,objectql,metadata-protocol,runtime)!: under
Fixed by #15195 (PR #22186). The gate's organization match is unchanged, as ruled. Closed
completed; this note also removespm:dispatchedand the assignee.- The stale
callerOrganizationIddocblock indelegated-admin-gate.ts("Undefined for asingle-posture caller") is carried by feat(core,objectql,plugin-security,plugin-sharing): the catalog is read from the registry; assignment tables reference it by name (ADR-0131 D2/D3/D4) #15196 S5b, which edits that file now.
- The seed is stamped. A fresh showcase
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 freshobjectstack dev --seed-adminshowcase boots (runner + independent verifier). Reader: triage first touch, then the lane owningplugin-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 NULLorganization_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(adminScopeField 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
OS_PORT=<p> pnpm -C examples/app-showcase exec objectstack dev --seed-admin -p <p> -d file:<fresh>.db; adminadmin@objectos.ai/admin123.POST /api/v1/auth/organization/invite-member, thenPOST /api/v1/auth/sign-up/email).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.POST /api/v1/data/sys_user_position {"user_id":T,"position":"contributor","business_unit_id":"bu_west_coast"}(bu_west_coastis a child ofbu_field_ops).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')".PATCHesorganization_idonto the seededbu_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
sys_business_unitrows haveorganization_id: nullby design — the seed loader never stamps the fallback organization onsys_*seeds (packages/metadata-protocol/src/seed-loader.ts).delegated-admin-gate.tscallerOrganizationId()returnscontext.organizationId ?? context.tenantIdwithout consulting the tenancy posture. Its docblock says the value is "Undefined for asingle-posture caller", but the dev boot's default-organization bootstrap gives every session anactiveOrganizationId, so it is always defined (boot banner:Tenancy: single).resolveSubtreethen reads the scope root throughresolveOwnOrganizationRow(...).own, which only matches rows stamped with that organization (per-organization-catalog.ts);resolveSubtreeByIdalso drops children whoseorganization_id !== organizationId. The org-less seeded tree is filtered out, the subtree is empty, and every write fails closed.per-organization-catalog.tscalls org-less rows "the CORRECT shape" under thesingleposture, and the enforcement loaderdbLoaderForinsecurity-plugin.tsusesown ?? organizationLessResidue, with a comment thatresolveOwnOrganizationRowis written for seeders.DelegatedAdminGateis 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 withoutorgContext, sopackages/verify/src/harness.tssetsautoDefaultOrganization: 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
delegated-admin-gate.ts(callerOrganizationId/resolveSubtree/resolveSubtreeById): resolve the scope the waydbLoaderFordoes — accept the org-less residue under a non-walled posture (or pass the posture in), without reopening the cross-organization by-name hazard [finding] the delegated-admin gate resolves a scope's business-unit anchor by NAME across organizations — in a single-database multi-org posture, a delegate can be handed another organization's subtree or lose its own, depending on which id sorts first #19775 closed.orgContext: true(the shared stack cannot carry it), asserting an in-subtree assignment succeeds and an out-of-subtree one is refused.access-security.crud-permission-matrixacceptance[0] re-runs green.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 toadminScope.manageAssignments, so a fixed gate may still not let a delegate re-staff others' assignments.Generated by Claude Code