You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit ed0e71d
Browse filesBrowse the repository at this point in the historyBrowse files
fix(cli)!: os plugin publish parses the compiled manifest through ManifestSchema before any upload (#27910)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} -->
_Requested by **Elon** · [project
thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9D4WPVh2L9JrUnP8Q91s8oA4)_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
#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 #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](https://claude.ai/code/session_016WV4cyHFdayr8QnKE8nszT)_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_016WV4cyHFdayr8QnKE8nszT
Co-authored-by: Claude <noreply@anthropic.com>
fix(cli)!: `os plugin publish` parses the artifact's compiled manifest through `ManifestSchema` before anything is uploaded, so a stale `.osplugin` (e.g. `packaging: 'manifest-deps'`) is refused at the publisher's terminal with the schema's prescription instead of failing later at its consumer's install
6
+
7
+
Clause-②: no (narrowing)
8
+
Metadata-protocol: narrowed
9
+
10
+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped: `ManifestSchema` is consumed as it stands, and what changes is that a third door, `os plugin publish`, holds the compiled manifest inside an artifact to it, as `os plugin build` holds the source manifest and `os plugin install` and the boot-time loader hold the compiled one. The refusal is the schema's own message (for `manifest-deps`, the retirement prescription), and the fix is to the source manifest and a rebuild, so there is nothing new for a metadata migration to rewrite and no tombstone. -->
11
+
12
+
**BREAKING** accept-set narrowing at `os plugin publish` (`@objectstack/cli`).
13
+
14
+
-**Before.**`os plugin publish``JSON.parse`d the artifact's `objectstack.plugin.json` and checked only that `id` and `version` were present and that `integrity` was an object. An artifact built before a schema change uploaded: one carrying the retired `packaging: 'manifest-deps'` reached the registry and was refused only at its consumer's `os plugin install`. Any other compiled-manifest drift (a missing `name` or `type`, an unknown key) uploaded the same way.
15
+
-**Now.** The publish parses the compiled manifest with `readCompiledPluginManifest` (`@objectstack/runtime`), the one helper `os plugin install` and the boot-time loader already use, before any request leaves the machine.
16
+
- A manifest the schema refuses stops the publish with `The artifact's objectstack.plugin.json is not a valid plugin manifest, so nothing was published:` and one line per schema issue. For `manifest-deps` that line is the retirement prescription. Neither the package row nor the version is posted.
17
+
- The hand-rolled malformed-`integrity` refusal is gone: a malformed map is a schema issue now. `Artifact manifest is missing id or version.` is now reached only by an empty `--manifest-id`.
18
+
- A valid artifact publishes unchanged: the uploaded `plugin_manifest` is still the manifest the artifact carries, byte for byte, without the defaults the parse stamps (`scope`, `defaultDatasource`).
19
+
-`readOspluginManifest` (internal to the CLI) parses through the same helper and is now async.
20
+
-**Who sees it.** Anyone publishing an `.osplugin` whose compiled manifest `ManifestSchema` refuses: one built by an `os plugin build` from before a schema change, or one assembled by hand.
21
+
22
+
**The remedy.** Fix the plugin's source `objectstack.plugin.json` as the refusal says (for `manifest-deps`: delete the `packaging` key; `bundled` is the default and the only strategy), rebuild the artifact with `os plugin build`, re-sign it with `os plugin sign`, and publish it again. An artifact `os plugin build` produced from a valid manifest publishes unchanged.
Copy file name to clipboardExpand all lines: docs/qa/platform-checklist/areas/cli.json
+10-4Lines changed: 10 additions & 4 deletions
Original file line number
Diff line number
Diff line change
@@ -1716,7 +1716,7 @@
1716
1716
"title": "os plugin build enforces the manifest at the packaging boundary: an invalid manifest is refused with located errors, `bundled` is the one packaging strategy (the retired `manifest-deps` is refused at the manifest, a native addon is refused by name), and the compiled manifest the artifact carries is readable back",
1717
1717
"since": "v15",
1718
1718
"status": "active",
1719
-
"revision": 6,
1719
+
"revision": 7,
1720
1720
"priority": "P2",
1721
1721
"surface": "cli",
1722
1722
"personas": [
@@ -1772,9 +1772,9 @@
1772
1772
"evidence": "the three refusals with their exit codes and the listings taken after them"
1773
1773
},
1774
1774
{
1775
-
"clause": "the artifact carries a COMPILED manifest — the authored manifest plus `main: \"dist/index.mjs\"` and an `integrity` map of archive-relative path → sha256 SRI digest, with the manifest itself and SIGNATURE excluded from the map — and os plugin publish reads that manifest back out of the artifact BEFORE any network call, refusing 'Artifact manifest is missing id or version.' when it cannot",
1775
+
"clause": "the artifact carries a COMPILED manifest — the authored manifest plus `main: \"dist/index.mjs\"` and an `integrity` map of archive-relative path → sha256 SRI digest, with the manifest itself and SIGNATURE excluded from the map — and os plugin publish reads that manifest back out of the artifact BEFORE any network call and parses it through ManifestSchema (readCompiledPluginManifest, the parse os plugin install and the boot loader share), refusing one the schema refuses with 'The artifact's objectstack.plugin.json is not a valid plugin manifest, so nothing was published:' and one line per schema issue (a missing version reads `version: …`; a pre-retirement `manifest-deps` artifact reads the retirement prescription) when it cannot",
1776
1776
"oracle": "build",
1777
-
"verify": "read the objectstack.plugin.json entry out of each archive and diff it against the authored file; then run publish against a good artifact (expect it to reach the auth refusal, proving the manifest was read) and against one whose manifest lacks a version (expect the artifact-manifest refusal, before any auth). ⚠️ The publish leg is scored on WHICH refusal arrives, not on success",
1777
+
"verify": "read the objectstack.plugin.json entry out of each archive and diff it against the authored file; then run publish against a good artifact (expect it to reach the auth refusal, proving the manifest was read) and against one whose manifest lacks a version (expect the schema refusal naming `version`, before any auth). ⚠️ The publish leg is scored on WHICH refusal arrives, not on success",
1778
1778
"evidence": "the compiled manifest of each artifact, and the two publish outputs distinguished by their refusal"
1779
1779
},
1780
1780
{
@@ -1798,7 +1798,7 @@
1798
1798
"source": [
1799
1799
"packages/cli/src/commands/plugin/build.ts#formatZodErrors — manifest read + named refusal; ManifestSchema.safeParse + formatZodErrors; the explicit id/version guard; packaging default and the Loaded line; entry resolution; the bundled-only esbuild call (only `@objectstack/*` external); findNativeAddons and the native-addon refusal ahead of the integrity map; computeIntegrity + the compiled manifest; the artifact write and its printed block",
1800
1800
"packages/cli/src/utils/osplugin.ts#MANIFEST_FILENAME — MANIFEST_FILENAME = 'objectstack.plugin.json'; OSPLUGIN_EXT; computeIntegrity (manifest and SIGNATURE excluded, keys sorted); the ustar writer with mtime pinned to 0; readOspluginManifest",
1801
-
"packages/cli/src/commands/plugin/publish.ts#version — artifact discovery; readOspluginManifest before any network call; the id/version refusal; the unsigned-artifact notice; the auth refusal",
1801
+
"packages/cli/src/commands/plugin/publish.ts#version — artifact discovery; the compiled manifest parsed through readCompiledPluginManifest (ManifestSchema) before any network call and the schema refusal; the empty `--manifest-id` refusal; the unsigned-artifact notice; the auth refusal",
1802
1802
"packages/cli/src/commands/plugin/install.ts#PluginInstall — the install-time re-verification the integrity negative points at (verifyIntegrity over the unpacked entries, `integrity_mismatch`; a compiled manifest ManifestSchema refuses, a malformed integrity map included, is `manifest_invalid` one step earlier through readCompiledPluginManifest; nothing installed) and the install-time tier warning; not scored here",
1803
1803
"packages/core/src/security/plugin-artifact-integrity.ts#verifyIntegrity — the one integrity verifier, shared by the publish preflight and the install-time check; not scored here",
1804
1804
"packages/runtime/src/installed-plugin-loader.ts#dispatchPluginRuntime — the boot-time tier dispatch point the trust-tier negative points at (`node` only; `sandbox` / `worker` refused by name; an undeclared tier refused); not scored here",
@@ -1844,6 +1844,12 @@
1844
1844
"date": "2026-10-30",
1845
1845
"change": "one source row re-pointed, no clause changed: os plugin install no longer has an `integrity_malformed` refusal. The compiled manifest is parsed through ManifestSchema first (readCompiledPluginManifest, the parse install and the boot loader share), so a malformed integrity map is refused as `manifest_invalid` before the integrity step; `integrity_mismatch` is unchanged. Re-verified against install.ts and compiled-plugin-manifest.ts at this PR's head, no qa-run record",
1846
1846
"ref": "#27686"
1847
+
},
1848
+
{
1849
+
"revision": 7,
1850
+
"date": "2026-10-30",
1851
+
"change": "the compiled-manifest clause's publish half (clause and verify) and the publish.ts source row re-pointed: os plugin publish no longer hand-parses the compiled manifest. It parses it through ManifestSchema with readCompiledPluginManifest (the helper os plugin install and the boot loader share), so a manifest that lacks a version is refused as 'not a valid plugin manifest' with the schema's issue line (`version: …`), not 'Artifact manifest is missing id or version.' — that sentence is now reachable only from an empty `--manifest-id`. The uploaded plugin_manifest stays the artifact's own manifest, unchanged",
console.log('\n Fix the source manifest as above and rebuild the artifact with `os plugin build` (then re-sign it with `os plugin sign`), and publish the fresh artifact.');
132
+
}
133
+
this.exit(1);
134
+
return;
135
+
}
136
+
constmanifest=read.manifest;
137
+
// What is uploaded is the manifest the artifact carries, as it carries
138
+
// it: the parse above stamps the schema's defaults (`scope`,
139
+
// `defaultDatasource`), which would make the declared manifest differ
140
+
// from the signed bytes' own. The entry is there and is a JSON object:
0 commit comments