Skip to content

Commit 47c3e12

Browse files
fix(cli)!: os plugin install and the boot loader parse the compiled manifest through ManifestSchema (#27791)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DTirkaM35ZFp9q361oePFGn)_ 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](https://claude.ai/code/session_016WV4czY8bzmjKmuhj1GP9c)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016WV4czY8bzmjKmuhj1GP9c --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 359e935 commit 47c3e12

16 files changed

Lines changed: 305 additions & 56 deletions
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
'@objectstack/cli': minor
3+
'@objectstack/runtime': minor
4+
---
5+
6+
fix(cli)!: `os plugin install` and the boot-time loader parse an artifact's compiled manifest through `ManifestSchema`, so a stale `.osplugin` (e.g. `packaging: 'manifest-deps'`) is refused with the schema's prescription instead of failing at boot as `import_failed`
7+
8+
Clause-②: yes (narrowing)
9+
Metadata-protocol: narrowed
10+
11+
<!-- 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 two more doors hold the compiled manifest inside an artifact to it, as `os plugin build` already holds the source manifest. 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. -->
12+
13+
**BREAKING** accept-set narrowing at `os plugin install` (`@objectstack/cli`) and at the boot-time loader for installed plugins (`@objectstack/runtime`'s `loadInstalledPlugins` / `verifyInstalledPlugins`, run by `os serve`, `os dev` and `os start`).
14+
15+
- **Before.** Both doors `JSON.parse`d the artifact's `objectstack.plugin.json` and read only `id` and `version` (and `runtime`, `main`, `integrity`, `permissions` by hand). An artifact built before a schema change installed: one carrying the retired `packaging: 'manifest-deps'` installed, and at boot its externalized imports failed and it was refused as `import_failed`, without the retirement prescription. Any other compiled-manifest drift (a missing `name` or `type`, an unknown key) installed the same way.
16+
- **Now.** One helper, `readCompiledPluginManifest` (a new named export of `@objectstack/runtime`'s root entry, with its result type `CompiledPluginManifestRead`: the one widening in this change), parses the compiled manifest through `ManifestSchema` for both doors.
17+
- `os plugin install` refuses an invalid one before anything is unpacked, as `manifest_invalid`, listing the schema's issues (`--json`: `details`). For `manifest-deps` that is the retirement prescription. Nothing is written under the plugin home and no installation is recorded.
18+
- The boot loader refuses the plugin by name as `manifest_invalid` (a new `InstalledPluginRefusalCode`), with the schema's message, before anything of it is imported. It stays non-fatal: the boot goes on and every other installed plugin loads.
19+
- `os plugin install`'s `manifest_incomplete` and `integrity_malformed` refusals are gone: a missing `id` / `version` and a malformed `integrity` map are schema issues, refused as `manifest_invalid`.
20+
- **Also.** `os plugin build`'s native-addon refusal names, for a bundled package that builds or ships a native addon, the files that import it and the chain that reaches it from the plugin's own source (`src/index.ts → ws → bufferutil`), the way the `.node`-module line already names its importers. A transitive hit now traces to the direct dependency.
21+
- **Who sees it.** Anyone installing, or booting with, an `.osplugin` whose compiled manifest `ManifestSchema` refuses: one built by an `os plugin build` from before a schema change, or one assembled by hand.
22+
23+
**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) and rebuild the artifact with `os plugin build`, then sign and install it again. An artifact `os plugin build` produced from a valid manifest installs and boots unchanged.

‎docs/qa/platform-checklist/areas/cli.json‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1716,7 +1716,7 @@
17161716
"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",
17171717
"since": "v15",
17181718
"status": "active",
1719-
"revision": 5,
1719+
"revision": 6,
17201720
"priority": "P2",
17211721
"surface": "cli",
17221722
"personas": [
@@ -1799,7 +1799,7 @@
17991799
"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",
18001800
"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",
18011801
"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",
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` / `integrity_malformed`, nothing installed) and the install-time tier warning; not scored here",
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",
18031803
"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",
18041804
"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",
18051805
"packages/spec/src/kernel/manifest.zod.ts#ManifestSchema — ManifestSchema id and version (both required, version regex-pinned); PluginPackagingSchema ('bundled' only — 'manifest-deps' is a retired value refused with its migration prescription, ADR-0025 v2 §3.3); PluginRuntimeSchema (the trust tier, ADR-0025 §3.6)",
@@ -1838,6 +1838,12 @@
18381838
"date": "2026-10-29",
18391839
"change": "the integrity and trust-tier negatives re-pointed, no clause changed: both said nothing in this repo verifies or dispatches, and both stopped being true with the install loop — os plugin install re-verifies the integrity map at unpack (install.ts#PluginInstall through verifyIntegrity, slice 1, 10f27b6508) and the boot-time loader dispatches on `runtime` before any import (installed-plugin-loader.ts#dispatchPluginRuntime, slice 2, d4ff1a025f). Each negative now names that code as the check this item does NOT score, in place of the tracker numbers it cited, which do not resolve in this repository; three source rows added for the code they point at. The scored clauses and steps are unchanged. Re-verified against the source at 86f2afeb23, no qa-run record",
18401840
"ref": "#27685"
1841+
},
1842+
{
1843+
"revision": 6,
1844+
"date": "2026-10-30",
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+
"ref": "#27686"
18411847
}
18421848
]
18431849
},

‎packages/cli/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ A code-bearing plugin — a directory carrying an `objectstack.plugin.json` mani
155155
| `os plugin disable <id>` / `os plugin enable <id>` | Keep an installed plugin from loading at the next boot, or load it again |
156156
| `os plugin uninstall <id>` | Mark an installed plugin for removal: the next boot loads nothing of it and removes its directory |
157157

158-
`os plugin install` refuses, by name, an unknown or revoked key, a signature over other bytes, a missing signature and an integrity mismatch. The next `os serve` (or `os dev` / `os start`) re-verifies every installation before importing any of its code — the kept artifact's sha256, its signature against the trusted keys as they are then (a key revoked since install refuses it), and the unpacked tree — and loads it by its declared `runtime` tier: `node` in process (author code in the host process, not isolated); `sandbox` and `worker` are refused by name until ADR-0025 M3 routes them, and a plugin that declares no tier is refused. A refused plugin does not stop the boot; each refusal is one `✗` line naming why. `os plugin` (singular) is unrelated to `os plugins` (plural), oclif's plugin manager, which this package does not ship — see [`os plugins` and `os help`](#os-plugins-and-os-help-not-commands).
158+
`os plugin install` refuses, by name, an unknown or revoked key, a signature over other bytes, a missing signature, a compiled `objectstack.plugin.json` that `ManifestSchema` refuses (`manifest_invalid`, with the schema's own message: for a retired value such as `packaging: 'manifest-deps'`, its prescription) and an integrity mismatch. The next `os serve` (or `os dev` / `os start`) re-verifies every installation before importing any of its code — the kept artifact's sha256, its signature against the trusted keys as they are then (a key revoked since install refuses it), and the unpacked tree — and loads it by its declared `runtime` tier: `node` in process (author code in the host process, not isolated); `sandbox` and `worker` are refused by name until ADR-0025 M3 routes them, and a plugin that declares no tier is refused. A refused plugin does not stop the boot; each refusal is one `✗` line naming why. `os plugin` (singular) is unrelated to `os plugins` (plural), oclif's plugin manager, which this package does not ship — see [`os plugins` and `os help`](#os-plugins-and-os-help-not-commands).
159159

160160
### Quality
161161

‎packages/cli/src/commands/plugin/build.ts‎

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,12 +137,50 @@ function nativePackageReason(dir: string): string | undefined {
137137
return shipped ? `ships a compiled addon (${shipped})` : undefined;
138138
}
139139

140+
/**
141+
* Who reaches the package at `dir` (a {@link packageRootOf} directory): the
142+
* files outside it that import a file inside it, and the shortest chain from
143+
* the plugin's own source to it, one step per package — so a transitive hit
144+
* (`ws` → `bufferutil`) names the direct dependency that pulled it in.
145+
*/
146+
function packageImporters(metafile: Metafile, dir: string): { importers: string[]; chain: string[] } {
147+
const inputs = Object.keys(metafile.inputs);
148+
const inside = (input: string): boolean => packageRootOf(input)?.dir === dir;
149+
const importers = inputs.filter(
150+
(i) => !inside(i) && metafile.inputs[i]!.imports.some((imp) => inside(imp.path)),
151+
);
152+
// Breadth-first from the plugin's own files (outside every `node_modules`).
153+
const from = new Map<string, string | null>();
154+
const queue = inputs.filter((i) => packageRootOf(i) === undefined);
155+
for (const start of queue) from.set(start, null);
156+
let hit: string | undefined;
157+
while (queue.length > 0 && hit === undefined) {
158+
const input = queue.shift()!;
159+
for (const imp of metafile.inputs[input]?.imports ?? []) {
160+
if (from.has(imp.path) || !(imp.path in metafile.inputs)) continue;
161+
from.set(imp.path, input);
162+
if (inside(imp.path)) {
163+
hit = imp.path;
164+
break;
165+
}
166+
queue.push(imp.path);
167+
}
168+
}
169+
const chain: string[] = [];
170+
for (let at: string | undefined = hit; at; at = from.get(at) ?? undefined) {
171+
const step = packageRootOf(at)?.name ?? at;
172+
if (chain[0] !== step) chain.unshift(step);
173+
}
174+
return { importers, chain };
175+
}
176+
140177
/**
141178
* Every native addon a build would pack or load (ADR-0025 v2 §3.3), one line
142179
* each: a `.node` module the bundle reaches (named with its importers), a
143180
* bundled package that builds or ships one (its JS may be bundleable, but the
144181
* binary it locates at run time — `bindings`, `node-gyp-build` — is not in the
145-
* artifact), and a `.node` file staged from `assets/`.
182+
* artifact; named with its importers and the chain that reaches it from the
183+
* plugin's own source), and a `.node` file staged from `assets/`.
146184
*/
147185
function findNativeAddons(metafile: Metafile, cwd: string, assets: ArchiveFile[]): string[] {
148186
const found: string[] = [];
@@ -158,7 +196,13 @@ function findNativeAddons(metafile: Metafile, cwd: string, assets: ArchiveFile[]
158196
}
159197
for (const { name, dir } of packages.values()) {
160198
const reason = nativePackageReason(resolvePath(cwd, dir));
161-
if (reason) found.push(`package \`${name}\` (${dir}) ${reason}`);
199+
if (!reason) continue;
200+
const { importers, chain } = packageImporters(metafile, dir);
201+
found.push(
202+
`package \`${name}\` (${dir}) ${reason}`
203+
+ (importers.length > 0 ? `, imported by ${importers.join(', ')}` : '')
204+
+ (chain.length > 1 ? ` (reached through ${chain.join(' → ')})` : ''),
205+
);
162206
}
163207
for (const asset of assets) {
164208
if (isNativeAddonPath(asset.path)) found.push(asset.path);

‎packages/cli/src/commands/plugin/install.integration.test.ts‎

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,15 +109,17 @@ const KEY_ID = 'acme-2026';
109109
const text = (t: string) => new TextEncoder().encode(t);
110110

111111
/** A `.osplugin` built as `os plugin build` builds one, its integrity map computed unless given. */
112-
function artifact(name: string, opts: { id: string; version?: string; runtime?: string; files?: ArchiveFile[]; integrity?: Record<string, string>; dir?: string }): string {
112+
function artifact(name: string, opts: { id: string; version?: string; runtime?: string; files?: ArchiveFile[]; integrity?: Record<string, string>; dir?: string; manifest?: Record<string, unknown> }): string {
113113
const files: ArchiveFile[] = opts.files ?? [{ path: 'dist/index.mjs', data: text(`export const id = ${JSON.stringify(opts.id)};\n`) }];
114114
const manifest = {
115115
id: opts.id,
116116
version: opts.version ?? '1.0.0',
117+
name: 'Acme',
117118
type: 'plugin',
118119
runtime: opts.runtime ?? 'node',
119120
packaging: 'bundled',
120121
integrity: opts.integrity ?? computeIntegrity(files),
122+
...opts.manifest,
121123
};
122124
const path = join(opts.dir ?? project, `${name}.osplugin`);
123125
writeFileSync(path, createTarGz([...files, { path: MANIFEST_FILENAME, data: text(JSON.stringify(manifest)) }]));
@@ -330,6 +332,31 @@ describe('os plugin install (ADR-0025 §3.5 M1)', () => {
330332
expect(existsSync(join(home, 'plugins', 'env_local', 'escape.txt'))).toBe(false);
331333
}, CASE_TIMEOUT_MS);
332334

335+
it("refuses a pre-retirement artifact (packaging: 'manifest-deps') with the schema's prescription, before unpacking anything", async () => {
336+
const path = artifact('retired', { id: 'com.acme.retired', manifest: { packaging: 'manifest-deps' } });
337+
sign(path);
338+
await trustPublisher();
339+
const result = await run(PluginInstall, [path, ...db(), '--json']);
340+
expectRefused(result, 'manifest_invalid', 'com.acme.retired');
341+
// The schema's own words reach the operator: the retired value and its remedy.
342+
const details = (result.payload.details as string[]).join('\n');
343+
expect(details).toMatch(/^packaging: `packaging: 'manifest-deps'` was removed/m);
344+
expect(details).toMatch(/Delete the key/);
345+
// Nothing of it was written under the plugin home.
346+
expect(existsSync(join(home, 'plugins', 'env_local', 'com.acme.retired'))).toBe(false);
347+
}, CASE_TIMEOUT_MS);
348+
349+
it('refuses a compiled manifest ManifestSchema refuses, by name, naming each issue', async () => {
350+
const path = artifact('drifted', { id: 'com.acme.drifted', manifest: { name: undefined, integrity: ['not', 'a', 'map'] } });
351+
sign(path);
352+
await trustPublisher();
353+
const result = await run(PluginInstall, [path, ...db(), '--json']);
354+
expectRefused(result, 'manifest_invalid', 'com.acme.drifted');
355+
const details = (result.payload.details as string[]).join('\n');
356+
expect(details).toMatch(/^name: /m);
357+
expect(details).toMatch(/^integrity: /m);
358+
}, CASE_TIMEOUT_MS);
359+
333360
it('refuses an unsigned artifact, and installs it only under --trust-unsigned, saying so on stderr', async () => {
334361
const path = artifact('unsigned', { id: 'com.acme.unsigned' });
335362
expectRefused(await run(PluginInstall, [path, ...db(), '--json']), 'signature_missing', 'com.acme.unsigned');

0 commit comments

Comments
 (0)