Skip to content

fix(cli): a one-shot command never mints a data key in the key home (#21471) - #21497

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-21471-report-never-mints-key
Oct 3, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-21471-report-never-mints-key

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21471

Clause-②: no

What changed

os secret orphans is a report that promises to write nothing. It composed the settings service with no crypto provider, so the service built its default one. In a development posture with no env key and no key file, that default creates a key file in the key home. The database stayed untouched, but the key home did not, and the next development-posture process on that host adopted the minted key. The storage arm of the data-migration plugins (os storage orphans, also report-only, and os migrate files-to-references) composed the service the same way.

  • One composition. New packages/cli/src/utils/one-shot-settings.ts holds the idiom os secret rewrap already used, moved rather than copied:
    • resolveExistingDataKey() builds the provider over a key that already exists, in the strict posture with the auto-key opt-in withheld, so it never mints;
    • refusingCryptoProvider() refuses every call and names why;
    • oneShotSettingsPlugin() hands the settings service one of those two, never the default.
  • secret/orphans.ts and utils/data-migration-plugins.ts compose through it. secret/rewrap.ts moves onto it, so there is one spelling, not two.
  • The two driver-contract tests boot the commands' own composition again. Their "the command's own list" comments were stale for rewrap since its provider change.
  • Changeset: @objectstack/cli patch (.changeset/21471-one-shot-never-mints-key.md).

Census: every CLI composition of the settings service, by any spelling

How the census was built:

  • every non-test module under packages/cli/src whose comment-masked code names SettingsServicePlugin, LocalCryptoProvider or its deprecated alias (the enumeration pin's own detector; at da6acc015d it named exactly the four source composers below plus the new helper);
  • the commands that reach those modules through a call;
  • the one composition a command reaches inside another package.
Member Reaches the settings service through Disposition Reading
os secret orphans (report and --delete) commands/secret/orphans.ts closed here Composed the default. The key file appeared in both modes, red at da6acc015d, green at bda27b5073
os storage orphans storage arm of utils/data-migration-plugins.ts closed here Report-only. Red at da6acc015d, green at bda27b5073
os migrate files-to-references (dry run and --apply) the same storage arm closed here Red at da6acc015d, green at bda27b5073. It needs the real key when one exists, because the storage plugin reads its stored credentials through the settings service. That is why the composition reads an existing key rather than always refusing
os secret rewrap commands/secret/rewrap.ts already correct; moved onto the helper Green at both commits
os serve, and os dev / os start, which spawn it the capability table's settings row; two default providers for secret fields not affected This is the long-lived host. Persisting a key in a development posture so restarts reuse it is its documented behaviour, and a production posture refuses without a key. It is the one host the enumeration pin allows, with that reason
os migrate plan / os migrate apply (composeHostStack) only a host config's own plugins not affected Host plugins are composed for declarations only, with start() suppressed. The settings plugin builds its default from a hook registered in start(). Both are green in the family pin
the other bootSchemaStack callers (meta resync, migrate account-issuer / audit-metadata-bodies / duplicates / meta --stored / multi-value-columns / recorded-by / resume / summary-nulls / value-shapes) none not affected They compose no settings service. Every mode is green in the family pin
os verify @objectstack/verify's boot harness, in another package affected, not closed here See Out of scope below

Pins

  • src/utils/one-shot-settings.pin.test.ts (unit tier)
    • The enumeration: every module whose code names the plugin or the provider is the helper or commands/serve.ts, failing by file name. A self-check confirms the detector ignores prose and sees a renamed destructure, the capability-table string and the alias.
    • The helper's contract in a development posture with an empty key home. Control: the default provider mints there.
    • Cases: no key means none is resolved, none is minted, and the service gets a refusing provider; an existing key file is read and never rewritten; an env key is used and the home is untouched; a set-but-unusable key is an answer, not a throw; the refusing provider refuses all five contract members.
  • src/utils/schema-migrate.one-shot-family.integration.test.ts (integration tier, by its existing bootSchemaStack import). It gains a third promise across the source-derived family: in a development posture with an empty key home, every mode of every bootSchemaStack caller leaves the home empty.
    • Positive control: the read-only boot with new SettingsServicePlugin({ registerRoutes: false }), the composition the report used to pass, leaves exactly one key file there.
    • A new caller is already forced into the table by the file's first case.
    • New cases ran at about 0.1 to 0.2 s each locally under the file's existing 120 s per-case timeout.

Verification (head bda27b5073; red leg at da6acc015d)

  • Red first. The pins were committed before the fix (da6acc015d) and run against the unfixed commands:
    • the enumeration pin failed, naming commands/secret/orphans.ts, commands/secret/rewrap.ts and utils/data-migration-plugins.ts (7 of 8 tests passed, the control included);
    • the family key-home cases failed 5 and passed 23, the control included. The 5 failures were migrate files-to-references, secret orphans, storage orphans, migrate files-to-references --apply and secret orphans --delete, each as "key material was created in the key home", with the key file present.
  • Green at bda27b5073.
    • vitest run src/utils/one-shot-settings.pin.test.ts: 8 / 8 passed.
    • vitest run --project integration over the family file and both driver-contract files: 3 files, 85 / 85 passed.
    • The other tests that reach the changed modules: unit, 8 files, 61 passed; integration (orphans.guards, rewrap.guards, summary-nulls, sys-secret-rewrap), 4 files, 37 passed.
    • pnpm --filter @objectstack/cli typecheck (tsc plus check:test-typecheck): exit 0, with no new test-typecheck debt.
  • Public door. The built CLI's os secret orphans --json, in a development posture with an empty key home, left the home empty.
  • pnpm lint (eslint . --no-inline-config, the whole repo): exit 0 at bda27b5073, not narrowed.
  • Gates. dispatch-gates.mjs --ran with no paths: 64 derived, 64 run, every one exit 0, and 0 NOT MEASURED (a derived zero, from recorded exit codes). Four gates first refused on a missing build (exit 3, nothing measured). After the prerequisite builds they were re-run to exit 0: check:dual-build-cjs-loads, check:i18n, check:i18n-coverage and check:i18n-walk-parity.
  • Not merged with origin/main. It is three commits ahead, and none of them touches these files. The derivation's stale-tree note names scripts/engine-double-contract.pinned.json, which this diff does not touch.

Acceptance notes

  • Visible difference. On a host whose key lives only in the key file, these commands now print the strict posture's one-line note on stderr, naming the persisted key's location, as os secret rewrap already did. stdout and --json are unchanged.
  • Unreadable stored values. With no key, a stored settings value that cannot be opened reads as null with a warning. A freshly minted key produced the same, because it can open nothing stored.
  • The enumeration pin's reach is packages/cli/src. A composition inside another package that a command calls into names nothing there; the pin header says so, and the census lists the one that exists.

Out of scope (reported to the seat, not filed here)

  • os verify reaches @objectstack/verify's boot harness. The harness composes the settings plugin with no provider and also sets a default local provider on the engine for secret fields.
    • Measured through the built CLI on examples/app-todo, in a development posture with an empty key home: after the run, the key home held a key file.
    • It is not closed here for two reasons. The composer is outside packages/cli. And it needs a different provider shape: the harness seals and opens secret fields against an in-memory database, so a read-or-refuse provider would break os verify on a keyless host where an ephemeral in-process key would not.

Generated by Claude Code

claude added 2 commits October 2, 2026 23:53
…the fix

Adds the one-shot settings composition (utils/one-shot-settings.ts) and the
two pins that hold it: the enumeration of every CLI module that names the
settings plugin or the local crypto provider, and, across every
bootSchemaStack caller and mode, an empty key home in a development posture
staying empty, with the default composition minting there as the control.

The commands still compose the default here, so the pins are red on this
commit by design; the next commit moves the commands onto the helper.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
`os secret orphans` and the storage arm of the data-migration plugins
(`os storage orphans`, `os migrate files-to-references`) composed the
settings service with no crypto provider, so it built its default one, which
in a development posture with no key writes a key file into the key home.

Every one of them now composes the service through utils/one-shot-settings.ts:
the provider over a key that already exists, in the strict posture with the
auto-key opt-in withheld, or one that refuses every call. `os secret rewrap`
moves onto the same helper, so there is one spelling. The two driver-contract
tests boot the commands' own composition again.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
@github-actions github-actions Bot added the size/m label Oct 3, 2026
@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 9 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/cli.mdx (via os secret orphans (command, read off packages/cli/src/commands/secret/orphans.ts), os secret rewrap (command, read off packages/cli/src/commands/secret/rewrap.ts))
What this run could not see
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 27 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 9ff74285f14b7b60699546211e53e9ccc13c0d61 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from bb30c025cd2dcd8706dad18f98c382c876137609 — the merge of head bda27b507349625ebc60538ceae017d8a1f7064e into base 9ff74285f14b7b60699546211e53e9ccc13c0d61, 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 bb30c025cd2dcd8706dad18f98c382c876137609 && git checkout bb30c025cd2dcd8706dad18f98c382c876137609
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9ff74285f14b7b60699546211e53e9ccc13c0d61 bda27b507349625ebc60538ceae017d8a1f7064e && git checkout -B drift-repro 9ff74285f14b7b60699546211e53e9ccc13c0d61 && git merge --no-ff bda27b507349625ebc60538ceae017d8a1f7064e

node scripts/docs-audit/affected-docs.mjs --json 9ff74285f14b7b60699546211e53e9ccc13c0d61

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 9ff74285f14b7b60699546211e53e9ccc13c0d61 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Landing note: one commit status is pending, and it is not a check run · domain:cli#1 · session_016GiHYRmLSNWTfbX9gVQkpz · read 2026-10-03T01:02Z

On head bda27b5073:

This diff touches no deployed app, so the seat readies and arms this PR and does not wait on the stuck vendor status. The merge queue runs its own merge_group CI.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 3, 2026 01:03
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 3, 2026 01:03
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 3, 2026
Merged via the queue into main with commit 25797a1 Oct 3, 2026
35 of 36 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21471-report-never-mints-key branch October 3, 2026 01:26
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/m tests tooling

Projects

None yet

2 participants