Repository navigation
Description
Found while implementing #14157 (the dev-admin seed gating on user rows instead of on a login). Filed unassigned; this is a CODE READING at 909a441, not an end-to-end measurement — the composed boot was not driven.
The reading
bootstrapPlatformAdmin (packages/plugins/plugin-security/src/bootstrap-platform-admin.ts, the single posture) selects its promotion target as the OLDEST human sys_user row:
507 const allUsers = await tryFind(ql, 'sys_user', {}, 50);
508 const humanUsers = allUsers.filter(isHumanUser);
513 const target = oldestOf(humanUsers); // sorted by created_at, first wins
526 logger.info('[security] first user promoted to platform admin: ' + target.email)
isHumanUser there is the mirror of isHumanUserRow — it excludes only the legacy usr_system service account. A person declared in defineStack({ data }) is an ordinary human row with no sys_account, and the declarative seed is awaited inside AppPlugin.start(), so those rows are always OLDER than any account created at kernel:ready or later.
So on an app that seeds people, the reading is that admin_full_access is granted to person0@demo.example — a directory row with no credential, which nobody can sign in as — while the account that CAN sign in is not promoted. sys_user_permission_set then carries a grant that can never be exercised, and the loginable admin lands in an app it cannot read.
Two things make this worth a card rather than a footnote:
- It is the SECOND half of the shape
maybeSeedDevAdmingates on "any sys_user row", but runs atkernel:ready— so any app that seeds people permanently loses theobjectstack devlogin #14157 fixes. That card moved the dev-admin seed off "any human row" and onto "a login exists"; this site still treats a directory row as a candidate for the platform-admin grant. AftermaybeSeedDevAdmingates on "any sys_user row", but runs atkernel:ready— so any app that seeds people permanently loses theobjectstack devlogin #14157 lands,objectstack devon a people-seeding app DOES mint a loginable admin — and on this reading that admin still would not be promoted, so the CLI's declared "then promote it to platform admin" is only half delivered.maybeSeedDevAdmingates on "any sys_user row", but runs atkernel:ready— so any app that seeds people permanently loses theobjectstack devlogin #14157 deliberately did not touch it: choosing between "oldest human", "oldest human WITH an account", and "the account the seed just provisioned" is a design decision, not a mechanical repair. - The grant is WRITTEN, not merely computed — an unusable row lands in the permission tables and stays there.
What to measure before fixing
- Drive a real composed boot (
objectstack dev) on an app whosedefineStack({ data })declaressys_userrows, then readsys_user_permission_set: whichuser_idholdsadmin_full_access, and does that user have anysys_accountrow? - Check the ordering: does
bootstrapPlatformAdminrun before or after AuthPlugin'skernel:readyseed? The selection is over whatever population exists when it runs. claimSeedOwnershiphands seeded business records to the promoted admin, so a wrong target also mis-assigns ownership.
Direction, not a decision
The narrow reading of "first user" that the code comment gives ("the real admin login") suggests the target should be the oldest human that can actually authenticate — one with a sys_account row — which is the same row-versus-login correction #14157 made one package over. Adjacent but distinct: #11979 is about config-anchoring the single posture itself, not about which row it selects.
Generated by Claude Code
Generated by Claude Code
Activity
huangyiirene commentedon Sep 2, 2026
Triage (R+90, triage seat, session
session_019kDRpB7D2XzVzkaLp57T5D):pm:queue·priority:p1·security(topic marker) ·domain:services· type Bug.Measurement is the first deliverable, as the card asks — it is a code reading at
909a4417, not a driven boot. Drive a composedobjectstack devon an app whosedefineStack({ data })declaressys_userrows, then readsys_user_permission_set: whichuser_idholdsadmin_full_access, and does that user have asys_account? Establish the ordering (doesbootstrapPlatformAdminrun before or after AuthPlugin'skernel:readyseed — the selection is over whatever population exists when it runs), and checkclaimSeedOwnership, since a wrong target also mis-assigns seeded business records. A measurement showing the reading is wrong is a complete answer and closes this card.If it reproduces, the repair direction is inherited, not invented: #14157 made exactly this row-versus-login correction one package over, and this site's own comment already says the target is meant to be "the real admin login" ⇒ promote the oldest human that can authenticate (has a
sys_account), not the oldest human row. That is restoring the stated intent, not choosing a new policy.⛔ One fork is reserved to the maintainer: if the measurement shows the change would move who holds
admin_full_accesson an existing deployment — rather than only fixing which row a fresh bootstrap promotes — stop and report to the decision inbox. Re-pointing an already-granted platform admin is a permission-boundary act (2026-08-28 negative boundary), ⛔ never a lane's call.p1: the CLI's declared "then promote it to platform admin" is half-delivered, the grant is written to
sys_user_permission_setand unusable, and the account that can sign in lands in an app it cannot read.Family: #14157 (the dev-seed half) · #14349 (the bootstrap-population posture question) · #11979 (config-anchoring the
singleposture — adjacent, distinct).Size/model suggestion: M,
opus.Generated by Claude Code
os-sales commentedon Sep 2, 2026
Claim: #14348 — PM seat session_01AUF1NoViznQK32gqpK8wS8 (
os-sales),domain:services, round R3, 2026-09-02 ~03:45Zclaude/issue-14348-promote-authenticable-first-user../objectstack-issue-14348(the dev creates it fromorigin/mainat claim;pnpm installthere)domain:servicesobjectstack devboot on a people-seeding app). WRITE, only if the reading reproduces and the maintainer-reserved fork does not fire:packages/plugins/plugin-security/src/bootstrap-platform-admin.ts(bootstrapPlatformAdmin, target selection around :507-526 at909a4417, re-taken by the dev) plus tests underpackages/plugins/plugin-security/src/**plus one changeset for@objectstack/plugin-security. READ-ONLY:packages/plugins/plugin-auth/**(held by PR fix(plugin-auth): gate the dev-admin seed on a LOGIN, not on user rows, and admit its own provisioning call #14352 until it merges; the seed it adds is the population this card measures against);packages/speczero writes.os-devsubagent, modelopus(triage size M /opus). dispatch-gates--tierderived atorigin/mainef8a4b9e: "Model tier — no path-derived mandate: the surface hits none of the 3 declared glob(s), derived here, not recalled. … Clause ② is NOT reachable from paths … This line is a FLOOR, never a clearance."admin_full_accesson an EXISTING deployment rather than only which row a fresh bootstrap promotes — is a stop-and-report to the decision inbox, never the dev's call.plugin-securityis held by no open PR (the dev re-verifies before its first edit);plugin-authis read-only for this card. A deployment that seeds a people directory and no credentials is locked out for good: bootstrap-status and the audience bootstrap bypass both count HUMANS, not LOGINS #14349 (posture) andmaybeSeedDevAdmingates on "any sys_user row", but runs atkernel:ready— so any app that seeds people permanently loses theobjectstack devlogin #14157 (the seed half) are notBlocked-by:— the measurement runs onorigin/main; if PR fix(plugin-auth): gate the dev-admin seed on a LOGIN, not on user rows, and admit its own provisioning call #14352 has merged by then the dev measures on that tree and says which.Generated by Claude Code
os-sales commentedon Sep 2, 2026
Claim:
os-devseat, sessionsession_01AUF1NoViznQK32gqpK8wS8, branchclaude/issue-14348-promote-authenticable-first-user(pushed empty as the write-routing probe before the first edit).Dispatched by PM seat
os-sales, round R3,domain:services. Declared file surface for this claim:packages/plugins/plugin-security/src/bootstrap-platform-admin.ts, new/updated tests underpackages/plugins/plugin-security/src/**, one changeset for@objectstack/plugin-security.packages/plugins/plugin-auth/**(held by PR fix(plugin-auth): gate the dev-admin seed on a LOGIN, not on user rows, and admit its own provisioning call #14352),packages/spec(zero writes).Measurement is the first deliverable per the triage comment; the reserved maintainer fork (would the repair move an already-granted
admin_full_accesson an existing deployment) is answered from the code before any edit.Generated by Claude Code
Generated by Claude Code
os-sales commentedon Sep 2, 2026
Dev interruption record — usage limit, not a stop; same agent resumed
The
os-devsubagent for this card was terminated at 2026-09-02 ~04:26Z by the account's 5-hour usage limit (HTTP 429rate_limit, reset 05:50Z). This is not a maintainer stop and not a dead claim: the claim (this seat,session_01AUF1NoViznQK32gqpK8wS8, branchclaude/issue-14348-promote-authenticable-first-user) is unchanged.State measured at 09:22Z before resuming: the remote branch exists with 0 commits ahead of
origin/main(nothing pushed); the local worktree holds two local commits (a3a6d9dc4) plus an untracked.changeset/promote-authenticable-first-user.md; the dev had just finished typecheck of the edited test files and was deriving the gate families when it died.origin/mainhas moved ~47 commits since the branch base.Resumed the same agent at ~09:25Z after the maintainer confirmed the reset, with the instruction to re-read its worktree, merge
origin/main, re-run the gates on the merged head, then push, open the draft PR and file the report. The measure-first stop condition from the dispatch order (⛔ stop and return to the decision inbox if the fix would move an existing deployment's admin) stands unchanged.Generated by Claude Code
os-sales commentedon Sep 2, 2026
os-dev-report
{ "issue": 14348, "status": "rework", "branch": "claude/issue-14348-promote-authenticable-first-user", "pr": "https://github.com/objectstack-ai/objectstack/pull/14532", "premise_still_valid": true, "summary": "The card reproduces exactly, measured on a driven composed boot (not a code reading). bootstrapPlatformAdmin (single posture) now promotes the oldest human holding a sys_account instead of the oldest sys_user row; when humans exist but none can authenticate it promotes nobody, writes no grant row and logs one info line (no new error-level sink site); and shouldReplayBootstrapFor also fires on sys_account inserts, which is load-bearing because a sign-up writes its sys_user row BEFORE its sys_account row. Status is rework, not done, for one reason: the change turns packages/plugins/plugin-auth/src/human-user-predicate-agreement.pin.test.ts red (8 failures), and packages/plugins/plugin-auth/** is read-only for this claim, so it is untouched here and CI will be red on plugin-auth until that pin is routed. The reserved maintainer fork is NOT triggered: the already_have_admin short-circuit runs before any target selection, so no existing deployment's admin can move. PM-side half-state noted: assignee and pm:dispatched were already set by the PM as the order said.", "tests": "Union run at de7dd456b (merge of origin/main 00ff228fe; clean merge, no conflicts, no overlap with my 4 files). MEASUREMENT (instrument: @objectstack/verify bootStack = AppPlugin then AuthPlugin then SecurityPlugin, the serve.ts:3382/:3590 order, over in-memory SQLite). On origin/main 1dcb995f: admin_full_access to person0@demo.example, has_sys_account false, sys_account table EMPTY, claimSeedOwnership ownershipClaimed 2 to that same row, and a later real sign-up with a credential account NOT promoted (replay answered already_have_admin). ORDERING measured by using the population as the probe: with no seeded people the promoted row is admin@objectos.ai, which exists only because AuthPlugin's kernel:ready dev-admin seed (auth-plugin.ts hook :975, seed :976) created it, so auth's kernel:ready completes before security's bootstrap (security-plugin.ts:3612); consistent with dispatchHookPropagating running handlers in registration order. POST-MERGE: with #14352 on main the defect SURVIVES - ablating only my selector on the merged tree still granted to person0@demo.example (holder_has_account false) and still handed it both seeded records WHILE a loginable admin@objectos.ai existed; with the selector restored the same boot promotes admin@objectos.ai. SUITES: pnpm --filter @objectstack/plugin-security test = 95 files / 1785 tests passed; typecheck clean with both edited test files confirmed in the tsconfig.test.json program via --listFiles (1 hit each); pnpm --filter @objectstack/verify test = 9 files / 48 passed; showcase-permission-seeding.dogfood.test.ts = 3 passed. GATES: derived on the merged head via node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (37 commands, no stale-tree warning), every exit captured before any pipe - 35 green, 0 red, 2 NOT MEASURED at exit 3 = PREREQUISITE NOT MET per their own verdict text (check-test-completeness.mjs grades a saved turbo test log that does not exist locally; check:dual-build-cjs-loads reads built output for packages this worktree never built). ABLATION 1 (selector, source-resolved): mutation confirmed on disk - anchor count 1 to 0, injected marker present, blob hash e4be93e5 to 70e59faa - 6 of 11 new cases go RED; restore proven by hash equality with the HEAD blob plus an empty git diff HEAD and marker count 0. ABLATION 2 (replay arm, dist-resolved, both legs rebuilt): FIRST ATTEMPT WAS VOID and is reported as such - the marker was a // comment, which tsup strips, so ablation-dist-preflight found it only in sourcemaps and refused the reading; re-run with an executable string literal, preflight confirmed the marker in dist/index.js and dist/index.mjs, and the mutated driven boot then promoted NOBODY even after a successful sign-up (two no_authenticable_user passes, empty grant table). Restore leg: rebuild exit 0, preflight --absent green, whole-tree git status --porcelain empty. On the mutated leg pnpm build exits 1 at the DTS step only (TS narrowing complaint about the synthetic marker); the JS bundles built and carry the mutation, which is what the runtime measurement consumes. NOT MEASURED and declared as such: no repo-wide pnpm lint run (CI owns the farm).", "mcp_calls": "4 - one claim comment, one pull_request_read get_files on #14352, one create_pull_request, one report comment. Card body and all comments were read through the zero-quota public-repo payload channel, never MCP.", "open_questions": [ { "question": "Who repairs packages/plugins/plugin-auth/src/human-user-predicate-agreement.pin.test.ts, whose probe this change makes stale? It reads plugin-security's isHumanUser verdict indirectly as report.adminPromoted, and promotion has legitimately gained a second conjunct (holds a sys_account). The two human predicates still agree - isHumanUser is unchanged by this PR - but the probe no longer isolates them, so 8 cases fail. It is NOT a one-liner: modelling a sys_account in that file's fake engine fixes six of the seven failing rows, and the seventh is the corpus entry with an email but NO id, which cannot be keyed to an account at all. This PR skips id-less rows because a row with no id cannot hold an exercisable grant (the old code promoted it by writing user_id: undefined) - the same fail-closed direction that file's own guard already chose - but that is a judgement about the pin's contract, and the pin's own failure message reserves it: 'Do NOT resolve this by editing one of them until it has been decided which is right.'", "options": [ "A - Extend this claim's file surface to that one pin file. Measured: PR #14352 changed 8 files and that pin is NOT among them, so the read-only clause's stated reason (held by #14352) does not apply to it and there is no collision risk.", "B - Route the pin to the plugin-auth owner or a follow-up card, since the id-less-row question is about what that pin should assert, and the package is theirs.", "C - Weaken this change so the pin stays green - promote the oldest human row when no human can authenticate." ], "recommendation": "A, with B as an equally acceptable routing. Long-term soundness leads and rules out C outright: C is a consumer-side tolerant fallback, exactly what Contract-first forbids, and it re-creates the defect this card exists to fix - on a people-seeded app it would again write an unusable grant to person0@demo.example, which is the harm the card calls out as WRITTEN rather than merely computed. Between A and B the difference is only routing latency, not correctness; A is cheapest because the collision reason was measured absent, and the repair is bounded and mechanical for six of seven rows. Real business need: an app seeding a people directory is a shipped, exercised shape (#14157 fixed the seed half for exactly it), so leaving CI red blocks a real fix rather than a speculative one. AI-authorability: the fixed direction is the safer one to generalise - a promotion predicate that refuses malformed and credential-less rows fails closed, which is what stops a generated metadata app from silently minting an unusable platform admin. Startup scope discipline: neither A nor B adds surface; both are fixture work on an existing pin, and C would add a permanent tolerant branch." } ], "out_of_scope_findings": [] }Generated by Claude Code
Generated by Claude Code
os-sales commentedon Sep 2, 2026
Claim file-surface revision (PM seat) — one adjacent pin file added for patch round 1
PR #14532 (head
de7dd456b) implements the fix inside the claimed surface and reports, without touching it, thatpackages/plugins/plugin-auth/src/human-user-predicate-agreement.pin.test.tsgoes red (8 failures) because that pin reads plugin-security's promotion result as a proxy for the human predicate, and promotion has legitimately gained the "can authenticate" conjunct this card asks for. Both human predicates are unchanged and still agree; the probe is stale.The seat (
session_01AUF1NoViznQK32gqpK8wS8,os-sales) widens this claim's file surface by exactly that one file, checked against the four adjacent-fix conditions: same defect family (a probe that conflates promotion with the human predicate), mechanical in intent (repoint the probe, or modelsys_accountin the file's fake engine), file held by no open PR or claim (#14360's fence isauth-manager.ts/admin-ban-endpoints.ts; #14353 is not dispatched), same gate family (package test suite). The rest ofpackages/plugins/plugin-auth/**stays read-only for this claim. Order of preference given to the dev: (1) repoint the probe at the predicate itself if reachable without a new export; (2) otherwise model accounts for id-bearing rows and assert the fail-closed outcome for the id-lessghost@example.testrow with a docblock — never by editing either predicate, and never by adding an export (Clause-② staysno; a new export is a stop condition).Patch round 1 issued to the same dev at ~10:22Z. Landing waits for the patched head to be green.
Generated by Claude Code
test(plugin-auth): repoint the human-predicate pin's probe past the p…
os-sales commentedon Sep 2, 2026
os-dev-report
SUPERSEDES the earlier
os-dev-reportcomment (5508036293, headde7dd456b, statusrework). This is the current report: patch round 1 landed, head is now3360e5f97, statusdone. Parse this one.{ "issue": 14348, "status": "done", "branch": "claude/issue-14348-promote-authenticable-first-user", "pr": "https://github.com/objectstack-ai/objectstack/pull/14532", "head_sha": "3360e5f97", "mergeable_state": "blocked — the expected state for a DRAFT PR; state open, draft true, 5 files, +678/-18. Not a merge conflict: the branch merges origin/main 00ff228fe cleanly and the base moved on afterwards, as it does for every open PR.", "premise_still_valid": true, "summary": "The card reproduces exactly, measured on a driven composed boot rather than read from the code. bootstrapPlatformAdmin (single posture) now promotes the oldest human holding a sys_account instead of the oldest sys_user row; when humans exist but none can authenticate it promotes nobody, writes no grant row and logs one info line (no new error-level sink site); and shouldReplayBootstrapFor also fires on sys_account inserts, which is load-bearing because a sign-up writes its sys_user row BEFORE its sys_account row. Patch round 1 closed the one item that had held this at rework: the human-predicate pin in plugin-auth, on the single file the PM added to this claim's surface. The reserved maintainer fork is NOT triggered - the already_have_admin short-circuit runs before any target selection, so no existing deployment's admin can move; a dedicated case pins that boundary. PM-side half-state noted: assignee and pm:dispatched were already set by the PM as the order said.", "tests": "Union run at 3360e5f97 (branch base: clean merge of origin/main 00ff228fe, no conflicts). MEASUREMENT (instrument: @objectstack/verify bootStack = AppPlugin then AuthPlugin then SecurityPlugin, the serve.ts:3382/:3590 order, over in-memory SQLite). On origin/main 1dcb995f: admin_full_access to person0@demo.example, has_sys_account false, sys_account table EMPTY, claimSeedOwnership ownershipClaimed 2 to that same row, and a later real sign-up with a credential account NOT promoted (replay answered already_have_admin). ORDERING measured by using the population as the probe: with no seeded people the promoted row is admin@objectos.ai, which exists only because AuthPlugin's kernel:ready dev-admin seed (auth-plugin.ts hook :975, seed :976) created it, so auth's kernel:ready completes before security's bootstrap (security-plugin.ts:3612); consistent with dispatchHookPropagating running handlers in registration order. POST-MERGE: with #14352 on main the defect SURVIVES - ablating only my selector on the merged tree still granted to person0@demo.example (holder_has_account false) and still handed it both seeded records WHILE a loginable admin@objectos.ai existed; with the selector restored the same boot promotes admin@objectos.ai. SUITES: plugin-security test = 95 files / 1785 passed; plugin-auth test = 88 files / 1816 passed; plugin-security typecheck clean with both edited test files proven in the tsconfig.test.json program via --listFiles (1 hit each); plugin-auth typecheck clean including check:test-typecheck ('OK - test layer compiles'), with the edited pin file proven in THAT program via tsconfig.test.json --listFiles (1 hit) since the base tsc --noEmit program excludes tests; verify test = 9 files / 48 passed; showcase-permission-seeding.dogfood.test.ts = 3 passed. PIN: 22 passed (22), from 8 failed / 14 passed before the patch. GATES: re-derived on the patched head (37 commands, identical list, no new family despite the grown change set), every exit captured before any pipe - 36 green, 0 red, 1 NOT MEASURED (check-test-completeness.mjs, exit 3 PREREQUISITE NOT MET: it grades a saved turbo run test log that only CI produces). Three gates first reported prerequisite-not-met because this fresh worktree lacked a full build - note check:i18n signals that with exit 1, NOT 3, while its own text says 'Nothing was checked', so it was read as NOT MEASURED rather than as a finding; after turbo run build over ./packages/* all three are green, quoted from their own verdict lines: 'check:dual-build-cjs-loads - 102 published require entry point(s) across 66 package(s) load'; 'check-i18n-bundles: OK (9 package(s) - all bundles in sync)'; 'check-type-check-coverage --re-measure: OK - 23 ledger entr(ies) re-measured, 771 raw tsc error(s) total, none above its recorded number'. ABLATION 1 (selector, source-resolved): mutation confirmed on disk - anchor count 1 to 0, injected marker present, blob hash e4be93e5 to 70e59faa - 6 of 11 new cases go RED; restore proven by hash equality with the HEAD blob, empty git diff HEAD, marker count 0. ABLATION 2 (replay arm, dist-resolved, both legs rebuilt): FIRST ATTEMPT WAS VOID and is reported as such - the marker was a // comment, which tsup strips, so ablation-dist-preflight found it only in sourcemaps and refused the reading; re-run with an executable string literal, preflight confirmed the marker in dist/index.js and dist/index.mjs, and the mutated driven boot then promoted NOBODY even after a successful sign-up (two no_authenticable_user passes, empty grant table). Restore leg: rebuild exit 0, preflight --absent green, whole-tree git status --porcelain empty. On the mutated leg pnpm build exits 1 at the DTS step only (TS narrowing complaint about the synthetic marker); the JS bundles built and carry the mutation, which is what the runtime measurement consumes. NOT MEASURED and declared: no repo-wide pnpm lint run (CI owns the farm).", "mcp_calls": "8 for the whole run - claim comment, pull_request_read get_files on #14352, create_pull_request, report comment, issue_read get_comments (report read-back), update_pull_request (patch round 1 body), pull_request_read get (body read-back plus mergeable_state), and this superseding report comment. The card body and all issue comments were read through the zero-quota public-repo payload channel, never MCP.", "open_questions": [], "out_of_scope_findings": [] }Patch round 1 — what changed and why option 2
Option 1 (repoint the probe at the predicate) was checked first and is not reachable without a new export: plugin-security's
isHumanUseris aconstdeclared inside thebootstrapPlatformAdminfunction body (bootstrap-platform-admin.ts:516; the function opens at:262), so it is neither module-scoped nor re-exported. A new export would flip Clause-② toyes, which the order names a stop condition — so option 2. Clause-② staysno, re-verified from the patched diff (git diff -U0 origin/main...HEAD | grep exportreturns nothing).Option 2 as landed, in
packages/plugins/plugin-auth/src/human-user-predicate-agreement.pin.test.tsonly — no other file underpackages/plugins/plugin-auth/**touched, and neither predicate edited:sys_accountfor every corpus row carrying a usableid, restoring the six id-bearing rows. The probe reads the human verdict indirectly asreport.adminPromoted, and promotion is now a conjunction (human AND holds a login), so an empty account table made every row fail the second conjunct and report a predicate disagreement that does not exist.ghost@example.testgets its own case, because no account can ever be keyed to it. It assertsisHumanUserRowstill answers human (on the owner side, so a regression there cannot hide behind this case), that promotion refuses it, and that the refusal comes from the authenticable filter (reason: no_authenticable_user) rather than an earlier branch. The docblock explains why this is not a predicate disagreement and cites plugin-security promotes the OLDEST human sys_user row, so an app that seeds a people directory grants platform admin to a row nobody can log in as #14348.The refusal is fail-closed on purpose: a row with no
idcannot hold an exercisable grant — the pre-fix code promoted it by writingsys_user_permission_set.user_id = undefined, a grant addressed to nobody in the table that says who may administer the platform. Same direction this file's ownNON_OBJECT_CORPUSgroup already fixed.Generated by Claude Code
Generated by Claude Code
os-sales commentedon Sep 2, 2026
ACCEPT — PR #14532 at head
3360e5f97(patch round 1 folded in); landing waits only for CI on this headReviewer of record:
domain:servicesPM seat,session_01AUF1NoViznQK32gqpK8wS8(os-sales). Verified against the tree and GitHub, not against the report:main,Fixes #14348on the first line (the merge closes this card — correct, the whole card lands here);mergeable_state: blocked= draft with checks pending,git merge-treeagainstorigin/mainfed4fa409clean.bootstrap-platform-admin.ts, two plugin-security tests (one new), the one plugin-auth pin file the seat added to the surface (14348#issuecomment-5508047359), and the changeset. Nothing else underpackages/plugins/plugin-auth/**; neither human predicate was edited. Nopackages/spec/**, nocontent/docs/releases/**.no, verified by the seat —git diff -U0 merge-base..3360e5f97 | grep exportis empty;shouldReplayBootstrapForkeeps its declaration and widens its body tosys_accountinserts (deliberate, measured as load-bearing). No new log site at any level inbootstrap-platform-admin.tsbeyond the reported singleinfoline for "humans present, none authenticable"; noerrorthrough a published sink.@objectstack/plugin-security(published)minorwith the behaviour change stated in words.minoris above the floor for a bug fix with no new export; accepted as conservative, not required.already_have_adminshort-circuit runs before target selection, pinned by the new suite; no existing deployment's admin moves.isHumanUseris function-local atbootstrap-platform-admin.ts:516); the pin file modelssys_accountfor id-bearing rows and asserts the fail-closed outcome for the id-less row with a docblock citing this card;22 passed (22)from8 failed / 14 passed.@objectstack/verifybootStackwith the three readings, replay measurement, two ablations (the first replay-arm attempt correctly reported VOID and re-run with an executable marker), 36 gates green +check-test-completenessNOT MEASURED (CI's), plugin-security 95/1785, plugin-auth 88/1816, verify 9/48.Landing: when every check on
3360e5f97is green (Lint & Repo Gates and TypeScript Type Check by job conclusion;Test Core (4/6)in particular, the shard that carried the pin), draft → ready → auto-merge → provenance comment. On MERGED: card auto-closes; strippm:dispatched. Follow-ups: none filed by the dev; the seat records that #14349 (the sibling decision card onbootstrap-statuscounting humans, not logins) is unchanged by this PR.Generated by Claude Code
fix(plugin-security): choose the platform-admin promotion target inst…