Repository navigation
feat(cli): os serve seals secrets with the crypto provider a plugin registers - #30993
objectstack-fleet[bot] wants to merge 6 commits into
Conversation
…sters `os serve` now reads a provider a plugin registered as the kernel service `crypto-provider` (CRYPTO_PROVIDER_SERVICE, @objectstack/types) during its init, and hands that one instance to the data engine, the datasource secret binder and the default settings service. Without a registration it keeps its own LocalCryptoProvider, from the same key source as before. Two declaring plugins refuse at boot, both named; a registration after init, or a replaced one, refuses too. Claude-Session: https://claude.ai/code/session_016WV4czs7cUFAphQEUFSrcz Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4czs7cUFAphQEUFSrcz Co-authored-by: Claude <noreply@anthropic.com>
…ators in the secret-rows double `refuseTwoCryptoProviderPlugins` iterated with the same text as the stack `plugins` loop that the #9863 audit-registration scan anchors on, so the scan could no longer name one place. Its loop now reads `candidate of composed`. The new test's in-memory `sys_secret` double matched by scalar equality and read a `$`-key as a field name; it now refuses a combinator or operator, as check:where-matcher requires. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4czs7cUFAphQEUFSrcz
📓 Docs Drift CheckThis PR changes 2 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 41 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 8663b53e486c38706c03591a0241f7b2b747757a && git checkout 8663b53e486c38706c03591a0241f7b2b747757a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c77dd17f2671f35e8f194ca1e63d8cc75bfb861a fbc8206285c9f7877adbe479cd7bb525a603efd3 && git checkout -B drift-repro c77dd17f2671f35e8f194ca1e63d8cc75bfb861a && git merge --no-ff fbc8206285c9f7877adbe479cd7bb525a603efd3
node scripts/docs-audit/affected-docs.mjs --json c77dd17f2671f35e8f194ca1e63d8cc75bfb861a
|
|
domain:server#1 · os-elon-musk · session_016WV4cytLmJxfAmgPVjUbbW Contract reviewServed-tier: Face: the Inputs read: card #30948 body; triage 6686459323; claim 6687208082 and its full re-post 6687449788 (file surface, ⛔ list, premise, ① Derived judgmentsScope and claim surface.
The registered provider is read after plugin init and before any secret write.
One instance to the three consumers.
The deferred-provider path.
Fallback only when none registered; production refusal unchanged on that path.
Two registrations refuse at boot, naming both.
No key material in snapshots or logs.
The settings shared-instance change (narrowing?).
Tests.
Must-fix.
Should-fix.
② Semver level
③ Boundary flags
Check-runs on the head (read at 16:17Z, API)60 check-runs: 23 success, 9 skipped, 2 failure, 26 in_progress. Red: Implemented-by: VERDICT: FAIL |
The host crypto provider test imports `serve.ts`, which reaches eight packages through function-body `import()` calls. check-test-source-alias's source-reach half (#30969) asks a changed test file to pay those first loads during collection, so the file now imports them for their side effects at module top, as the other serve-reaching cli tests do (#10126). serve.ts keeps its dynamic imports. Also notes in serve.ts that the host provider's init-done reading assumes `ctx.getKernel`. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4czs7cUFAphQEUFSrcz
|
domain:server#1 · os-elon-musk · session_016WV4cytLmJxfAmgPVjUbbW Contract review · deltaServed-tier: Delta over record 6689701608 (head
① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
|
domain:server#1 · os-elon-musk · session_016WV4cytLmJxfAmgPVjUbbW 席位核对 · ACCEPT · 2026-11-06T16:41Z · #30948 (serve uses a registered crypto provider)Head-sha: fbc8206
|
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 42258941523 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 分类: 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Requested by Elon · project thread
Fixes #30948
Clause-②: yes (widening)
Metadata-protocol: none
维护者速读
os serve现在用插件在初始化时注册的密钥加密提供方(服务名crypto-provider)封存全部机密,即对象的机密字段、数据源凭据和加密设置;没有插件注册时仍用原来的本地提供方。os serve启动收尾时总会建本地提供方并覆盖插件设好的那个,KMS 接不进来。packages/cli/src/commands/serve.ts的createHostCryptoProvider(选哪个提供方、何时拒绝),同文件启动后接线处的hostCrypto.provider.confirm(),再看测试packages/cli/src/commands/serve-host-crypto-provider.test.ts。pnpm --filter @objectstack/cli exec vitest run src/commands/serve-host-crypto-provider.test.ts,预期 16 条全部通过。What changes
A plugin supplies the host's secret crypto provider by registering an
ICryptoProvideras the kernel servicecrypto-providerwithctx.registerServicein itsinit(), and declaring it inprovidesServices. The name is exported asCRYPTO_PROVIDER_SERVICEfrom@objectstack/types, beside its precedentDATASOURCE_SECRET_RESOLVER_SERVICE, so objectstack-ai/cloud imports it instead of a literal. No spec entry:CoreServiceNameis the dispatcher's slot vocabulary, and the registered value's type is the existingICryptoProvidercontract, unchanged.os serve(packages/cli/src/commands/serve.ts):createHostCryptoProviderbuilds oneICryptoProviderthat delegates to the provider the boot chooses, plus a small host plugin (com.objectstack.cli.host-crypto-provider) that keeps the plugin context. The choice is made atkernel:ready, after everyinit(): the registered provider when there is one, otherwise the deferredLocalCryptoProviderserve already built. That one instance is handed to the data engine (setCryptoProvider, secret fields), the datasource secret binder (datasource credentials) and the defaultSettingsServicePlugin(encrypted settings, through its existingcryptoProvideroption).LocalCryptoProvider, keyed offOS_SECRET_KEYor the persisted key file as before; its production refusal for an unstable key still fires atkernel:ready, unchanged.LocalCryptoProvideritself is not touched. The one difference on this path: the default settings service now shares that one local instance instead of constructing its ownnew LocalCryptoProvider()from the same key source (the seat accepted this; a test pins that a value either one sealed opens under the other).runtime.start(),refuseTwoCryptoProviderPluginsrefuses a composition where more than one plugin (config-mounted or resolver-mounted, deduplicated by name) declaresprovidesServices: ['crypto-provider'], naming both. An undeclared secondregisterService('crypto-provider')is refused by the kernel itself (it names the service, not the plugins; recording the registrant inkernel.tsis out of scope per the claim).init()has run. A secret sealed or opened while plugins are still initializing is refused (HostCryptoProviderRefusal) rather than sealed by a provider the boot has not chosen. A credential written before the post-start wiring is sealed by the registered provider.hostCrypto.provider.confirm()refuses a provider registered after init (the boot had already chosen the local one) and a registered provider replaced after the choice. A value registered under the name that lacks the provider's methods refuses, naming the missing methods. Every refusal names the service and plugins, never a registered value. AHostCryptoProviderRefusalor the production key refusal stops the boot; any other wiring failure keeps the old warn-and-continue.Tests
packages/cli/src/commands/serve-host-crypto-provider.test.ts(new, 16 tests,integrationtier byvitest-tiers.ts), over a realObjectKernel, the real host plugin, a realObjectQLwith asecretfield, the realSettingsService, and a test KMS provider that wrapsLocalCryptoProviderwith its own key and records each scope it sealed:object_secret_field,datasource_credential,settings), and the local fallback is never built;kernel:readyfrom the same key source asnew LocalCryptoProvider(); an encrypted setting opens under aLocalCryptoProviderbuilt the original way and the other way round; a secret field sealed by it opens under the original provider;kernel:readyis sealed by the registered provider; a seal during init is refused in both plugin orders and records nothing; late registration and replacement refuse; a non-provider value refuses naming what it lacks;run(): one host provider over the deferred local one, its plugin mounted, the same provider handed to the binder, the default settings service and the engine, nonew LocalCryptoProvider(left, and the declaration check beforeruntime.start().Acceptance notes
os serve(one-shot-settings.ts,os secret rewrap,os secret orphans) build their ownLocalCryptoProviderand do not consult a registered provider. A KMS-keyed deployment needs them adopted separately.registerInstalledPlugins(installed packages) are not in the declaration check; the kernel's own refusal of a second registration still applies to them.SettingsServicePluginthe app composes itself (not the default capability) is not handed the host provider; it builds its own as before.CONTRACT_REVIEW_TIERreview named in the claim (the key-holder boundary).Verification
Premise on main 256a4b7, and the same boot on this branch (real os dev --fresh boot)
A probe app (outside the repo) mounts a plugin that registers a test provider as
crypto-providerininit()and also callssetCryptoProviderwith it instart(), then inserts a row into an object with asecretfield and prints which scopes its provider sealed.On
main256a4b7, the plugin's provider is overwritten:On this branch, the same probe:
(The engine holds the host provider, which delegates to the registered one: the plugin's provider sealed the write.) The same probe with the registration switched off keeps the original line
↪ secret fields: LocalCryptoProvider wired (dev)and its provider seals nothing.Tests
Ablation: removing the registry read turns the first test red
Via
scripts/ablation-replace.mjsonpackages/cli/src/commands/serve.ts(the test importsserve.tsfrom source):Gates
Generated by Claude Code