Skip to content

fix(cli)!: os plugin install and the boot loader parse the compiled manifest through ManifestSchema - #27791

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-bhb6si
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-bhb6si

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

Fixes #27686

Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: follow-up owed — packages/cli/README.md (its os plugin install paragraph lists the refusals by name and does not yet name manifest_invalid; outside this card's declared file surface). No skill under skills/** teaches the compiled-manifest read.

维护者速读

  • 改了什么:os plugin install 和启动时加载已安装插件的那一步,现在都用同一个函数把 .osplugin 里的编译清单按 ManifestSchema 校验;os plugin build 拒收原生插件时会写出是谁把它引进来的。
  • 为什么改:旧版本打出的包(比如带已退役的 packaging: 'manifest-deps')以前照装不误,到启动时才以 import_failed 失败,看不到退役处方;现在安装时就被拒,并给出 schema 自己的处方。
  • 风险与回滚:这是收窄:清单不合 schema 的包(缺 name/type、多余键等)从此装不上,启动时也会被逐个点名拒载(不致命,其它插件照常加载);回滚即回退本 PR。
  • 先看:packages/runtime/src/compiled-plugin-manifest.ts(唯一的解析函数)→ packages/cli/src/commands/plugin/install.ts 的清单读取段 → packages/runtime/src/installed-plugin-loader.ts 第 4 步。
  • 两分钟验证:pnpm --filter @objectstack/cli exec vitest run src/commands/plugin/install.integration.test.ts -t manifest-deps,预期看到 manifest_invalid 拒装且 details 首行是 packaging: 加退役处方。
  • 需要你做的:无,席位落地(另需一次契约复审 PASS)。

What changes

  • One parse helper, both doors. readCompiledPluginManifest (new, packages/runtime/src/compiled-plugin-manifest.ts, exported from @objectstack/runtime) reads objectstack.plugin.json out of an artifact's files and parses it through ManifestSchema. It answers the parsed manifest, or unreadable (no manifest, not a JSON object) / invalid (the schema's issues, one path: message line each, plus the same on one line).
  • os plugin install asks it at step 2, before integrity and before anything is unpacked. invalid is refused as manifest_invalid with the issues as details; for packaging: 'manifest-deps' the first line is the retirement prescription. unreadable keeps its manifest_unreadable refusal. The hand-rolled id/version check (manifest_incomplete) and the hand-rolled integrity shape check (integrity_malformed) are gone: both are schema issues now, so there is no second copy of either rule. The record's runtime / grantedPermissions come from the parsed manifest.
  • The boot loader (installed-plugin-loader.ts, check 4) asks the same helper. invalid refuses that plugin as manifest_invalid (new member of InstalledPluginRefusalCode) with the schema's message, before its entry module is imported; the boot stays non-fatal and the next plugin loads. unreadable keeps artifact_unreadable.
  • os plugin build's native-addon refusal names, for a bundled package that builds or ships an addon, the files outside it that import it and the shortest chain from the plugin's own source (src/index.ts → ws-like → fake-native), the way the .node-module line already names its importers.
  • Changeset: @objectstack/cli and @objectstack/runtime minor, fix(cli)!, BREAKING banner, adr-0087: not-required (no-migration-prescription).

Tests

  • packages/runtime/src/installed-plugin-loader.test.ts: two new table rows (manifest-deps → manifest_invalid; a manifest with no type → manifest_invalid), each asserting nothing was imported; and one case asserting the refusal message carries the prescription (packaging: \packaging: 'manifest-deps'` was removed … Delete the key) while the next plugin is imported and loaded. Fixture manifest gains type: 'plugin'` (a valid manifest).
  • packages/cli/src/commands/plugin/install.integration.test.ts: a signed, trusted manifest-deps artifact is refused as manifest_invalid, details opens with the prescription, and nothing exists under plugins/env_local/com.acme.retired; a drifted manifest (no name, an array integrity) is refused naming both issues. Fixture gains name.
  • packages/cli/test/serve-installed-plugins.integration.test.ts (real os serve boots): a pre-retirement install laid down as an id/version-only install did is refused at boot as manifest_invalid with the prescription, not import_failed, its init never runs, and the boot exits 0. Fixture gains name.
  • packages/cli/test/plugin-build-bundled-only.test.ts: a transitive native package (src/index.ts → ws-like → fake-native, binding.gyp) is refused naming imported by node_modules/ws-like/lib/buffer-util.js and reached through src/index.ts → ws-like → fake-native, and ws-like is never named as a native package.
  • json-stdout-purity.e2e.test.ts and schema-migrate.one-shot-family.integration.test.ts fixtures gain name (their artifacts now meet ManifestSchema).

Ablation (each fix removed on disk, the pins re-run, then restored with git checkout HEAD --, tree clean):

  • loader parse replaced by a bare JSON.parse → 3 failed / 56 passed (the three new loader pins); restored → 59 / 59.
  • install parse replaced by a bare JSON.parse, and the build's packageImporters call replaced by empty lists → 3 failed / 31 passed (the two install pins + the chain pin); restored → 34 / 34.

Acceptance notes

  • ManifestSchema is consumed as it stands; packages/spec is not touched.
  • The ADR-0087 D2 conversion manifest-packaging-manifest-deps-removed could strip the value from a stored artifact, but the triage ruling is that both doors refuse with the schema's message, and a conversion at install would hand the operator bytes that differ from the signed artifact; neither door converts.
  • An artifact os plugin build produced from a valid source manifest re-parses unchanged (the boot test's os plugin build artifact installs and loads, com.acme.built).

Verification

Local readings on objectstack-ai/objectstack at f6d16b37 (base origin/main 81d00a4e):

  • pnpm --filter @objectstack/runtime typecheck: exit 0. pnpm --filter @objectstack/cli typecheck (tsc + test typecheck): exit 0.
  • packages/runtime full vitest run: Test Files 482 passed (482), Tests 8767 passed / 19 skipped.
  • packages/cli full vitest run (unit + integration, after pnpm --filter @objectstack/cli build): Test Files 532 passed / 1 skipped (533), Tests 6637 passed / 9 skipped.
  • packages/cli nightly tier, OS_TEST_TIERS=nightly: test/json-stdout-purity.e2e.test.ts 77 / 77.
  • Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derives 61 families; every one run with its exit code recorded before any pipe, plus pnpm lint (exit 0). --ran reconciliation: 61 derived famil(ies) accounted for — 59 run, 2 NOT-MEASURED, 0 unrun. 59 exit 0.
    • NOT MEASURED: pnpm check:dual-build-cjs-loads (exit 3, needs every package's dist), pnpm check:i18n-coverage (exit 3, needs the example closure it prints). CI runs both.
  • Dogfood battery (owed by the (narrowing) arm): run on the final head, reported in the card's report comment.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_016WV4czY8bzmjKmuhj1GP9c

…anifest through ManifestSchema

One helper, readCompiledPluginManifest in @objectstack/runtime, parses an
artifact's objectstack.plugin.json through ManifestSchema for both doors.
os plugin install refuses an invalid manifest before unpacking as
manifest_invalid with the schema's issues (for packaging: 'manifest-deps',
the retirement prescription); the boot loader refuses the plugin by name
with the schema's message, non-fatal, before importing anything.

os plugin build's native-addon refusal now names, for a native package,
the files importing it and the chain from the plugin's own source.

Claude-Session: https://claude.ai/code/session_016WV4czY8bzmjKmuhj1GP9c
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 29, 2026
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/cli, @objectstack/runtime, @objectstack/spec, touching 18 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/cli/README.md, packages/runtime/src/index.ts, packages/spec/liveness/manifest.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/plugins/index.mdx (via os plugin install (command, read off packages/cli/src/commands/plugin/install.ts))
  • content/docs/protocol/kernel/lifecycle.mdx (via objectstack.plugin.json (literal, a string literal in COMPILED_PLUGIN_MANIFEST_FILE; a string literal in MANIFEST_FILE), os plugin build (command, read off packages/cli/src/commands/plugin/build.ts), os plugin install (command, read off packages/cli/src/commands/plugin/install.ts))
  • content/docs/protocol/kernel/plugin-spec.mdx (via objectstack.plugin.json (literal, a string literal in COMPILED_PLUGIN_MANIFEST_FILE; a string literal in MANIFEST_FILE), os plugin build (command, read off packages/cli/src/commands/plugin/build.ts))

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

  • content/docs/releases/v17/17-5.mdx (via os plugin build (command, read off packages/cli/src/commands/plugin/build.ts))

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 changed file(s) yielded no anchor (packages/cli/README.md, packages/runtime/src/index.ts, packages/spec/liveness/manifest.json) — pages documenting those are invisible to this run
  • the SDK route bridge reached 58 of 210 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 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; 97 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 — 179 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 cb074ec3b35d69a1b98fe62ea23d77b8b6051f0c → packageMentionDocs.

Which tree this was computed on

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

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

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

Card thread for #27686 · session_016WV4czY8bzmjKmuhj1GP9c

Skills-docs: updated here — packages/cli/README.md (commit 75f2611): the os plugin install paragraph now names the manifest_invalid refusal and that it carries the schema's own message (for packaging: 'manifest-deps', its prescription). This line supersedes the body's Skills-docs: follow-up owed — packages/cli/README.md; the seat amended the claim's file surface to include that one sentence. No skill under skills/** teaches the compiled-manifest read.

Re-run on 75f2611 (README-only delta): check:doc-authoring, check:issue-citations (both spellings), check:closed-ceremony-text, check:keyed-text-bounds, check:published-files, check-affected-docs, check-drift-comment, check-comment-mask-adoption, check:nul-bytes — all exit 0. The derived family set is unchanged by the commit.

…oot doors' refusal pins

The install door, the boot loader and the os serve boot pin author
packaging: 'manifest-deps' on purpose to assert it is refused, as the
build door's pin already does; each is excluded by name with its reason.

Claude-Session: https://claude.ai/code/session_016WV4czY8bzmjKmuhj1GP9c
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

Contract-review-claim: session_016WV4d1Np3hU7RQnA13YZcL · head a03c314 · 2026-10-29T23:51Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a03c314054d907f9938deb310bb658166eb12fc6
Local-runs: none
Scope: packages/cli/src/commands/plugin/install.ts, packages/cli/src/commands/plugin/build.ts, packages/runtime/src/compiled-plugin-manifest.ts, packages/runtime/src/index.ts, packages/runtime/src/installed-plugin-loader.ts, packages/cli/src/commands/plugin/install.integration.test.ts, packages/cli/src/utils/schema-migrate.one-shot-family.integration.test.ts, packages/cli/test/json-stdout-purity.e2e.test.ts, packages/cli/test/plugin-build-bundled-only.test.ts, packages/cli/test/serve-installed-plugins.integration.test.ts, packages/runtime/src/installed-plugin-loader.test.ts, packages/spec/src/kernel/manifest-packaging-retirement.test.ts + packages/spec/src/kernel/manifest.zod.ts (ManifestSchema, PluginPackagingSchema, PACKAGING_RETIRED, PluginIntegritySchema), packages/spec/src/shared/retired-key.ts (enumWithRetiredValues), packages/spec/src/shared/error-map.zod.ts (formatZodIssue), packages/core/src/security/plugin-artifact-integrity.ts (verifyIntegrity), packages/cli/src/utils/osplugin.ts, packages/cli/src/utils/plugin-install.ts (materializePlugin), packages/runtime/package.json (exports), scripts/pm/clause2-line.mjs (the Clause-② reader)

Face: narrowing — the PR body and the committed changeset both carry Clause-②: no (narrowing); no schema and no governed path in the file list. Read: card #27686 body and every comment (triage 6504372423, the seat's claim 6509288230 with its two surface amendments, the dev report 6511301044 as a claim to verify); PR #27791 body, 14-file list, net diff of a03c3140 against its merge base 81d00a4e on main, and its three comments; merged PR #27679 for how ManifestSchema carries the retirement. Check-runs on a03c3140 at 2026-10-30T00:04Z: 48 runs, all completed, every one success or skipped (Build Core, Lint & Repo Gates, TypeScript Type Check, Check Changeset, Governed Surface Queue Guard, Spec property liveness, PR-body guards and all eight Test Core shards among the successes; the skips are opt-in and matrix placeholders). Earlier readings during this review had Test Core 2/8 through 7/8 in_progress; the final reading is the one recorded. Those conclusions are the gate verdicts; nothing was built, run or re-run here. The dev's ablation and gate counts are its claim, not evidence.

① Derived judgments

  1. os plugin install refuses an invalid compiled manifest before anything is written — narrowing, real. install.ts step 2 calls readCompiledPluginManifest(files) right after archiveEntryProblems and before pluginInstallDir, verifyIntegrity (step 3) and materializePlugin; refuse() only emits JSON or prints and exits, so on manifest_invalid no byte reaches the plugin home and no record is written (the install pin asserts the directory is absent). The hand-rolled manifest_incomplete (id/version by hand) and integrity_malformed (shape by hand) refusals are deleted: both facts are now ManifestSchema issues, so neither door keeps a second copy of a manifest rule. The read sits after signature verification over the raw bytes (step 1), which is the right order — untrusted bytes are not parsed before the key is checked. Accept-set change: a compiled manifest that is not a ManifestSchema manifest (retired packaging value, missing name/type, unknown key under strictObject, bad id pattern, non-SemVer version, non-record integrity) installed on main and is refused here. Declared as BREAKING on both carriers. Judged right.
  2. Boot loader, check 4: reported reason becomes the schema's message, still non-fatal. checkRecord returns refuse('manifest_invalid', ...) through the same Check channel as every other refusal, before dispatchPluginRuntime and before any import(); verifyInstalledPlugins / loadInstalledPlugins keep going to the next record (the com.acme.fine pin and the real os serve boot pin, exit 0). unreadable keeps artifact_unreadable, so the only code that moves is a schema refusal, which on main surfaced later as import_failed. manifest_invalid is added to InstalledPluginRefusalCode; the one consumer outside the loader (packages/cli/src/utils/installed-plugins-boot.ts) prints error generically, no exhaustive map to update. Judged right, and it is what triage 6504372423 ordered.
  3. packages/runtime/src/index.ts widens @objectstack/runtime's published surface — and the declaration does not say so. packages/runtime/package.json has one exports entry, . → dist/index.js; the diff adds export { readCompiledPluginManifest } and export type { CompiledPluginManifestRead } to that entry (neither existed at the merge base; the helper file is new). The export is a design necessity — packages/cli can only reach a runtime helper through the package root, and the "one helper for both doors" ruling puts it in runtime — so the code is right and the declaration is wrong: the Clause-② value answers 「本卡放宽接受集或扩大公开面吗」, and a new named export on a published package's root entry enlarges the public surface, so the truthful value is yes. The fleet's one reader (scripts/pm/clause2-line.mjs, "The four combinations") spells a diff that widens one surface and narrows another as Clause-②: yes (narrowing) — both facts read, the narrowing face and this tier unchanged, the level still at least minor (which the changeset already has). The PR body and the changeset both read Clause-②: no (narrowing). Must-fix (②/declaration): change the line on BOTH carriers to Clause-②: yes (narrowing); Metadata-protocol: narrowed and the levels stay. (The seat's own claim 6509288230 carried no (narrowing) before the dev placed the helper in runtime; the dev report names the export and still left the line.)
  4. build.ts native-addon refusal names the importer chain. packageImporters lists files outside the package that import into it, and a breadth-first walk from the plugin's own inputs (those with no node_modules root) to the first file inside the package, collapsed to one step per package name; the line gains , imported by … and (reached through a → b → c) only when there is something to say (a chain of two or more steps), so the direct-hit line is unchanged. The .node-module branch above it was already naming importers; this brings the package branch to parity. metafile.inputs[i]! is safe (i comes from Object.keys(metafile.inputs)); imp.path in metafile.inputs guards external imports. Pinned by the ws-like → fake-native case, which also asserts ws-like is never itself named native. Judged right; no accept-set change (what was refused is refused, with more words).

② Semver level

  • Changeset .changeset/27686-plugin-install-parses-compiled-manifest.md: @objectstack/cli: minor, @objectstack/runtime: minor. Packages the diff publishes: packages/cli and packages/runtime — matches (packages/spec and packages/core are only read; the spec edit is a test file, which publishes nothing). Metadata-protocol: narrowed → kind takes at least minor on both moved packages — consistent. narrowed is the right kind: a requirement on what an author's compiled objectstack.plugin.json must satisfy is tightened at two doors (nothing in ManifestSchema itself moves, as the changeset says).
  • BREAKING banner present (**BREAKING** accept-set narrowing …), title fix(cli)!, FROM → TO remedy in the body (fix the source manifest as the refusal says, rebuild, sign, install). ADR-0087 disposition: exactly one marker, not-required (no-migration-prescription) — honest here: no key is added, removed, renamed or re-shaped and migrate meta has nothing new to rewrite (PR feat(spec, cli)!: bundled is the one plugin packaging strategy — manifest-deps is retired, and os plugin build refuses a native addon (#27123, slice 3 of 3, server half) #27679 already registered the D2 conversion for the value itself); check:adr-0087-registration ran green in the Lint gates.
  • Clause-② / Metadata-protocol lines are present and byte-identical on the PR body and the changeset — consistent with each other, and both wrong in the same way (①.3): must read Clause-②: yes (narrowing). The level is unaffected by the fix.
  • Prose contradiction check: packages/cli/README.md install paragraph now names manifest_invalid with the schema's own message — agrees with the code; the PR body's Skills-docs: follow-up owed is superseded by comment 6510905884, which the seat's amended surface covers.

③ Boundary flags

  • Dev report open_questions: []; deviations: (a) PR opened without assignees — procedural, no bearing on the diff; (b) surface grown twice by seat amendment — both amendments are on the card (6509288230) before the commits that used them (75f2611 README, a03c314 spec test). No stop-on-breach is owed.
  • Spec test edit (seat-approved, test-only): three EXCLUDED entries in manifest-packaging-retirement.test.ts. Read each file: install.integration.test.ts authors packaging: 'manifest-deps' once, inside the pin asserting manifest_invalid and the prescription; installed-plugin-loader.test.ts twice, both inside refusal pins (table row and the "never imported, next plugin loads" case); serve-installed-plugins.integration.test.ts once, in beforeAll laying down a pre-retirement install whose only assertions are that boot refuses it as manifest_invalid, never import_failed, and never runs its init. Each reason holds. The walk's exclusion is file-level (same mechanism and granularity as the existing build-door pin entry); the anti-vacuity bounds (more than 1000 files visited, more than 3 authoring a packaging value) are untouched by three files, so the guard is not weakened otherwise.
  • schema-migrate.one-shot-family.integration.test.ts and json-stdout-purity.e2e.test.ts: each gains one fixture line, name: …. Both drive a real os plugin install on a hand-assembled artifact whose manifest lacked name — a required ManifestSchema key — so the narrowing itself turns them red; the fix makes the fixtures what os plugin build would have produced. They are tests of the install door and fall under the claim's "their tests"; naming them: a consequence of the narrowing, inside the surface, not a scope creep. (That two in-repo fixtures were never ManifestSchema manifests is itself evidence that the gap the card names was real.)
  • Stale evidence prose this PR leaves behind (should-fix, follow-up carrier): packages/spec/liveness/manifest.json:307 (the integrity row's evidence) and docs/qa/platform-checklist/areas/cli.json:1802 both still name integrity_malformed as an os plugin install refusal; this PR deletes that code (it is a manifest_invalid schema issue now). Spec property liveness passed because it checks the install.ts#PluginInstall anchor, not the prose. The claim forbids packages/spec edits beyond the test file, so this is either a seat amendment or a follow-up card — not a landing blocker.
  • A third hand reader of the compiled manifest remains, outside the ruling: packages/cli/src/commands/plugin/publish.ts (step 2) and readOspluginManifest in packages/cli/src/utils/osplugin.ts still JSON.parse the compiled manifest and read id/version/name by hand. Triage named two doors (install, boot) and this PR delivers exactly those; the publish preflight is a candidate for the same helper. Noted for a follow-up card, not a fix here.

④ Design judgment

  • Against the published spec on main: ManifestSchema (packages/spec/src/kernel/manifest.zod.ts, unchanged by this PR — verified by diff) is a strictObject whose packaging is enumWithRetiredValues(['bundled'], PACKAGING_RETIRED); the retired member's prescription is the enum's own error message at path packaging (PR feat(spec, cli)!: bundled is the one plugin packaging strategy — manifest-deps is retired, and os plugin build refuses a native addon (#27123, slice 3 of 3, server half) #27679 landed that; formatZodIssue renders it ✗ packaging: …). This PR makes that refusal real at two doors that bypassed it; it takes no declared capability away at the consumer — nothing a valid manifest declares is refused that main's os plugin build would accept.
  • One helper, no second copy: readCompiledPluginManifest is the only ManifestSchema.safeParse of the compiled manifest; both doors map its three-way answer (ok / unreadable / invalid) onto their own channels and nothing else. The manifest_mismatch compare in the loader and verifyIntegrity in install consume the parsed value. Holds.
  • Break attempts, read:
    • A valid artifact built by current os plugin build must still install. build.ts writes { ...parsed.data, main: 'dist/index.mjs', integrity } — the schema's own OUTPUT plus two declared optional keys. ManifestSchema has no .transform/.preprocess/.refine; the only output-vs-input differences are defaults (scope: 'project', defaultDatasource: 'default'), both of which re-parse; main is a plain optional string, integrity is z.record(string, string) and computeIntegrity yields exactly that. So the compiled manifest round-trips. Pinned live by the boot test's os plugin build artifact (com.acme.built) installing and loading.
    • Build-only keys in a compiled manifest. There are none: the compiled manifest is the parsed source manifest plus main and integrity, both declared on ManifestSchema for precisely this reason (the main docblock says so). A manifest carrying a key the schema does not declare was never a ManifestSchema manifest; refusing it is the declared narrowing.
    • Refusal before any byte is written. Confirmed in ①.1: the parse precedes pluginInstallDir, the integrity step, the lock and materializePlugin; refuse() touches no disk.
    • Boot refusal of an already-installed stale plugin. The realistic upgrade case — a record laid down by the old install for a manifest-deps artifact — is pinned by the real os serve boot test (materializePlugin + recordPluginInstallation), refused by name with the prescription, boot exit 0, next plugin loaded.
    • Message rendering. The helper strips ✗ and trims each formatZodIssue line, so nested union branches lose their indentation (flattened list). Cosmetic; the path: message form the pins assert survives. Not a fix.
  • Findings: one must-fix (①.3, Clause-②: yes (narrowing) on both carriers); one should-fix (③, stale integrity_malformed evidence prose in packages/spec/liveness/manifest.json and docs/qa/platform-checklist/areas/cli.json). The code holds; the declaration does not.

Implemented-by: claude/project-thread-bhb6si
Reviewed-by: session_016WV4d1Np3hU7RQnA13YZcL

VERDICT: FAIL

…egrity_malformed evidence

The changeset now reads `Clause-②: yes (narrowing)`: readCompiledPluginManifest
and CompiledPluginManifestRead are new named exports of @objectstack/runtime's
root entry. The spec liveness row for `integrity` and the cli platform-checklist
item (revision 6) no longer name the deleted `integrity_malformed` refusal: a
malformed map is a ManifestSchema issue, refused as manifest_invalid.

Claude-Session: https://claude.ai/code/session_016WV4czY8bzmjKmuhj1GP9c
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Card thread for #27686 · session_016WV4czY8bzmjKmuhj1GP9c

Corrected declaration (contract review 6511494551, must-fix ①.3) — this line supersedes the body's Clause-②: no (narrowing):

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

Why: readCompiledPluginManifest and CompiledPluginManifestRead are new named exports of @objectstack/runtime's root entry (the widening); the install and boot-loader refusals are the narrowing. Levels, banner and ADR-0087 disposition are unchanged. Commit f9521e4f carries the same line in .changeset/27686-plugin-install-parses-compiled-manifest.md, and (should-fix ③) re-points the integrity row's evidence in packages/spec/liveness/manifest.json and the cli.plugin-manifest-build-contract source row in docs/qa/platform-checklist/areas/cli.json (revision 6, history entry) from the deleted integrity_malformed to manifest_invalid.

Local on f9521e4f: check:platform-checklist, check-adr-links (+ --self-test), check:liveness and the 15 other @objectstack/spec check families the derivation names (after a spec build), check-adr-0087-registration, check-changeset-no-major, check-empty-changeset, check:changeset-gate-self-tests, check:doc-authoring, check:issue-citations — all exit 0.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

Contract-review-claim: session_016WV4d1Np3hU7RQnA13YZcL · head f9521e4 · 2026-10-30T00:15Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f9521e4fc03120931867cd0361bff810a075d93a
Local-runs: none
Scope: .changeset/27686-plugin-install-parses-compiled-manifest.md, packages/spec/liveness/manifest.json, docs/qa/platform-checklist/areas/cli.json + scripts/pm/clause2-line.mjs (readClause2Line, readArmToken), scripts/pm/record-recognisers.mjs (reviewFace), scripts/pm/check-owed-reviews.mjs (owedFace), scripts/pm/check-governed-queue-guard.mjs (recordVerdict), scripts/check-changeset-no-major.mjs (declarationFromPullRequest, judgeLevel), scripts/check-adr-0087-registration.mjs, scripts/release-github-releases.mjs (classifyReleaseBullet), scripts/pm/post-stamped.mjs and scripts/pm/fleet-write/ops.mjs (the --body / issue_patch route), packages/runtime/src/index.ts, packages/cli/src/commands/plugin/install.ts

Delta review on top of record 6511494551 (FAIL on a03c3140): the one commit a03c3140..f9521e4f (3 files, 7 insertions / 3 deletions, no source or test file), PR comment 6511632334 (the dev's corrected-declaration comment), the seat's claim 6509288230 as amended, the seat's claim line 6511663042 for this head, and the PR body as it stands now. Everything judged PASS-worthy in 6511494551 on the code (①.1, ①.2, ①.4, ④) is unchanged by this delta and is adopted, not re-argued. Check-runs on f9521e4f at 2026-10-30T00:22Z: 48 runs; 24 success, 10 skipped, 14 still in_progress (Build & E2E UI, Build Core, Live E2E UI, Test Core (1/8), Test Core (2/8), Test Core (3/8), Test Core (4/8), Test Core (5/8), Test Core (6/8), Test Core (7/8), Test Core (8/8), Type Check UI, Type Check · consumer gates, Type Check · workspace); no run has failed. Among the green: Check Changeset, Governed Surface Queue Guard, PR-body guards, Spec property liveness, Lint · repo & tooling gates, Lint · CI/release/engine gates, Lint · ESLint. Those conclusions are the gate verdicts; a run still in progress is recorded as such. Nothing was built, run or re-run here; the dev's "all exit 0" list in 6511632334 is its claim, not evidence.

① Derived judgments

  1. The must-fix (①.3 of 6511494551) is answered on the committed carrier. The changeset now reads Clause-②: yes (narrowing) / Metadata-protocol: narrowed, and its "Now." bullet names the widening by its two members: readCompiledPluginManifest and CompiledPluginManifestRead, new named exports of @objectstack/runtime's root entry (the only exports entry of that package) — exactly the widening the record found, no more and no less (packages/runtime/src/index.ts is untouched by the delta and still adds those two names only). Levels (minor/minor), the BREAKING banner, the fix(cli)! title and the ADR-0087 not-required (no-migration-prescription) marker are unchanged, which is right: yes takes at least minor, which both already had.
  2. No accept set, public surface or runtime behaviour moves in the delta. The three files are a changeset, a spec evidence ledger and a QA checklist row; no src/, no test, no schema.
  3. The should-fix (③ of 6511494551) is answered. packages/spec/liveness/manifest.json integrity row: the evidence no longer names integrity_malformed; it says a map that is not a path → digest record is refused one step earlier as a ManifestSchema issue, manifest_invalid, by readCompiledPluginManifest, and that integrity_mismatch is unchanged — that is what install.ts at this head does (the parse at step 2, verifyIntegrity at step 3). The anchor install.ts#PluginInstall still resolves; Spec property liveness is green on this head. docs/qa/platform-checklist/areas/cli.json: the one source row is re-pointed the same way, revision 5 → 6, and a revision-6 history entry in the shape of its revision-5 neighbour (revision, date, change, ref) says one source row moved and no clause changed — true of the diff. Nit, not a fix: the liveness row's verifiedAt keeps the date it had before this change (two days earlier) although its evidence was re-read for this change; the clock is a freshness window, not a change marker, so nothing reads it wrong.

② Semver level

  • Changeset: @objectstack/cli: minor, @objectstack/runtime: minor — the two packages the diff moves; Metadata-protocol: narrowed → at least minor on both; Clause-②: yes → at least minor; consistent. BREAKING banner, migration (fix the source manifest, rebuild, sign, install), one ADR-0087 marker: unchanged and still right. Check Changeset is green on this head.
  • Declaration across the three carriers — what stands where. Changeset (committed): yes (narrowing). Seat claim 6509288230 (amended): Clause-②: yes (narrowing) (amended … ) — readArmToken takes the first parenthetical as the arm and leaves a later one as reasoning, so it reads declared, value yes, arm narrowing. PR comment 6511632334: yes (narrowing), stated as superseding the body. PR body: still Clause-②: no (narrowing) (read at this head; the body was not edited).
  • What the readers read, exactly. (a) The queue guard (recordVerdict) calls reviewFace(pair.paths, pair.repo) with no declaration text: it reads the Clause line nowhere, and on this PR — no governed path — it gates on no record at all (green on both heads). (b) check-owed-reviews (owedFace) reads the PR body: no (narrowing) and yes (narrowing) both put the narrowing limb first and both map to the narrowing face at CONTRACT_REVIEW_TIER — the owed review and its tier are identical under either spelling. (c) check-changeset-no-major (declarationFromPullRequest) reads the PR body's value, arm and protocol: narrowed beside arm narrowing clears the narrowed-without-arm contradiction with either value; added-under-no needs added; the yes leg refuses only a moved package left at patch with nothing raised, and both are minor — so the body's no neither hides a refusal nor spares one; green on this head. (d) check-adr-0087-registration reads the changeset body: arm narrowing → the breaking signal; yes now matches the banner and the !. (e) release-github-releases (classifyReleaseBullet) reads the CHANGELOG bullet, i.e. the changeset: yes (narrowing) + narrowed → breaking → the manual section (no registered marker), which is the right shelf; the PR body never reaches the release notes. (f) post-stamped reads the Clause line only on claim comments it posts. So: every mechanical reader that decides a level, a face, a breaking signal or a release shelf either reads the committed changeset (now correct) or reads the PR body in a way that cannot distinguish the two spellings. No gate verdict differs.
  • Is a PR-comment correction acceptable? As the record of what was corrected and why, yes — and 6511632334 does that well. As the carrier, no: nothing reads a comment for the declaration, and the Post-Task Checklist puts the line in the PR body. The dev's reason — "the PR body cannot be re-edited through the relay" — does not hold as stated: the relay's op table carries issue_patch with a body, which is post-stamped --body=27791 --file=… (PATCH /issues/27791 — a pull request is an issue to that endpoint). What that route costs is the first-line signature rule (the body's first non-empty line would have to become the seat signature, ahead of the platform's claude-projects-attribution comment and the _Requested by_ line), which is a header reshaping the maintainer may not want on a thread-created PR. Should-fix, not a landing blocker: bring the body's line to Clause-②: yes (narrowing) — by the thread's own platform edit, by hand, or through the relay with the attribution lines kept directly under the signature — before this PR lands, so the human carrier and the committed carrier agree. It is a should-fix because the committed carrier is right and no reader's verdict turns on the body's value (② above); it would be a must-fix if any did.

③ Boundary flags

  • The claimed surface was amended (6509288230) to cover both ledger files before the commit that touched them; the delta touches nothing outside it (no packages/spec/src, no governed path).
  • The third hand reader of the compiled manifest (publish.ts, readOspluginManifest) is filed as cli: os plugin publish and readOspluginManifest still hand-parse the compiled manifest, a third door outside the one ManifestSchema parse #27686 gives install and boot #27810, open — the follow-up this record asked for exists; nothing of it is in this PR, correctly.
  • docs/qa/platform-checklist/** is validated by pnpm check:platform-checklist, which lint.yml says runs by maintainer hand and by the platform-checklist-watchdog patrol, not on a PR: the cli.json edit's shape is read here (it mirrors its revision-5 neighbour) and the dev's local "exit 0" is a claim; no gate on this head measures it. Named, no fix.
  • Dev report for the delta: none owed beyond 6511632334; open_questions stay empty.

④ Design judgment

  • The narrowing against the published spec on main is as judged in 6511494551 ④ (holds; one parse helper; refusal before any byte is written; a current os plugin build artifact round-trips; the stale-install boot case pinned) — the delta changes no code, so that judgment stands on this head byte-for-byte for every src/ and test path.
  • The declaration now tells the truth where it is read for a decision (changeset) and lags where it is read by a person (PR body). The widening is correctly named as exactly two exports, so a consumer reading the CHANGELOG learns what was added and what was taken away.
  • Findings: no must-fix. Should-fix: the PR body's Clause-② line (②). Nit: liveness verifiedAt.

Implemented-by: claude/project-thread-bhb6si
Reviewed-by: session_016WV4d1Np3hU7RQnA13YZcL

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

维护者速读

  • 改了什么:os plugin install 在解包前先用 ManifestSchema 校验插件包里的编译清单,不合法就带 schema 自己的提示拒装(如已退役的 manifest-deps 给出退役处方);启动加载器遇到同样的包仍是非致命跳过,但报的原因改成 schema 的提示,不再是笼统的 import_failed。os plugin build 的原生插件拒绝信息补上依赖链。
  • 为什么改:两个入口原先只读 id/version,旧包能装上、启动才失败,且没有处方(cli: os plugin install reads only id/version of the compiled manifest, so a stale artifact (e.g. packaging: manifest-deps) installs and fails at boot as import_failed instead of meeting the prescription #27686)。
  • 风险与回滚:收窄改动(BREAKING,minor);当前 os plugin build 产出的包照常安装;@objectstack/runtime 新增导出 readCompiledPluginManifest(一个解析助手两个入口共用)。回滚即还原本 PR。
  • 先看:packages/runtime/src/compiled-plugin-manifest.ts 与 packages/cli/src/commands/plugin/install.ts。
  • 两分钟验证:install / 启动加载器的拒绝用例,去掉解析即转红。
  • 需要你做的:无(非受管 PR,契约复审 PASS,席位按本任落地授权落地)。

席位意见:ACCEPT。

席位核对

Head-sha: f9521e4fc03120931867cd0361bff810a075d93a

  • Shape: one parse helper (readCompiledPluginManifest in @objectstack/runtime) serves os plugin install (refuses before any byte reaches the install dir) and the boot loader (non-fatal, named, the schema's message); the hand-rolled manifest_incomplete / integrity_malformed copies are deleted; build.ts names the native-addon importer chain; three seat-approved test-only EXCLUDED entries in packages/spec/src/kernel/manifest-packaging-retirement.test.ts; one README sentence; the liveness integrity evidence and the QA checklist row re-pointed to manifest_invalid; changeset minor for the touched packages with BREAKING banner and ADR-0087 not-required (no-migration-prescription). check-governed-merges --pr 27791: NOT governed, 361 changed lines.
  • Premise measured: install read only id/version on main; runtime 8767 and cli 6637 tests pass; dogfood 2867 passed / 0 failed (code head; later commits are prose, ledger and one spec test allowlist); the spec absence walk reproduced red with the three pins and is 15/15 after the entries (dev report 6511301044).
  • Gate check-runs on the head: 50 runs, 40 success, 10 skipped, 0 failure/pending.
  • Contract review: CONTRACT_REVIEW_TIER FAIL 6511494551 on a03c3140 (must-fix: the declaration), answered by f9521e4f; delta PASS 6511756144 on this head.
  • Declaration correction: the diff widens @objectstack/runtime's published entry (readCompiledPluginManifest, CompiledPluginManifestRead) beside the install narrowing, so the right line is Clause-②: yes (narrowing) — now on the changeset and the claim (6509288230). The PR body still reads no (narrowing); it is not re-edited because the relay's signature-first-line rule would displace the PR's attribution header. Corrected values recorded here and in PR comment 6511632334: Clause-②: yes (narrowing) and Skills-docs: updated here — packages/cli/README.md. Per the delta review no gate verdict turns on the body's value.
  • Out of scope: filed cli: os plugin publish and readOspluginManifest still hand-parse the compiled manifest, a third door outside the one ManifestSchema parse #27686 gives install and boot #27810 (the os plugin publish / readOspluginManifest door still hand-parses the manifest). Nit not taken: the liveness row's verifiedAt was not advanced.
  • Landing: under the maintainer's landing grant for this seat term (seat 4 thread, 2026-10-29 22:04Z): pr_ready + automerge_enable through the relay.

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

Projects

None yet

1 participant