Repository navigation
Commit 4cd48e2
fix(cli)!: the one-shot secret commands choose the crypto provider from the installed plugins too, by os serve's rule (#31506)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1X1zG3Xn4JonaKvDLLr5gnjL)_
Refs #28284
Clause-②: yes (narrowing) — `os secret rewrap`, `os secret orphans` and
the data migrations' settings arm refuse to run when a plugin `os plugin
install` installed is refused and nothing rules out that it provides the
crypto provider (`crypto-provider`), and when a config plugin and an
installed plugin both declare it, where they ran without reading the
installed plugins; widened beside it: the optional
`leaveUninstallsPending` on `LoadInstalledPluginsInput`
(`@objectstack/runtime`).
Metadata-protocol: none
<!-- adr-0087: not-required (no-migration-prescription) no key of any
schema is added, removed or re-shaped and no stored row is converted: a
one-shot run whose installed plugins hold a refusal that may be the
crypto provider stops until the operator fixes that plugin or disables
it, which is a choice about one installed plugin made by hand, not a
mechanical conversion -->
**BREAKING** for a deployment that runs `os secret rewrap`, `os secret
orphans` or a data migration with an installed plugin (`os plugin
install`) that `os serve` would refuse to boot with, or with a crypto
provider declared both in the host config's `plugins` and by an
installed plugin.
## 维护者速读
- **改了什么**:一次性命令(`os secret rewrap`、`os secret orphans`、数据迁移的 settings
部分)选择加密提供者时,现在除了宿主配置的 `plugins`,也读取 `os plugin install` 安装的插件,走 `os
serve` 启动时读取它们的同一个加载器,并按 `os serve` 的同一规则判断。
- 已加载且声明 `crypto-provider` 的已安装插件,就是本次运行加解密所用的插件。
- 被拒且无法排除是加密提供者的已安装插件,会让本次运行拒绝执行。
- 两处同时声明加密提供者,也拒绝执行。
- 本次运行不注册、也不移除任何已安装插件;待完成的卸载留给下次启动。
- **为什么改**:QA 第三轮发现的安全加固(细节按披露裁决不公开)
- **风险与回滚**:改 `@objectstack/cli` 和 `@objectstack/runtime`(均为 patch,声明为
`Clause-②: yes (narrowing)`)。
- 没有安装记录的项目,行为不变。
- runtime 的 `loadInstalledPlugins` 新增一个可选输入 `leaveUninstallsPending`。
- 回滚即 revert 本 PR。
- **先看**:`packages/cli/src/utils/one-shot-settings.ts` 的
`readDeclaredCryptoProvider` 和新的
`readInstalledPlugins`;`packages/cli/src/utils/installed-plugins-boot.ts`
的 `loadHostInstalledPlugins`。
- **两分钟验证**:`pnpm --filter @objectstack/cli exec vitest run
src/utils/one-shot-crypto-installed-plugins.test.ts`,预期 11/11。
- **需要你做的**:无,席位做 contract review 后落地。
## What changes
- **Before.** A one-shot run (`oneShotCrypto`) chose the crypto provider
from the host config's `plugins` only. The installed plugins of the
project were not read.
- **Now.** `readDeclaredCryptoProvider` reads the installed plugins
beside the config's `plugins`, and judges them by `os serve`'s rule.
- **The read.** It goes through the new `loadHostInstalledPlugins` (cli
`utils/installed-plugins-boot.ts`). That is the loader call
`registerInstalledPlugins` already made, split out so `os serve` and the
one-shot run read the installed set one way: the trusted keys, the
bundle wrap, host package resolution, and a loader failure as one
`loader_failed` refusal. `registerInstalledPlugins` now calls it,
unchanged otherwise.
- **An installed plugin that loads and declares `crypto-provider`** is
the declaring plugin, composed into the run's boot exactly as a config
one is.
- **A refused installed plugin** goes through `serve`'s
`refuseRefusedInstalledCryptoProviderPlugins`: the same rule-outs, the
same refusal and the same per-code step.
- **Two declarations**, config or installed, are refused by
`refuseTwoCryptoProviderPlugins`, both named.
- **A name already taken:** an installed plugin whose name a config
plugin, or an installed plugin read before it, takes is refused as
`plugin_name_taken`, and one whose name cannot be read as
`kernel_refused` (with the thrown reason), as `os serve` refuses them.
- **A signed installed provider** boots: the wrapper that names the
provider when its `init` fails carries the artifact verdict its host
recorded for the plugin.
- **The refusal texts** say an installed provider was installed by `os
plugin install`, and a config one that it is declared in the host
config.
- **With no host config**, the installed plugins are still read, as `os
serve` reads them.
- **Nothing is registered or removed.** The run passes
`leaveUninstallsPending: true`.
- **`@objectstack/runtime`:**
`LoadInstalledPluginsInput.leaveUninstallsPending?`. When it is set, a
pending uninstall is neither finished nor loaded, as the next boot that
finishes it would not load it either. When it is unset, `os serve`'s
boot is unchanged.
## Why
QA 第三轮发现的安全加固(细节按披露裁决不公开)
## Verification
Readings on head `9f07d719c` (two commits on `main` at `a61ddc93a`; the
second answers review round 1).
<details><summary>Pins, red-first and restore</summary>
- New `packages/cli/src/utils/one-shot-crypto-installed-plugins.test.ts`
(11). It stands in for `loadHostInstalledPlugins`, whose verification is
pinned in the runtime suite and end to end in
`test/serve-installed-plugins.integration.test.ts`. It boots each
composition in a real `ObjectKernel`.
- With no host config, and again beside a config that declares none, an
installed plugin that loads and declares the provider is the one the run
seals and opens with. The local key is never resolved, even with
`OS_SECRET_KEY` set.
- Refused, each naming the plugin, its code and the step:
- a refused installed plugin (`tree_changed`);
- an imported refused plugin that declares the provider;
- `loader_failed`.
- CONTROL: a ruled-out refusal (`runtime_not_dispatched`, or a
`kernel_refused` plugin that declares other services), a loaded plugin
that declares none, and no installations file each leave the local
provider over the existing key.
- Refused before either initializes: a config plugin and an installed
plugin that both declare it, both named.
- Refused as `plugin_name_taken`: an installed declaring plugin whose
name a config plugin takes; and one whose name an installed plugin read
before it takes (that first plugin is not named).
- A signed installed provider, with the verdict recorded on it as the
loader records it, boots and seals with its provider.
- An installed provider that registers none, and one whose `init`
throws, are each refused with "as a plugin installed by `os plugin
install`" and never "host config".
- The loader is asked with `{ leaveUninstallsPending: true }`.
- `installed-plugin-loader.test.ts` (runtime, 1 new case): with
`leaveUninstallsPending`, a pending uninstall keeps its directory and
record and is not loaded, while the next plugin loads. CONTROL: the
default read finishes it.
- Red-first with `main`'s blobs:
- `one-shot-settings.ts`: the new file read 7 failed | 1 passed (8). The
control case was the one that passed.
- `installed-plugin-loader.ts`: the loader file read 1 failed | 65
passed (66).
- Round 1, `one-shot-settings.ts` at `f89f99cd0`'s blob: the new file
read 3 failed | 8 passed (11). The three new cases failed.
- Restored, on this head: 11/11 and 66/66. The five cli files around it
(the new file, `one-shot-crypto-provider`, `one-shot-settings.pin`,
`installed-plugins-boot`, `serve-host-crypto-provider`) read 74/74.
`one-shot-crypto-provider.test.ts` and `installed-plugins-boot.test.ts`
are unchanged and green (24/24 across the three cli files).
</details>
<details><summary>Package suites and typecheck</summary>
- `tsc --noEmit` in `packages/cli` and `packages/runtime`: exit 0 (cli
again on this head).
- Under the verify lock, on `f89f99cd0` and before the rebase onto
`a61ddc93a`, which touched no file of this diff:
- `pnpm --filter @objectstack/runtime test`: Test Files 560 passed
(560), Tests 9001 passed | 19 skipped, `VERDICT command-exit 0`.
- `pnpm --filter @objectstack/cli test` (both tiers, so the one-shot
family integration and the `os secret` driver contracts too): Test Files
612 passed | 1 skipped (613), Tests 7591 passed | 9 skipped, `VERDICT
command-exit 0`.
- `eslint --no-inline-config` on the changed source and test files: exit
0.
- `check-adr-0087-registration`: `[BREAKING+bang+clause-②-narrowing]
not-required (no-migration-prescription)`.
</details>
<details><summary>Derived gates</summary>
- `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands` derives 66 families on this diff, the same list on both
heads. They ran again on this head after a cli build.
- Every family ran and exited 0. `check:type-check-debt` ran under the
verify lock.
- `--ran` reconciliation: 66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN.
</details>
Not measured:
- A one-shot command end to end over a real signed installed provider
plugin. The installed set's verification and import are the loader `os
serve` uses, which is pinned on its own, and the choice over it, signed
stub included, is pinned here with a stand-in loader.
- The package suites on this head: the round-1 delta touches
`one-shot-settings.ts`, its new test file and the changeset only.
- A name an installed plugin shares with a plugin `os serve`
auto-registers (not one of the host config's). `os serve` refuses it as
`plugin_name_taken`, but the one-shot run does not compose those
plugins, so it compares against the host config's names and the
installed plugins read before it only.
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(No `packages/spec` change.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `eslint --no-inline-config` on the changed
files: exit 0.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: not touched.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English, except the maintainer
summary the lane's PRs carry.
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FEkQ5mgePB9NQ4PNhvYVxk)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 6a70821 commit 4cd48e2
6 files changed
Lines changed: 499 additions & 27 deletions
File tree
- .changeset
- packages
- cli/src/utils
- runtime/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 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
| 10 | + | |
10 | 11 | | |
11 | 12 | | |
12 | 13 | | |
| |||
75 | 76 | | |
76 | 77 | | |
77 | 78 | | |
78 | | - | |
79 | | - | |
80 | | - | |
81 | | - | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
82 | 90 | | |
83 | 91 | | |
84 | | - | |
85 | | - | |
86 | 92 | | |
87 | | - | |
| 93 | + | |
88 | 94 | | |
| 95 | + | |
89 | 96 | | |
90 | 97 | | |
91 | 98 | | |
| |||
99 | 106 | | |
100 | 107 | | |
101 | 108 | | |
102 | | - | |
| 109 | + | |
103 | 110 | | |
104 | 111 | | |
105 | 112 | | |
106 | 113 | | |
107 | 114 | | |
108 | 115 | | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
109 | 124 | | |
110 | 125 | | |
111 | 126 | | |
| |||
0 commit comments