Skip to content

fix(cli)!: os plugin publish parses the compiled manifest through ManifestSchema before any upload - #27910

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-m87tmz
Oct 30, 2026
Merged

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

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

Fixes #27810

Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: none — no skill under skills/** and no page under content/docs teaches what os plugin publish checks in the compiled manifest (skills/objectstack-platform/references/operations.md and packages/cli/README.md list the command as "upload / publish a signed .osplugin", which stays true); the QA ledger row that did name the old refusal is re-pointed here.

维护者速读

  • 改了什么:os plugin publish 现在用安装与启动已在用的同一个函数(readCompiledPluginManifest),按 ManifestSchema 校验 .osplugin 里的编译清单,校验不过就在发任何网络请求之前拒绝;CLI 内部的 readOspluginManifest 也改走这个函数。
  • 为什么改:以前发布只做 JSON.parse,旧版本打出的包(比如带已退役的 packaging: 'manifest-deps')能照样传到市场,要到用户安装时才被拒;现在在发布者自己的终端就被拒,并给出 schema 的处方。
  • 风险与回滚:这是收窄:清单不合 schema 的包从此发不出去(缺 name/type、多余键、畸形 integrity 等);合法的包照常发布,上传的 plugin_manifest 仍是包里原样的清单(不带解析时补上的默认值)。回滚即回退本 PR。
  • 先看:packages/cli/src/commands/plugin/publish.ts 第 2 步(校验与原样上传两段)→ packages/cli/src/utils/osplugin.ts 的 readOspluginManifest → packages/cli/test/plugin-publish.test.ts 新增的拒绝与原样上传钉子。
  • 两分钟验证:pnpm --filter @objectstack/cli exec vitest run test/plugin-publish.test.ts -t manifest-deps,预期两条通过:发布被拒、零次请求、输出含 packaging: `packaging: 'manifest-deps'` was removed 与 Delete the key。
  • 需要你做的:无,席位落地(另需一次契约复审 PASS)。

What changes

  • os plugin publish, step 2. The compiled manifest is read with readCompiledPluginManifest from @objectstack/runtime (the helper PR fix(cli)!: os plugin install and the boot loader parse the compiled manifest through ManifestSchema #27791 gave os plugin install and the boot loader), right after the archive is read and before the integrity preflight, the signature read, the auth lookup and both POSTs.
    • unreadable keeps the existing Cannot read manifest from artifact: … refusal.
    • invalid prints The artifact's objectstack.plugin.json is not a valid plugin manifest, so nothing was published:, one • path: message line per schema issue (for manifest-deps, the retirement prescription), and the rebuild / re-sign remedy, then exits 1.
    • The hand-rolled malformed-integrity check is gone: the map's shape is a schema issue. Artifact manifest is missing id or version. stays, now reachable only from an empty --manifest-id.
    • Every field the command reads (id, version, name, runtime, description, integrity) comes from the parsed manifest.
  • The uploaded plugin_manifest is the artifact's own manifest, unchanged. Flag for the reviewer. ManifestSchema stamps two defaults at parse (scope: 'project', defaultDatasource: 'default'), so uploading the parsed value would make the declared manifest differ from the one inside the signed bytes on every valid publish. After the helper answers ok, publish decodes the same entry once more for the payload only. That decode reads no rule: the verdict and every read field are the helper's. The pin uploads the manifest the artifact carries, as it carries it reds if plugin_manifest becomes the parsed value (measured, below). The helper's module is untouched.
  • readOspluginManifest (internal to the CLI, not in any exports entry; its only caller is plugin-publish.test.ts, so no inspect/diagnose path depends on a lenient read) parses through the same helper, throws the helper's message, and answers the parsed manifest. It is now async and imports @objectstack/runtime on call: every os plugin command loads osplugin.ts, and the runtime's root entry measured ~1.2 s to import against ~0.3 s for @objectstack/core, which os plugin build and os plugin sign would otherwise pay for a read they never make. publish.ts imports the helper statically, as install.ts does.
  • Spec guard. manifest-packaging-retirement.test.ts gains one EXCLUDED entry, packages/cli/test/plugin-publish.test.ts ("the publish door's own pin authors it to assert os plugin publish refuses it before any upload"), the same shape 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 used (claim amended by the seat first).
  • QA ledger. docs/qa/platform-checklist/areas/cli.json, item cli.plugin-manifest-build-contract: the compiled-manifest clause and its verify step now name the schema refusal instead of Artifact manifest is missing id or version., the publish.ts#version source row is re-worded, revision 6 → 7 with a history entry (claim amended by the seat first).
  • Changeset. @objectstack/cli minor, fix(cli)!, BREAKING banner, adr-0087: not-required (no-migration-prescription), both declaration lines.

Tests

packages/cli/test/plugin-publish.test.ts, five new pins and two updated:

  • readOspluginManifest throws the manifest-deps prescription (packaging: `packaging: 'manifest-deps'` was removed, Delete the key).
  • readOspluginManifest throws a drifted manifest's issues by name (name: , integrity: ).
  • publish refuses a manifest-deps artifact with zero fetch calls, the prescription on a • line, and the os plugin build remedy.
  • publish refuses a drifted manifest (no name, integrity an array) with zero fetch calls, naming both issues.
  • a valid artifact's upload carries plugin_manifest deep-equal to the artifact's manifest, with no scope / defaultDatasource.
  • the existing round-trip and absent-manifest pins, now awaiting the async read.

Ablation, on disk: restoring publish.ts and osplugin.ts from main turns the four refusal pins red (Tests 4 failed | 8 passed); setting plugin_manifest: manifest (the parsed value) turns the verbatim pin red (1 failed | 11 passed). Both restored; the file then passes 12/12.

Acceptance notes

  • Refusal before anything leaves the machine: the manifest read sits before the signature file read, the cloud-config read and both POSTs; the pins count fetch calls (0).
  • One parse, no second copy: no manifest rule is written in publish.ts or osplugin.ts; both ask readCompiledPluginManifest.
  • Not changed: the integrity preflight, the signature, auth and visibility paths, and the payload of a valid publish.

Verification

Head bd6d77e2. Gate readings (87 derived families, pnpm lint, typecheck, the CLI unit layer, the dogfood battery) are in the os-dev-report comment on #27810, read from their logs; dispatch-gates --ran reconciled 87 derived / 87 run / 0 NOT-MEASURED, every exit 0.


Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_016WV4cyHFdayr8QnKE8nszT

…ifestSchema before any upload

`os plugin publish` and `readOspluginManifest` read the artifact's
objectstack.plugin.json with a bare JSON.parse, so a stale artifact (for
example one carrying the retired `packaging: 'manifest-deps'`) reached the
registry and failed only at its consumer's install. Both now ask
`readCompiledPluginManifest`, the helper `os plugin install` and the boot
loader use, and publish refuses an invalid manifest with the schema's issues
before any request is sent. The uploaded plugin_manifest stays the
artifact's own manifest, without the defaults the parse stamps.

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/protocol/kernel/lifecycle.mdx (via os plugin publish (command, read off packages/cli/src/commands/plugin/publish.ts))
  • content/docs/protocol/kernel/plugin-spec.mdx (via os plugin publish (command, read off packages/cli/src/commands/plugin/publish.ts))

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

  • content/docs/releases/v17/17-6.mdx (via os plugin publish (command, read off packages/cli/src/commands/plugin/publish.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
  • 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 — 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 d770b1505e5b172c5a0cea2c22878067da2966b2 → packageMentionDocs.

Which tree this was computed on

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

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

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

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

Contract-review-claim: session_016WV4d1Np3hU7RQnA13YZcL · head bd6d77e · 2026-10-30T03:27Z

@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: bd6d77e29bc0479703070e9407f5fca7795349c6
Local-runs: none
Scope: packages/cli/src/commands/plugin/publish.ts, packages/cli/src/utils/osplugin.ts, packages/spec/src/kernel/manifest-packaging-retirement.test.ts, packages/cli/test/plugin-publish.test.ts, docs/qa/platform-checklist/areas/cli.json + packages/runtime/src/compiled-plugin-manifest.ts, packages/spec/src/kernel/manifest.zod.ts, packages/cli/src/commands/plugin/install.ts, packages/core/src/security/plugin-artifact-integrity.ts, packages/cli/src/commands/plugin/build.ts, packages/cli/src/commands/plugin/sign.ts

Face: narrowing (no path hits; the Clause-②: no (narrowing) line on the PR body and the changeset decides it). On this face the floor is every diff path outside the prose faces, so the five non-prose paths above are all in scope; the one prose path, .changeset/27810-plugin-publish-parses-compiled-manifest.md, is the lane seat's ACCEPT at TIER_DEFAULT and is read here only for ②. Inputs: card #27810 (body + both comments, 6512268493 and 6513168362), PR #27910 (body, file list, net diff origin/main d770b150…bd6d77e2, merge-base = origin/main), and the head's check-runs. Cited code read on origin/main at d770b150, never the stale local checkout.

Check-runs on the head at 2026-10-30T03:37Z: every completed, non-skipped run is success — among them Lint & Repo Gates, Build Core, Governed Surface Queue Guard, Check Changeset, Lint · ESLint, Lint · repo & tooling gates, Lint · CI, release & engine gates, Lint · docs & code guards, Lint · PM dispatch-gates, Type Check · source gates, Type Check · consumer gates, Type Check · debt ledger, Build & E2E UI, Live E2E UI, Test UI, Spec property liveness, Check PR Size, PR-body guards. Still in_progress (pending, not a pass): Test Core (1/8…8/8), Type Check · workspace, Type Check UI. This PASS judges the diff; the landing still waits for those to conclude success (Multi-agent discipline §7), and a red among them re-opens this record.

① Derived judgments

  1. Accept-set narrowing at os plugin publish — right, and exactly the card's ask. main decoded the artifact's objectstack.plugin.json with a bare JSON.parse and checked only id/version presence and that integrity was an object. The head hands readTarGz's entries to readCompiledPluginManifest (packages/runtime/src/compiled-plugin-manifest.ts, the helper PR fix(cli)!: os plugin install and the boot loader parse the compiled manifest through ManifestSchema #27791 gave install.ts line 302 and installed-plugin-loader.ts line 363) and refuses on ok: false before the signature read, the cloud-config read and both POSTs. The refused set is precisely what ManifestSchema (strictObject) refuses: a retired packaging: 'manifest-deps' (with its enumWithRetiredValues prescription), an unknown key, a missing name/type/version, a non-semver version, an integrity that is not a string→string record (PluginIntegritySchema), and integrity: null (which main let through to verifyIntegrity's permissive null branch). Each of these is a tightening a publisher meets at their own terminal instead of at a consumer's install — the business reason on the card.
  2. No second copy of the rule — right. Neither face file states a manifest rule. The hand-rolled malformed-integrity branch is deleted, and verifyIntegrity(archiveFiles, manifest.integrity, …) now takes the schema-typed Record<string,string> | undefined, matching the signature at plugin-artifact-integrity.ts line 75. Artifact manifest is missing id or version. survives and is reachable only through an empty --manifest-id (schema version is a required regex-matched string; id is required), as the body says.
  3. Payload of a valid publish unchanged — right. plugin_manifest is the artifact's own JSON (re-decoded from the same entry), as on main; scope/defaultDatasource are not stamped in. The pin uploads the manifest the artifact carries, as it carries it holds this. osplugin: base64, artifact_checksum, the signature sidecar, auth precedence, visibility handling: untouched.
  4. readOspluginManifest sync → async, return type Record<string, unknown> → ObjectStackManifestParsed — an internal change, not a public-surface change. packages/cli/package.json exports names ., ./console, ./hook-body; src/index.ts re-exports commands only; utils/osplugin.ts is on no entry. Its sole caller on origin/main is packages/cli/test/plugin-publish.test.ts (both pins updated to await). So Clause-②: no is right: nothing published widens. (Contrast PR fix(cli)!: os plugin install and the boot loader parse the compiled manifest through ManifestSchema #27791, which declared yes (narrowing) because it added the readCompiledPluginManifest export; this PR adds none.)
  5. Spec guard EXCLUDED entry — right and necessary. The publish pins author packaging: 'manifest-deps' both in a spread literal and in two it(...) titles (double-quoted strings, not inline code, so stripInlineCode does not mask them); the walk's AUTHORING regex matches. The entry sits beside the install door's entry with the same one-line job statement, which is what the "⛔ NOT an allowlist file" rule asks of a structural exclusion. The retirement itself is not weakened: PluginPackagingSchema.options stays ['bundled'].
  6. Fresh artifacts keep publishing. build.ts line 361 compiles { ...manifest, main: 'dist/index.mjs', integrity } from a manifest the build already parsed through ManifestSchema; main and integrity are declared keys, so a current os plugin build output passes the strict parse (the same fact fix(cli)!: os plugin install and the boot loader parse the compiled manifest through ManifestSchema #27791 relied on at install/boot).
  7. The PR body's "gate readings are in the os-dev-report comment on 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" — no such comment exists (the card has exactly two comments, triage and claim; the PR has docs-drift and the review claim). This record therefore relies on the head's check-runs alone, which is what the brief prescribes anyway. Not a tree defect; noted so the lane seat does not cite a comment that is not there.

② Semver level

  • .changeset/27810-plugin-publish-parses-compiled-manifest.md: '@objectstack/cli': minor, fix(cli)!, BREAKING banner, one adr-0087: not-required (no-migration-prescription) marker, Clause-②: no (narrowing), Metadata-protocol: narrowed. The only package the diff moves is @objectstack/cli (packages/spec moves a test file only, packages/runtime is untouched) — right.
  • Level per ruling A (check-changeset-no-major.mjs): a declared kind (narrowed) ⇒ at least one moved package minor+ — cli is minor. ✓. narrowed beside a (narrowing) arm — no contradiction. ✓ Check Changeset is success on the head.
  • Metadata-protocol: narrowed is the honest kind: a compiled-manifest value a publisher could carry through this door yesterday (manifest-deps, an unknown key, a null integrity map) is refused today — "a value refused or a requirement tightened is narrowed". It is also the kind the same family's PR fix(cli)!: os plugin install and the boot loader parse the compiled manifest through ManifestSchema #27791 declared for install and boot.
  • Clause-②: no (narrowing): the diff narrows a published accept set (the publish door) and widens no public surface (①.4) — right. (narrowing) is the BREAKING signal; the banner and ADR-0087 disposition are present.
  • ADR-0087 not-required (no-migration-prescription) is the right disposition: no key is added, removed, renamed or re-shaped; the remedy is "fix the source manifest as the refusal says, rebuild, re-sign" — a rebuild instruction, not a FROM → TO metadata migration, and no tombstone is owed (the manifest-deps tombstone already exists on PluginPackagingSchema). Same disposition as fix(cli)!: os plugin install and the boot loader parse the compiled manifest through ManifestSchema #27791, which the gate accepted.
  • The PR body carries the Skills-docs: none — REASON line the narrowed kind owes.

③ Boundary flags

Dev flag (1) — plugin_manifest uploaded verbatim, with a second decode of the entry. Answered: accepted, with one should-fix (④.1). The read is right on substance: ManifestSchema stamps defaultDatasource: 'default' (manifest.zod.ts line 581) and scope: 'project' (line 636) at parse, so uploading read.manifest would change the declared manifest on every valid publish relative to main's payload and relative to the bytes inside the signed artifact; that is not within the narrowing this PR declares (the card asks for a refusal, not a payload change), so keeping the artifact's own JSON is correct. The signature is over the whole artifact blob (sign.ts line 83, signPayload(artifact, …)), so the verbatim payload is not what the cloud's signature check hangs on — but the invariant "declared manifest == the manifest the blob carries" is still the sane one and the pin holds it. The second JSON.parse reads no rule and cannot disagree with the helper's decode (same bytes). What is weak is the shape, not the semantics: see ④.1.

Dev flag (2) — readOspluginManifest async with a lazy import('@objectstack/runtime'). Answered: accepted. osplugin.ts is imported by build.ts, sign.ts, install.ts and publish.ts; oclif's pattern strategy loads only the invoked command module, so a top-level import of the runtime root in the shared util would charge os plugin build and os plugin sign — which never read a compiled manifest back — a load install and publish already pay (both import @objectstack/runtime statically). The lazy import confines the cost to the one call that needs it; @objectstack/runtime and @objectstack/spec are dependencies of the CLI, the ./kernel subpath the type import uses exists, and ESLint (success on the head) carries no restriction on ImportExpression. The function is internal (①.4), so async is a change its one caller absorbed in this diff. The ~1.2 s figure is the dev's measurement, not re-measured here; the design does not depend on the exact number.

No open_questions on the card. The card's own boundary ("no edit to the helper's own module unless the measurement demands it") was honoured: compiled-plugin-manifest.ts is untouched — which is also why ④.1 is a should-fix and not a defect of this PR.

④ Design judgment

packages/cli/src/commands/plugin/publish.ts — holds. Read against the helper's contract: CompiledPluginManifestRead is { ok: true; manifest } or { ok: false; reason: 'unreadable' | 'invalid'; message; issues }; the command maps unreadable to the pre-existing Cannot read manifest from artifact: sentence (the helper's message for a missing entry is objectstack.plugin.json not found, so the old absent-manifest wording is preserved) and invalid to one • path: message line per issue plus the rebuild/re-sign remedy, then this.exit(1). The helper never throws (it catches internally), so leaving it outside the readTarGz try-block is correct. Ordering is as the card asks: the refusal precedes the integrity preflight, the .sig read, tryReadCloudConfig and both postJson calls; the pins count fetch at 0. Scenario tried: an artifact whose manifest is valid but whose integrity digests are stale — still refused by the integrity preflight, unchanged. Artifact with no integrity map — still a permissive skip (.optional()), unchanged. --manifest-id override still wins over manifest.id. Zod issue order for the drifted pin (name before integrity) follows strictObject key order (line 674 before line 1106), so the regex pin is deterministic.

  1. should-fix (follow-up, not blocking): the verbatim payload is produced by JSON.parse(Buffer.from(archiveFiles.find((f) => f.path === MANIFEST_FILENAME)!.data)…) — a non-null assertion and a second decode, guaranteed today only because MANIFEST_FILENAME (cli) and COMPILED_PLUGIN_MANIFEST_FILE (runtime) are two constants spelling the same string, and the helper found the entry. If they ever drift, the ! turns a refusal into a TypeError. The sustainable shape is for readCompiledPluginManifest to hand back the raw document it already decoded (or the entry it found) so the door decodes once and asserts nothing; that edit was outside this card's file surface by dispatch, so it is a one-line follow-up card against packages/runtime/src/compiled-plugin-manifest.ts, not a FAIL here.

packages/cli/src/utils/osplugin.ts — holds. readOspluginManifest now returns the helper's parsed manifest and throws read.message for both unreadable and invalid; the /not found/ pin and the two new prescription pins read that message. The module stays free of oclif and filesystem, as its header promises. Tried to break it: a caller awaiting the old sync return — none exists outside the updated test (①.4). The lazy import under tsup/vitest resolves the workspace package the same way install.ts's static import does.

packages/spec/src/kernel/manifest-packaging-retirement.test.ts — holds. One EXCLUDED entry, placed beside the install door's entry, with a one-line job statement in the same voice. The walk's anti-vacuity floors (visited > 1000, mentioning > 3) are unaffected by excluding one more file. The exclusion covers only packages/cli/test/plugin-publish.test.ts; publish.ts and osplugin.ts themselves spell the value only in prose comments and inline code (`packaging: 'manifest-deps'`), which stripInlineCode removes — so the production files stay under the walk, as they should.

packages/cli/test/plugin-publish.test.ts — holds. Five new pins and two updated: the two door refusals count fetch calls (0) and match the prescription on a • line; the verbatim-payload pin is toEqual(manifest) plus two not.toHaveProperty on the stamped defaults, so it reds if the parsed value is ever uploaded; the readOspluginManifest pins cover unreadable and both invalid shapes. The fixture manifest (id, name, version, type, runtime, packaging: 'bundled', main, permissions, integrity) is a manifest ManifestSchema accepts, so the happy path is real, not vacuous.

docs/qa/platform-checklist/areas/cli.json — holds. Item cli.plugin-manifest-build-contract revision 6 → 7: the compiled-manifest clause and its verify step now name the schema refusal sentence that publish.ts prints and the version: … issue a version-less manifest yields; the publish.ts#version source row names readCompiledPluginManifest and the empty --manifest-id refusal; a history entry with ref: "#27810" follows the revision-6 entry's shape. Every sentence in it is true of the head.

Verdict on the narrowing itself, against the published spec on main: it takes no declared capability away at the consumer — it makes ManifestSchema's own refusal real at the third and last door that reads a compiled manifest, so the three doors (publish, install, boot) now agree, and nothing a valid os plugin build produces is refused. That is Prime Directive #12 ("reject it at authoring/publish so the error surfaces loudly") done the way #10 asks.

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

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

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

维护者速读

  • 改了什么:os plugin publish 用安装和启动已在用的 readCompiledPluginManifest 按 ManifestSchema 校验 .osplugin 的编译清单,不合格就在任何网络请求前拒绝并给处方。
  • 为什么改:以前发布只做 JSON.parse,带已退役键(如 packaging: 'manifest-deps')的包能传上市场,要到用户安装时才被拒(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)。
  • 风险与回滚:收窄(Clause-②: no (narrowing)):不合 schema 的清单从此发不出去;合法包照常发布,上传的 plugin_manifest 仍是包内原样 JSON。回滚即还原本 PR。
  • 先看:publish.ts 第 2 步,再看 osplugin.ts 的 readOspluginManifest。
  • 两分钟验证:plugin-publish.test.ts 新增 5 例;还原两处源码后 4 例拒绝钉子变红。
  • 需要你做的:无(非受管;契约复审 PASS 6514352615;席位按本任落地授权落地)。

席位意见:ACCEPT。

席位核对

Head-sha: bd6d77e29bc0479703070e9407f5fca7795349c6

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 30, 2026 03:56
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 30, 2026 03:56
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 30, 2026
Merged via the queue into main with commit ed0e71d Oct 30, 2026
51 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-m87tmz branch October 30, 2026 04:32
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