Skip to content

fix(service-settings,cli): pick the data key file that opens the stored secrets, or refuse in production - #30087

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-kj4zeo
Nov 4, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-kj4zeo

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread

Fixes #30042

维护者速读

  • 改了什么:两个数据密钥文件(共享的 $HOME/.objectstack/dev-crypto-key 与项目里旧 os start 留下的那份)不一致时,CLI 不再替子进程挑;serve 子进程在数据库打开后,用每把钥匙试开最新一条 sys_secret,用能打开的那把并说明选了哪个文件;两把都打不开时,生产启动拒绝启动并点名两个文件,开发启动打印启动错误。一次性命令(os migrate、os secret rewrap/orphans)走同一套解析和试开;OS_HOME 里开头的 ~ 现在按 os start 的方式展开。
  • 为什么改:fix(cli,service-settings): os dev and os start share one data-key file #30040 之后,只跑过 os start 的项目若机器上另有别的项目 os dev 生成的家目录密钥,会用错钥匙启动,所有已存密钥读出来都是"未设置",只留一行日志。这是下个版本会带出去的回归(p1)。
  • 风险与回滚:风险在生产启动多了一种拒启(两把钥匙都打不开已存密钥时),这正是 LocalCryptoProvider 的 fail-loud 约定;serve 的共享 crypto provider 改为首次使用时才构造。回滚:revert 本 PR 即回到 fix(cli,service-settings): os dev and os start share one data-key file #30040 的行为,不改任何密文、不重新封装。
  • 先看:packages/services/service-settings/src/local-crypto-provider.ts(chooseKeyFileByTrialOpen / settleKeyFileChannel / keyFilePath)、packages/services/service-settings/src/settings-service-plugin.ts(start() 里的选钥)、packages/cli/src/commands/serve.ts(共享 provider 延迟构造)。
  • 两分钟验证:pnpm --filter @objectstack/cli exec vitest run --project integration test/dev-start-shared-data-key.integration.test.ts,预期 6/6 通过:两把钥匙时 os start 读回密钥,两把都打不开时生产启动以 "Refusing to start in production" 退出并点名两个文件。
  • 需要你做的:无,席位落地。

Clause-②: yes (narrowing)
Metadata-protocol: none

The yes is the new @objectstack/service-settings exports; the (narrowing) arm is the boot refusal: a production os start / os serve whose two candidate key files both fail to open the newest stored secret now refuses to start, where it used to start and serve every stored secret as unset. No key of any schema, export or stored row changes shape, so Metadata-protocol: none and patch; the changeset carries the BREAKING banner, the remedy and not-required (no-migration-prescription).

What changes

  • The choice moves to where the database is open. applyCryptoKeyFileChannel (CLI parent) still resolves resolvePersistedKeyFile. On both-differ it no longer picks: it names the canonical file on OS_INTERNAL_CRYPTO_KEY_FILE and the legacy file on one new parent-owned channel, OS_INTERNAL_CRYPTO_KEY_FILE_CANDIDATE (deleted on every other answer, so an inherited copy never speaks).
  • SettingsServicePlugin.start() settles it (settleKeyFileChannel), in Phase 2: objectql has started (optional dependency), so the table is readable, and every consumer that decrypts does so at kernel:ready or later. storedSecretReaderOverEngine reads the newest sys_secret rows as the system and takes the first whose marker the provider knows; chooseKeyFileByTrialOpen opens it under each file's key (canonical first), once per member of CRYPTO_CONTEXT_SCOPES because sys_secret records no producer, and discards the plaintext. The winner is written back to the channel, the candidate is deleted, and the boot prints which file and why.
  • Postures, measured against the provider's fail-loud contract. Neither key opens it: production throws DataKeyOpensNothingError ("Refusing to start in production…", both paths, the remedy), which fails the plugin's start() and so the boot (rollbackOnFailure); development and test keep the canonical file and print a console.error naming both. No stored row: the canonical file, as today. A read that fails other than a missing table: the canonical file with a boot error naming the read (no verdict is invented from a failed read).
  • No provider is built before the choice. serve.ts builds the shared provider (datasource binder + secret fields) with deferredLocalCryptoProvider(), constructed at first use; the post-start secret-field wiring calls resolve() so the constructor's production refusal still fires at boot. A provider built while a candidate is still pending prints a warning naming both files (safety net, never silent).
  • One-shot commands. resolveExistingDataKey resolves the same file with the same legacy path (resolveStartHome + dev-crypto-key), sets the channels on its own env (it is its own parent), reads the project file when it alone holds a key, and on both-differ returns a deferred provider that the boot's settings service settles.
  • ~ in OS_HOME / OBJECTSTACK_HOME is expanded in keyFilePath exactly as resolveStartHome does.
  • The ciphertext format is unchanged and nothing is rewrapped. protocol.ts and engine.ts are untouched.

Tests

  • service-settings local-crypto-provider.test.ts (+21): the case-1 shape picks the legacy key; dev-then-start keeps canonical; every scope's row is judged; production refusal names both paths; development/test error notice; no row → canonical; failed read → not judged; one-file answers pass through untried; settle mutates the channel once for concurrent callers; refusal leaves both channels; provider-before-settle warning; deferred provider reads options at first use; engine reader (newest, unknown marker skipped, missing table → none, other failure rejects); ~ expansion.
  • service-settings settings-engine-bind-window.test.ts (+2, real LiteKernel): every kernel:ready reader sees the legacy file chosen; production with a key that opens nothing fails bootstrap() naming both files.
  • cli internal-crypto-key-file-channel.test.ts: both-differ hands the child both channels; case-1 and dev-then-start shapes read back after the child's settle; inherited candidate removed; ~ parity with resolveStartHome.
  • cli one-shot-settings.pin.test.ts (+3): legacy-only read by a one-shot; both-differ provider follows the trial; nothing minted.
  • cli test/dev-start-shared-data-key.integration.test.ts (+3, public door, real os start): the card's case 1 reads the secret back and names the chosen file; a production boot whose keys open nothing refuses naming both files. Budget re-measured (24.3 s for four boots → 40).

Acceptance notes

  • sys_secret row chosen for the trial: the newest by created_at with a known marker. A database mixing rows from both keys is judged by its newest row, the key the deployment sealed with last.
  • The datasource-admin block in serve.ts used to catch a provider-construction error and leave secrets undefined; with the deferred provider that error surfaces at first use instead, and production still refuses at the secret-field wiring as before.

Verification

All readings on head ec16e14d (the branch merged with main at that point).

  • Build: the touched closure, 63/63 tasks green. Typecheck: @objectstack/service-settings 0, @objectstack/cli 0.
  • Tests: service-settings 1151/1151 (67 files); cli unit 5365/5365 (382 files); cli integration dev-start-shared-data-key.integration.test.ts 6/6 (24.3 s of tests).
  • pnpm lint: exit 0.
  • node scripts/pm/dispatch-gates.mjs --commands: 66 derived gates, each run and its exit code recorded; --ran: 66 run, 0 NOT-MEASURED, 0 UNRUN. Three exited 3 on the first pass for environment prerequisites (the teardown self-test's pinned commit absent from the shallow clone; check:dual-build-cjs-loads and check:i18n-coverage before every dist existed). After fetching that commit and with the dists built they exited 0, and those are the codes recorded.
  • Ablations, each through scripts/ablation-replace.mjs with the dist rebuilt and preflighted, each restored to HEAD (blob equal, diff empty):
    • A, the trial forced to the canonical file: service-settings 12 red, cli 2 red.
    • B, the production refusal disabled: 3 red.
    • C, ~ expansion disabled in keyFilePath: service-settings 2 red, cli 2 red.
  • Full dogfood (narrowing arm), on this head: closure build @objectstack/dogfood^... exit 0, then pnpm --filter @objectstack/dogfood test exit 0: 417 files passed, 3 skipped (420); 3462 tests passed, 28 skipped (3490); 1695 s.

…ne that opens its stored secrets, or refuses

When the canonical key file and the project's legacy file hold different
keys, the process that opens the database trial-opens the newest stored
sys_secret ciphertext under each and reads the file whose key opens it.
Production refuses to boot when neither opens it; development prints a
boot error. One-shot commands resolve the same file the same way, and a
leading ~ in OS_HOME is expanded as the CLI home resolution does.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzhkoyLG9aL3MMfCgCW
…nt candidate channel, one-shot resolution and ~ expansion

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzhkoyLG9aL3MMfCgCW
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Nov 4, 2026
@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/service-settings, touching 23 documentable anchor(s).

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

  • content/docs/automation/webhooks.mdx (via sys_secret (literal, a string literal in storedSecretReaderOverEngine))
  • content/docs/data-modeling/drivers.mdx (via sys_secret (literal, a string literal in storedSecretReaderOverEngine))
  • content/docs/data-modeling/external-datasources.mdx (via sys_secret (literal, a string literal in storedSecretReaderOverEngine))
  • content/docs/data-modeling/objects.mdx (via sys_secret (literal, a string literal in storedSecretReaderOverEngine))
  • content/docs/data-modeling/odata-datasources.mdx (via sys_secret (literal, a string literal in storedSecretReaderOverEngine))
  • content/docs/data-modeling/validation-rules.mdx (via sys_secret (literal, a string literal in storedSecretReaderOverEngine))
  • content/docs/deployment/backup-restore.mdx (via sys_secret (literal, a string literal in storedSecretReaderOverEngine))
  • content/docs/deployment/cli.mdx (via sys_secret (literal, a string literal in storedSecretReaderOverEngine))
  • content/docs/deployment/environment-variables.mdx (via sys_secret (literal, a string literal in storedSecretReaderOverEngine))
  • content/docs/permissions/authorization.mdx (via sys_secret (literal, a string literal in storedSecretReaderOverEngine))
  • content/docs/protocol/kernel/config-resolution.mdx (via sys_secret (literal, a string literal in storedSecretReaderOverEngine))

⛔ 5 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx (via sys_secret (literal, a string literal in storedSecretReaderOverEngine))
  • content/docs/releases/v16.mdx (via sys_secret (literal, a string literal in storedSecretReaderOverEngine))
  • content/docs/releases/v17/17-0.mdx (via sys_secret (literal, a string literal in storedSecretReaderOverEngine))
  • content/docs/releases/v17/17-7.mdx (via sys_secret (literal, a string literal in storedSecretReaderOverEngine))
  • content/docs/releases/v18/18-0.mdx (via sys_secret (literal, a string literal in storedSecretReaderOverEngine))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 3 anchor(s) matched too much of the corpus to be a work list: created_at (literal, 43 pages), objectstack.config.ts (literal, 38 pages), os serve (command, 47 pages)
  • 10 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 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; 103 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 — 41 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 d79eecfe5b78598f9117726b542615ca2332d58f → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json d79eecfe5b78598f9117726b542615ca2332d58f

⚠️ 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 d79eecfe5b78598f9117726b542615ca2332d58f → 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

domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: ec16e14dd158e592c9e6653372cc18eb64770038
Local-runs: none
Scope: card #30042 and its comments (6628876432, 6629080567, 6631208465); the seat order; PR #30040's body, its ACCEPT 6628086274, and the pending #29934 changeset; the PR body and the full diff (12 files, +1110/−39).

Provider source at head: readCiphertext, ciphertextDerivationStatus, decrypt, resolveDataKey, loadExistingKey, resolvePersistedKeyFile. Wider reads:

  • settings-service-plugin.ts (init / start / kernel:ready), and serve.ts at both provider sites;
  • the start.ts / dev.ts call sites, start-home.ts, one-shot-settings.ts and its callers (secret rewrap, secret orphans, data-migration-plugins.ts, storage orphans);
  • sys-secret.object.ts, CRYPTO_CONTEXT_SCOPES, and the datasource secret binder's decrypt path.

Gates and rules read: AGENTS.md step 3, and check-changeset-no-major.mjs (read-only, with the PR body as the event). For check-adr-0087-registration.mjs, findMigrationPrescription / readDisposition were applied to the changeset. The Supersedes precedent .changeset/29872-…. Head check-runs, polled twice.

Face: a crypto-key boundary change. The CLI parent no longer rules between two data-key files. It hands the serve child both files on parent-owned OS_INTERNAL_* channels. The settings service then, in start() with the database open, trial-opens the newest known-marker sys_secret row under each key and points the channel at the key that opens it. A production boot where neither key opens it is refused. One-shot commands take the same resolution and trial. ~ in OS_HOME is expanded as resolveStartHome expands it. No ciphertext changes shape, and nothing is rewrapped.

① Derived judgments

  • The trial is correct.

    • keyOpensSample builds new LocalCryptoProvider({ key }) (explicit key: no file read, no mint) and calls the real decrypt, which dispatches on the derivation the ciphertext records (v1 or v2).
    • Rows whose marker reads unknown are skipped before any key is tried, so an unknown derivation cannot produce a "neither opens" verdict.
    • Each key is tried under every CRYPTO_CONTEXT_SCOPES member (pinned per scope).
    • The plaintext is discarded, decrypt errors are swallowed, and every notice names paths only.
    • The cost is at most 2 keys × 3 scopes AES-GCM opens of one row, once per boot, shared by concurrent callers.
  • The row read is sound. Rows are ordered by created_at desc, limit 10, and read as the system with bypassTenantAudit, the same option shape the secret store's own get uses. created_at is stamped unconditionally. A missing table counts as "no row". Any other read failure is not-judged (canonical plus a boot error), never a verdict.

  • One residual, within the ruling's "one stored ciphertext". Only the newest row is judged.

    • In a database with rows under both keys, the rows under the unchosen key still read as unset.
    • A corrupt newest row fails under both keys and, in production, refuses with a key-problem remedy.

    Neither of these selects the wrong key when the newest row is sound. Filed as a follow-up.

  • Every case shape is pinned.

  • Env keys still win, and the parent owns the channels. On OS_SECRET_KEY or a usable OS_DEV_CRYPTO_KEY, the parent deletes both channels (pinned with inherited values). The candidate channel is set only on both-differ, so an inherited candidate cannot steer a run. os serve run directly has no parent and gets canonical, as today. A provider built while a candidate is still pending warns and names the untried file.

  • The choice is made in the right place. SettingsServicePlugin.start() runs after objectql. The plugin's provider is built at kernel:ready, after start(). serve.ts uses deferredLocalCryptoProvider(), and a production no-key refusal still surfaces at boot through the existing rethrow.

  • The one-shot commands share the choice. resolveExistingDataKey computes the legacy path with start.ts's own predicate, sets both channels on its own process.env, builds the strict provider before setting the candidate (nothing minted, pinned), and on both-differ returns a deferred provider that the boot's settings plugin settles. secret rewrap, secret orphans and every buildDataMigrationPlugins({ storage: true }) caller go through it.

  • ~ expansion matches the CLI. expandHome follows resolveStartHome's rule byte for byte, pinned in both packages with parity tests.

  • Gates. check-changeset-no-major passes the level axis (yes (narrowing) · none · patch). The ADR-0087 disposition reads not-required (no-migration-prescription), with no prescription found. Check Changeset, PR-body guards, PM dispatch-gates, the Dogfood Regression Gate and Test UI are green at head. Test Core and the Type Check jobs were still in progress at review time.

② Semver level

patch on @objectstack/cli and @objectstack/service-settings, with Clause-②: yes (narrowing) and Metadata-protocol: none. This agrees with AGENTS.md step 3: a non-protocol breaking change ships patch and keeps its BREAKING banner and ADR-0087 disposition. The yes is honest. The six new runtime exports and four types (KEY_FILE_CANDIDATE_CHANNEL_ENV, chooseKeyFileByTrialOpen, settleKeyFileChannel, storedSecretReaderOverEngine, deferredLocalCryptoProvider, DataKeyOpensNothingError, and the types) are all named in the widening sentence and all exported from index.ts. none is honest too: no authorable key, value or type moves.

③ Boundary flags

  • Crypto-key custody moves from the parent's file comparison to a trial in the child, backed by evidence from the database. No key bytes or plaintext cross a log line or an error message.
  • The new OS_INTERNAL_CRYPTO_KEY_FILE_CANDIDATE channel is owned by the parent. It is not an operator setting, and it is not documented in environment-variables.mdx (checked).
  • The Supersedes paragraph is correct. fix(cli,service-settings): os dev and os start share one data-key file #30040's note is still pending, so both notes ship in one release, and the paragraph uses the "Supersedes, in the same release:" form the stock already uses (29872-…).
  • Should-fix, carried as a follow-up: the two opens notices claim the chosen file "opens this database's stored secrets". The trial judged one row, so "opens the newest stored secret" is the accurate wording.
  • Nits, carried: a stale catch comment in serve.ts (around 5344–5355), the service-settings README line on the file tier, and a JSDoc sentence noting that resolveExistingDataKey mutates the real process.env.

④ Narrowing face

Who hits it: a production os start / os serve boot whose canonical and legacy key files both hold different keys, and whose newest sys_secret row opens under neither key. Before, the boot started and served every stored secret as unset, with a log line. Now it refuses, naming both paths, the consequence and the remedy: restore the sealing key to the canonical path, or set OS_SECRET_KEY. Development boots still start on the canonical file, with a boot error. These are not narrowed: a single-file deployment, an env-key deployment, a database with no stored secret, and os serve run without a parent. Dogfood ran on the final head and is green in CI.

Implemented-by: claude/project-thread-kj4zeo
Reviewed-by: session_01WGufzibcN9oHa8kDbTTnL5

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5

席位核对

Verdict: ACCEPT at head ec16e14dd158e592c9e6653372cc18eb64770038 · 2026-11-04T13:10Z

  • Shape:
    • In the both-differ case, the boot trial-opens the newest known-marker sys_secret row under the canonical and the legacy key, in the serve child with the database open, and uses the key that opens it.
    • When neither key opens it, production refuses to boot (DataKeyOpensNothingError, naming both paths), and development keeps canonical with a boot error. No row keeps canonical.
    • One-shot commands share the resolution and the trial, and ~ in OS_HOME is expanded as resolveStartHome expands it.
  • Review: the CONTRACT_REVIEW_TIER review at this head is PASS (record above).
  • Premise measured:
    • Case 1 (a start-only project plus a foreign home key) now reads with the legacy key, pinned at unit level, in the kernel bind window and at the public door.
    • Ablations, each red and then restored: A, the trial forced to canonical (12+2 red); B, the production refusal turned off (3 red); C, ~ expansion turned off (2+2 red).
    • service-settings 1151, cli unit 5365, cli integration 6/6, closure build 63/63, typecheck 0, dogfood 417 files / 3462 tests, and 66 gates exit 0.
  • Declaration: Clause-②: yes (narrowing) (new exports beside the production boot refusal), Metadata-protocol: none, patch, BREAKING with remedy, adr-0087: not-required (no-migration-prescription), and a Supersedes paragraph for os dev and os start keep their crypto key in two different files, so a secret saved under npm run dev reads back as unset under os start on the same project database #29934's pending note. Reviewed as correct. The claim's provisional no is superseded by the measured declaration.
  • Accepted deviations: the harness-pinned branch claude/project-thread-kj4zeo. The seat files the follow-ups: trial robustness (judge the fetched page, not one row; report a mixed-key database), the notice wording ("opens the newest stored secret"), operator docs for the two-file rule, and the nits in the record.
  • Landing: this is a p1 release-blocker. On all-green at this head, the seat sends pr_ready + automerge_enable through the relay. It closes cli: a boot whose data key cannot open the database's sys_secret rows still serves every secret as unset — os start / os dev should pick the key that opens them, or refuse loudly; one-shot commands read only the canonical key file #30042.

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/xl tests tooling

Projects

None yet

1 participant