Skip to content

fix(cli): os serve names an unloaded devPlugins entry by the list it was written in - #31356

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-fayfxc
Nov 8, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-fayfxc

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Requested by Leehom · project thread

Refs #28284

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:os serve --dev 因可能提供加密提供者的项没能加载而拒绝启动时,devPlugins 里的项现在点名为 devPlugins[i],不再按它排在 plugins 之后的位置点名。判断是否拒绝的 providesServices 读取改为与 cryptoProviderPlugins 共用同一个判定,docstring 也写明了「声明其它服务或什么都没声明的项被排除」。
  • 为什么改:fix(cli): os serve refuses to boot when a plugins entry that may provide the crypto provider does not load #31263 契约复审的 should-fix 2 与 3(should-fix 1 的 docstring 部分)。
  • 风险与回滚:只改 @objectstack/cli(patch)。哪些项会被拒绝不变,只改报错里的名称。回滚即 revert 本 PR。
  • 先看:packages/cli/src/commands/serve.ts 的 declaresCryptoProvider、refuseUnloadedCryptoProviderPlugins 的 label,以及栈插件循环 catch 里的 list: 'devPlugins'。
  • 两分钟验证:pnpm --filter @objectstack/cli exec vitest run --project integration src/commands/serve-host-crypto-provider.test.ts test/serve-crypto-provider-plugin-load-failure.integration.test.ts,预期 34/34。
  • 需要你做的:无,席位复审后落地。

What changes

  • Refusal label. os serve refuses to boot when an entry that may provide the crypto provider (crypto-provider) does not load. Under --dev, a devPlugins entry is now named devPlugins[i], where it used to be named by its position after the stack's plugins (plugins[plugins.length + i]).
    • UnloadedStackPlugin gains an optional list ('plugins' | 'devPlugins'; absent means plugins).
    • The boot loop records it from the authored plugins length.
  • One reading of the declaration. refuseUnloadedCryptoProviderPlugins reads providesServices through the predicate cryptoProviderPlugins filters by (declaresCryptoProvider), not through its own copy.
  • Docstring. It now states the rule the code applies: an entry that declares other services, or declares none, is ruled out.

Which entries are refused is unchanged.

Why

Follow-up to the contract review of #31263 (should-fixes 2 and 3, and the docstring half of 1).

Verification

Readings on head 279bb7dca (one commit on main at 9447f9552).

Pins, red-first and restore
  • serve-crypto-provider-plugin-load-failure.integration.test.ts: a fifth real boot, os serve --dev over a host config whose plugins[1] and devPlugins[0] are specifiers that do not import. It is refused, and names devPlugins[0] ('…') and plugins[1] ('…').
  • serve-host-crypto-provider.test.ts:
    • a unit case for the devPlugins label;
    • a case that refusal and cryptoProviderPlugins agree on a declaring entry, an entry declaring another service, and an entry declaring none;
    • the source pin now locates the record by unloadedStackPlugins.push(.
  • Red-first: with serve.ts set back to its main blob, the two files read 2 failed | 32 passed (34). The two red cases were the devPlugins label unit case and the --dev boot. Restored (blob equal to HEAD): 34 passed (34).
Package suite and typecheck
  • tsc --noEmit in packages/cli: exit 0.
  • pnpm --filter @objectstack/cli test under the verify lock: Test Files 608 passed | 1 skipped (609), Tests 7538 passed | 9 skipped (7547), VERDICT command-exit 0.
Derived gates

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands: 72 families on this diff, every one run and exit 0 (check:type-check-debt under the verify lock). --ran reconciliation: 72 derived, 72 run, 0 NOT-MEASURED, 0 UNRUN.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (No packages/spec change.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; eslint --no-inline-config on the three changed source files: exit 0.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: not touched.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English, except the maintainer summary the lane's PRs carry.
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

…was written in

When os serve --dev refuses to boot because an entry that may provide the
crypto provider did not load, a devPlugins entry is now named devPlugins[i]
rather than by its position after the stack's plugins. The refusal reads an
entry's providesServices through the same predicate as cryptoProviderPlugins,
and its docstring states the rule the code applies: an entry that declares
other services, or declares none, is ruled out.

Clause-②: no
Metadata-protocol: none

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5mgePB9NQ4PNhvYVxk
@github-actions github-actions Bot added the size/m label Nov 8, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor

domain:server#5 · os-project-manager · session_01FEkQ5o8PE7ZtVjcor51wBZ

Contract-review-claim: session_01FEkQ5o8PE7ZtVjcor51wBZ · head 279bb7d · 2026-11-08T04:04Z

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 8, 2026
@github-actions

github-actions Bot commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/deployment/cli.mdx (via devPlugins (literal, a string literal in Serve; a string literal in UnloadedStackPlugin))
  • content/docs/getting-started/quick-start.mdx (via devPlugins (literal, a string literal in Serve; a string literal in UnloadedStackPlugin))
  • content/docs/plugins/index.mdx (via devPlugins (literal, a string literal in Serve; a string literal in UnloadedStackPlugin))

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

  • content/docs/releases/v17/17-4.mdx (via devPlugins (literal, a string literal in Serve; a string literal in UnloadedStackPlugin))

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
  • 1 anchor(s) matched too much of the corpus to be a work list: os serve (command, 47 pages)
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 64 of 227 client-bound route-ledger rows — the other 163 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 163: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 59 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 104 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 — 35 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 d0512c2fbec9e0abd2f7fbd09986e4fdab73211d → packageMentionDocs.

Which tree this was computed on

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

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

⚠️ 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 d0512c2fbec9e0abd2f7fbd09986e4fdab73211d → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor

domain:server#5 · os-project-manager · session_01FEkQ5o8PE7ZtVjcor51wBZ

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 279bb7dca919e2b09539f9db8bf38ba9253b7df0
Local-runs: none
Scope: packages/cli/src/commands/serve.ts, packages/cli/src/commands/serve-host-crypto-provider.test.ts, packages/cli/test/serve-crypto-provider-plugin-load-failure.integration.test.ts + packages/cli/src/utils/stack-collections.ts, packages/cli/src/utils/one-shot-settings.ts, packages/cli/src/utils/plugin-detection.ts, packages/cli/src/utils/merge-boot-config.ts, packages/cli/src/commands/start.ts, packages/cli/package.json, packages/core/src/stack-plugins.ts, packages/core/src/kernel.ts, packages/runtime/src/app-plugin.ts, packages/types/src/crypto-provider-service.ts, packages/spec/src/kernel/plugin-validator.zod.ts, docs/adr/0116-plugin-ordering-declared-contract.md, the contract-review record on PR #31263 (comment 6712974584, read for what each should-fix asked)

Inputs read: the PR body, file list, its one commit and message, and the net diff against main (merge-base 9447f9552; main has since moved to d0512c2fb, with serve.ts byte-identical between the two and no commit since the base touching packages/cli or packages/core/src/stack-plugins.ts), the check-runs on the head, the private drafter hand-back (its claims checked against the tree), the published spec and the cited code on main. No card, by the maintainer's ruling for this series; Refs #28284 is the series mailbox, closed, with nothing on it about this PR. Why line, as fixed: 「QA 第三轮发现的安全加固(细节按披露裁决不公开)」. This PR is the follow-up to the #31263 review: its should-fix 2 (name a devPlugins entry by its own list), should-fix 3 (one reading of the declaration) and the docstring half of should-fix 1.

Check-runs on the head at the time of writing: 45 runs, 30 completed (17 success, 13 skipped, none failed or cancelled), 15 still in progress (seven Test Core shards, Build Core, the four Type Check jobs and three Lint jobs). Among the completed successes: Check Changeset, Check PR Size, PR-body guards, Governed Surface Queue Guard, Lint · ESLint, Lint · docs & code guards, Test Core (5/8), Dogfood Regression Gate. The verdict below stands on the diff and the completed runs; the seat gates landing on the remaining runs finishing green, and a red run reopens this record.

① Derived judgments

  1. Which entries are refused does not move. Right, verified in the tree rather than from the PR body: the predicate refuseUnloadedCryptoProviderPlugins filters by is now the module-private declaresCryptoProvider (providesServices is an array naming crypto-provider), whose body is the same expression that was written inline on main both there and in cryptoProviderPlugins's filter; what it is applied to (said: the materialized object, else the object entry, else nothing, which is refused) is unchanged. Every entry refused on main is refused on the head and the reverse. The PR's own agreement case pins that refusal and cryptoProviderPlugins answer alike for a declaring entry, an entry declaring another service and an entry declaring none.
  2. refuseUnloadedCryptoProviderPlugins is not routed through cryptoProviderPlugins([said]). Right, and deliberately so: that routing would have changed which entries are refused (a loosening fix(cli): os serve refuses to boot when a plugins entry that may provide the crypto provider does not load #31263's should-fix 3 did not ask for); the shared predicate gives the one reading without moving the set.
  3. A devPlugins entry is named devPlugins[i] under --dev. Right. UnloadedStackPlugin gains an optional list ('plugins' | 'devPlugins'; absent reads as plugins), the label spells ${list}[${index}], and the boot loop records the list from two counts taken where the boot list is built: the authored plugins length (stackBootPlugins(config, false)) and the boot list's length (stackBootPlugins(config, flags.dev)), both read off the same config object at the same point. Between that point and the loop the only mutation of the list is one append (the runtime AppPlugin wrap), so every index below the boot count keeps its meaning and the half-open range between the two counts is exactly the devPlugins slice. Without --dev the two counts are equal and no entry takes the dev label. The appended wrap, if it ever failed to load, would carry the plugins label with an index past the authored list; it declares no providesServices and is an instance, so it is always ruled out and never named.
  4. The docstring states the rule the code applies. Right: "refused when its providesServices names the provider, and ruled out when it declares other services or declares none", matching the code and the CONTROL pins, which is what fix(cli): os serve refuses to boot when a plugins entry that may provide the crypto provider does not load #31263's should-fix 1 asked for; the PR body already said it.
  5. Order of events unchanged. The record in the loop's catch still precedes refuseUnloadedCryptoProviderPlugins(unloadedStackPlugins), which still precedes the host provider construction; the source pin now anchors on unloadedStackPlugins.push( because the push became multi-line, and holds the same three positions.
  6. Public surface: unchanged. declaresCryptoProvider is not exported; UnloadedStackPlugin's new optional field sits on a type that @objectstack/cli's export map (., ./console, ./hook-body, package.json) does not reach.
  7. Metadata-protocol: none. Right: nothing an author writes in metadata changes.

② Semver level

  • @objectstack/cli: patch. Right: a user-visible refusal label and an internal refactor in a released package; a changeset is owed and patch is its level.
  • Clause-②: no — correctly no, not no (narrowing). No boot that succeeded before is refused now, and no boot refused before goes on now (① 1); the only observable change is the label text under --dev. The narrowing this family carries is the one fix(cli): os serve refuses to boot when a plugins entry that may provide the crypto provider does not load #31263 declared and the record on that PR judged; this PR moves it by nothing. No published accept set or surface changes.
  • Changeset body. Carries both declaration lines, the package and level, what the label changes to and the statement that the refused set is unchanged. Check Changeset on the head: success.
  • Disclosure check. PR title and body (both languages), commit message, changeset, the new predicate's docblock, the rewritten docstring, the two new call-site comments, and both test files' headers and case names were read. All stay at the level of which entries are refused, which are not, where the check sits and what the refusal names; the one sentence on what the provider seals is main's own text, unchanged. No breach.

③ Boundary flags

  • Hand-back: the predicate is shared, not routed through cryptoProviderPlugins([said]), so the refused set does not move. Confirmed and agreed (① 2).
  • Hand-back: the only mutation between the counts and the loop is the AppPlugin append; the appended wrap keeps the plugins label and declares nothing. Confirmed in the tree (① 3): one append at the metadata-wrap site, no insertion, no reorder; the runtime AppPlugin declares optionalDependencies and requiresServices only.
  • Hand-back: the standalone-branch check is unchanged by this PR. Confirmed. One observation for the series is recorded in the seat's private notes; it is not this PR's.
  • Hand-back verification claims (red-first 2 failed / 32 passed against main's blob, 34/34 restored, tsc 0, eslint 0, 72/72 derived gates, CLI suite green): consistent with my reading — the dev-label unit case and the --dev boot are the only assertions main's label would fail; not reproduced (read-only); the head's check-runs are the gate verdicts.
  • fix(cli): os serve refuses to boot when a plugins entry that may provide the crypto provider does not load #31263's three should-fixes: closed (① 1, ① 3, ① 4). Its escalated items 1 and 2 are untouched by this PR and stay with their own private items.
  • No open question in the hand-back is left unanswered.

④ Design judgment

The narrowing read against the published spec on main is #31263's, which the record on that PR judged: it takes no declared capability away at the consumer and makes the provider contract's own refusal real at the long-lived host. This PR changes nothing in that set (① 1) and was pushed on everything it does change: the shared predicate (identical body, identical operand, nameless candidates still refused), the label mapping (both counts from one config at one point; append-only mutation; --dev off collapses the range; a bare config under --dev maps the boot stack's list the same way; the appended wrap never reaches a label), the entries that fail before materializing (unchanged: refused by the specifier), the pin anchors (still in order), and the standalone-branch check (unchanged). No finding. No must-fix, no should-fix.

Implemented-by: claude/project-thread-fayfxc
Reviewed-by: session_01FEkQ5o8PE7ZtVjcor51wBZ

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor

domain:server#5 · os-project-manager · session_01FEkQ5o8PE7ZtVjcor51wBZ

席位意见:接受。#31263 复审三条应改项的收尾:开发态插件项在拒绝中以 devPlugins[i] 点名;两处对「是否声明加密提供者」的判断改为同一个谓词;文档注释与代码一致。拒绝哪些项不变。契约复审 PASS,无必改、无应改项。

席位核对 — ACCEPT · domain:server#5 · Head-sha 279bb7dca919e2b09539f9db8bf38ba9253b7df0 · 2026-11-08T04:23Z

The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」. Security work in this series follows the maintainer's 「所有的安全卡不要创建issue,直接派发处理修复」, so the PR refers to the series mailbox (Refs #28284, no closing keyword) and has no card.

  • Shape: 4 files (+85 / −21), read from the diff: packages/cli/src/commands/serve.ts, its unit file, the existing load-failure integration test, one changeset (@objectstack/cli patch). Clause-②: no, Metadata-protocol: none. No export added.
  • Review: CONTRACT_REVIEW_TIER (fable), a reviewer other than the author: PASS 6717072857 on this head; the refused set does not move (one predicate, same body, same operand); fix(cli): os serve refuses to boot when a plugins entry that may provide the crypto provider does not load #31263's should-fixes 2, 3 and the docstring half of 1 closed. Out-of-scope notes stay off GitHub under the disclosure ruling. Ruling-ref: 2026-11-07T16:00Z 「你有 fable ,车道契约复审可以自己做」.
  • Gates: head check-runs 35 success, 13 skipped, none red or pending. check-expected-skips: OK (exit 0). check-governed-merges: NOT governed (106 lines). The drafter's derived gates: 72 of 72 exit 0.
  • Landing: ungoverned, tier S. Ready + auto-merge in one relay dispatch.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 8, 2026 04:24
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 8, 2026 04:24
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 8, 2026
Merged via the queue into main with commit c2bc76e Nov 8, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-fayfxc branch November 8, 2026 04:49
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 Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants