Repository navigation
Commit f6f663a
fix(service-settings,cli): pick the data key file that opens the stored secrets, or refuse in production (#30087)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} -->
_Requested by **Yibo Wang** · [project
thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwB6KCC4KfBqy33ZRD1NPFK95)_
Fixes #30042
## 维护者速读
- 改了什么:两个数据密钥文件(共享的 `$HOME/.objectstack/dev-crypto-key` 与项目里旧 `os start`
留下的那份)不一致时,CLI 不再替子进程挑;`serve` 子进程在数据库打开后,用每把钥匙试开最新一条
`sys_secret`,用能打开的那把并说明选了哪个文件;两把都打不开时,生产启动拒绝启动并点名两个文件,开发启动打印启动错误。一次性命令(`os
migrate`、`os secret rewrap/orphans`)走同一套解析和试开;`OS_HOME` 里开头的 `~` 现在按 `os
start` 的方式展开。
- 为什么改:#30040 之后,只跑过 `os start` 的项目若机器上另有别的项目 `os dev`
生成的家目录密钥,会用错钥匙启动,所有已存密钥读出来都是"未设置",只留一行日志。这是下个版本会带出去的回归(p1)。
- 风险与回滚:风险在生产启动多了一种拒启(两把钥匙都打不开已存密钥时),这正是 `LocalCryptoProvider` 的
fail-loud 约定;serve 的共享 crypto provider 改为首次使用时才构造。回滚:revert 本 PR 即回到
#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.
<!-- signed: domain:server#6 · os-support-ai ·
session_01WGufzhkoyLG9aL3MMfCgCW -->
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 3a27208 commit f6f663a
12 files changed
Lines changed: 1110 additions & 39 deletions
File tree
- .changeset
- packages
- cli
- src
- commands
- utils
- test
- services/service-settings/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5325 | 5325 | | |
5326 | 5326 | | |
5327 | 5327 | | |
5328 | | - | |
| 5328 | + | |
5329 | 5329 | | |
5330 | 5330 | | |
5331 | 5331 | | |
5332 | | - | |
5333 | | - | |
5334 | | - | |
| 5332 | + | |
| 5333 | + | |
| 5334 | + | |
| 5335 | + | |
| 5336 | + | |
| 5337 | + | |
| 5338 | + | |
5335 | 5339 | | |
5336 | 5340 | | |
5337 | 5341 | | |
| |||
5631 | 5635 | | |
5632 | 5636 | | |
5633 | 5637 | | |
| 5638 | + | |
| 5639 | + | |
| 5640 | + | |
| 5641 | + | |
| 5642 | + | |
5634 | 5643 | | |
5635 | 5644 | | |
5636 | 5645 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
14 | | - | |
| 14 | + | |
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
19 | | - | |
| 19 | + | |
20 | 20 | | |
| 21 | + | |
21 | 22 | | |
22 | 23 | | |
| 24 | + | |
23 | 25 | | |
24 | 26 | | |
25 | 27 | | |
| |||
102 | 104 | | |
103 | 105 | | |
104 | 106 | | |
105 | | - | |
| 107 | + | |
106 | 108 | | |
107 | | - | |
108 | | - | |
109 | | - | |
| 109 | + | |
110 | 110 | | |
111 | 111 | | |
112 | | - | |
113 | | - | |
114 | | - | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
115 | 143 | | |
116 | 144 | | |
117 | 145 | | |
| |||
131 | 159 | | |
132 | 160 | | |
133 | 161 | | |
134 | | - | |
| 162 | + | |
135 | 163 | | |
136 | 164 | | |
| 165 | + | |
137 | 166 | | |
138 | 167 | | |
139 | 168 | | |
140 | 169 | | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
22 | 22 | | |
23 | 23 | | |
24 | 24 | | |
25 | | - | |
26 | | - | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
27 | 31 | | |
28 | 32 | | |
29 | 33 | | |
| |||
46 | 50 | | |
47 | 51 | | |
48 | 52 | | |
49 | | - | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
50 | 56 | | |
51 | 57 | | |
52 | 58 | | |
| |||
57 | 63 | | |
58 | 64 | | |
59 | 65 | | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
60 | 69 | | |
61 | 70 | | |
62 | 71 | | |
| |||
68 | 77 | | |
69 | 78 | | |
70 | 79 | | |
71 | | - | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
72 | 86 | | |
73 | | - | |
74 | | - | |
75 | | - | |
| 87 | + | |
| 88 | + | |
76 | 89 | | |
77 | 90 | | |
78 | 91 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
37 | | - | |
38 | | - | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
39 | 40 | | |
40 | 41 | | |
41 | 42 | | |
42 | | - | |
| 43 | + | |
43 | 44 | | |
44 | 45 | | |
45 | 46 | | |
| |||
104 | 105 | | |
105 | 106 | | |
106 | 107 | | |
| 108 | + | |
107 | 109 | | |
108 | 110 | | |
109 | 111 | | |
| |||
202 | 204 | | |
203 | 205 | | |
204 | 206 | | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
0 commit comments