Skip to content

fix(plugin-security): the platform-admin promotion WITHHELD warning says what 18.0 supports for a v17 database - #24718

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-24709-platform-admin-withheld-18-text
Oct 20, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-24709-platform-admin-withheld-18-text

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #24709

Clause-②: no

What this changes

The boot warning "[security] platform-admin promotion WITHHELD: an unscoped grant exists whose permission-set name (sys_user_permission_set.permission_set) was never written" ended by sending the operator to the organization-ownership migration: "The organization-ownership migration's id-to-name rewrite (ADR-0131 D10) names it; the next pass after that reads it by name." 18.0 refuses that migration on every database, so an operator booting a v17-shaped database with an unnamed grant was pointed at a refusal.

The warning now ends with the sentence the boot refusal and the two sibling release-text rows already carry (PR #24635 and PR #24679 landed it): "18.0 supports fresh installs only and does not upgrade a v17 database: keep a v17 database on a 17.x release until the 18.x release that carries the upgrade path."

Text only. The withholding does not move: findPlatformAdminGrantHolder, the unnamedHolder guard, the returned adminPromoted: false / admin_grant_unnamed result, the logger.warn / logger.info fallback and every other clause of the warning (what is withheld, why, "this pass will not mint a second one") are unchanged.

Files: packages/plugins/plugin-security/src/bootstrap-platform-admin.ts (the string only, 2 lines), packages/plugins/plugin-security/src/grant-readers-unnamed-grant.test.ts (one new case), .changeset/24709-platform-admin-withheld-18-text.md (@objectstack/plugin-security patch).

Pin

One case in grant-readers-unnamed-grant.test.ts, beside the existing unnamed-holder cases, which already build the state (an unscoped admin_full_access grant written with a NULL name, an older account the age rule would otherwise promote). It runs bootstrapPlatformAdmin with a capturing logger and asserts:

  • the pass took the unnamed-holder branch (adminPromoted: false, reason: 'admin_grant_unnamed') and logged the WITHHELD warning exactly once (anti-vacuity);
  • the whole warning text equals the kept clauses followed by the 18.0 sentence, and ends with it;
  • the text names none of organization-ownership, os migrate, --out, --apply, --plan-file, ceremony, id-to-name.

Evidence (head 7357dcca56)

  • Closure build: pnpm --workspace-concurrency=2 --filter '@objectstack/plugin-security^...' build, under the verify lock, VERDICT command-exit 0.
  • Pin file alone: pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2 src/grant-readers-unnamed-grant.test.ts is 8 passed (7 existing plus the new case), VERDICT command-exit 0, run again after the ablation was restored: 8 passed.
  • Typecheck: pnpm --filter @objectstack/plugin-security typecheck exit 0, including check:test-typecheck (the test layer compiles, 0 errors).
  • Ablation (the diff changes a test-pinned string): the old clause put back through scripts/ablation-replace.mjs (anchor hit once, blob eac7d8f87baa to f9123b5ffce1 on disk). The test imports ./bootstrap-platform-admin.js from src, so no rebuild leg applies. Result: 1 failed, 7 passed, the new case red with the old clause as the received text, the expected direction (red). Restore proven on disk: blob equals the HEAD blob and git diff HEAD is empty.
  • Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 68 commands; each was run with its exit captured before any pipe, then --ran reconciled: 68 derived, 67 run (all exit 0), 1 NOT-MEASURED, 0 UNRUN. pnpm check:i18n first exited 3 (prerequisite not met), was re-run after building its stated closure, and exited 0.
  • NOT MEASURED: pnpm check:dual-build-cjs-loads, reason: it reads the built output of every package and exits 3 (prerequisite not met) without a whole-workspace build, which CI's Build Core owns; this diff changes no export, package.json or build config.
  • Left to CI: the full plugin-security suite, test:repo, the type-check lanes, and the lint workflow's repo-wide gates.

Acceptance notes


Generated by Claude Code

…ays what 18.0 supports for a v17 database

The warning ended by pointing at the organization-ownership migration's
id-to-name rewrite, which 18.0 refuses on every database. It now ends with
the sentence the boot refusal carries. The withholding, the returned result
and the log level are unchanged; a pin beside the unnamed-holder cases holds
the text.

Claude-Session: https://claude.ai/code/session_011TdZ8GjixNZFLz1xJD9zzK
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation tests tooling labels Oct 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 15 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 54159fd742e5a79fc10ac41854dba3395e1a1ffe → packageMentionDocs.

Which tree this was computed on

This run read content/docs from def634d102bf9b788b36392fd8370fd06d01a86c — the merge of head 7357dcca56269ee4e0d9ca3c860c8cc7acde2ab4 into base 54159fd742e5a79fc10ac41854dba3395e1a1ffe, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin def634d102bf9b788b36392fd8370fd06d01a86c && git checkout def634d102bf9b788b36392fd8370fd06d01a86c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 54159fd742e5a79fc10ac41854dba3395e1a1ffe 7357dcca56269ee4e0d9ca3c860c8cc7acde2ab4 && git checkout -B drift-repro 54159fd742e5a79fc10ac41854dba3395e1a1ffe && git merge --no-ff 7357dcca56269ee4e0d9ca3c860c8cc7acde2ab4

node scripts/docs-audit/affected-docs.mjs --json 54159fd742e5a79fc10ac41854dba3395e1a1ffe

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · domain:services seat 1 · 2026-10-20T02:01Z

os-justin · session_011TdZ8GjixNZFLz1xJD9zzK (seat post #6021). Reviewed against GitHub and origin/main 54159fd742, not against the report. Head under review:

7357dcca56
  1. Shape. Draft, base main, first line Fixes #24709, line-start Clause-②: no in the body and the changeset. 18.0 release text still sends operators to os migrate organization-ownership --out / --apply --plan-file, which 18.0 refuses on every database (family close-out: spec upgrade-guide entries, runtime boot refusals, plugin-security report, docs) #24637 and feat(cli,runtime)!: the upgrade ceremony opens — v17 and 18.0 databases take it, and a server serves only a database it created or one the ceremony certified (18.x steps 5 and 6) #24681 appear only as "remains open", with no keyword beside them. Changeset: @objectstack/plugin-security patch. Each sentence was read against the diff: two lines of the withheld string change, and the withholding, the admin_grant_unnamed result and the warn / info fallback do not. Not governed, 52 changed lines.
  2. Premise, measured by the seat. At 73d4cc7391 the string sits at bootstrap-platform-admin.ts:880–884 under if (!walled && unnamedHolder), and no test read it. The new sentence equals the card's quotation byte for byte, and the same text closes the sibling row PR fix(plugin-security): the boot report's unnamed-grant line says what 18.0 supports for a v17 database #24679 merged (catalog-reference-report.ts).
  3. Gates. At ACCEPT, these check-runs on this head are success: Build Core, Check Changeset, Governed Surface Queue Guard, Type Check · source gates, Type Check · debt ledger, Lint · PM dispatch-gates, Lint · scoped · tooling self-tests, Dogfood Regression Gate, and the claim and closing-keyword guards. Test Core and the remaining lint and type-check lanes are in_progress; none has failed. Locally, 67 of 68 derived families ran and exited 0. check:dual-build-cjs-loads is NOT MEASURED locally (it needs every dist), and CI answers it.
  4. Accepted deviations.
    • The pin sits in the existing grant-readers-unnamed-grant.test.ts, which already builds the unnamed-holder state. It asserts the whole text and that the warning is logged once. One ablation went red, and the restore was proven by blob hash.
    • main moved by five commits and was not merged in; none touches plugin-security. The landing re-checks this by trial merge.
    • Model-free commit trailers per AGENTS.md.
  5. Correction to the PR body's Acceptance notes, item 1 (the dev flagged it; the dev does not edit the body, and this record is the correction). The comment above the unnamedHolder guard names no migration. Three developer comments still describe the ceremony's rewrite in the future tense, not four: the "By name, and the grant that names nothing" section of bootstrap-platform-admin.ts, the header of grant-readers-unnamed-grant.test.ts, and a comment in cleanup-package-permissions.ts.
  6. Out-of-scope dispositions. Those three comments are developer-facing, with no operator reach: Acceptance notes, not filed. The seat records them on its seat post's list of stale code comments.
  7. Landing clause. No contract review is owed: it declares no widening and no narrowing, touches no published schema and no governed text. Ready + auto-merge through the relay once every check on the current head is green or an expected skip, and a fresh trial merge onto main is clean. A push resets this reading.

Generated by Claude Code

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

Labels

documentation Improvements or additions to documentation size/s tests tooling

Projects

None yet

2 participants