Skip to content

Commit 9409792

Browse files
committed
fix(plugins): address review feedback on trust-pinning
- Update install.npm-spec.test.ts: the existing it.each table installed bare @openclaw/* specs (with spawn payloads) and asserted ok: true via the trusted-source bypass. With the new pinning + integrity gate those fixtures must use exact-version specs and pass expectedIntegrity to keep proving the legitimate trust path. Add a companion negative it.each that asserts the scanner now blocks the bare/dist-tag/ no-integrity variants. - Replace the misleading provider-install-catalog.ts citation in the install.ts code comment; that policy is not on current main. - Add an Unreleased Fixes changelog entry under Plugins/install. Verification: - pnpm test src/plugins/install.npm-spec.test.ts src/plugins/install.test.ts -> 113/113 pass - pnpm exec oxfmt --check --threads=1 src/plugins/install.ts src/plugins/install.test.ts src/plugins/install.npm-spec.test.ts CHANGELOG.md -> clean - node scripts/run-oxlint.mjs src/plugins/install.ts src/plugins/install.test.ts src/plugins/install.npm-spec.test.ts -> 0 warnings 0 errors - pnpm tsgo:core -> clean
1 parent 182050b commit 9409792

3 files changed

Lines changed: 58 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai
1818

1919
### Fixes
2020

21+
- Plugins/install: require an exact-version pin and a non-empty `expectedIntegrity` before granting the trusted-source security-scan bypass for official `@openclaw/*` npm plugin installs, so a name-only or `@latest` spec can no longer skip the install scanner if the upstream scope is ever compromised. Thanks @fede-kamel.
2122
- Channels/secrets: resolve SecretRef-backed channel credentials through external plugin secret contracts after the plugin split, covering runtime startup, target discovery, webhook auth, disabled-account enumeration, and late-bound web_search config. (#76449) Thanks @joshavant.
2223
- Docker/Gateway: pass Docker setup `.env` values into gateway and CLI containers and preserve exec SecretRef `passEnv` keys in managed service plans, so 1Password Connect-backed Discord tokens keep resolving after doctor or plugin repair. Thanks @vincentkoc.
2324
- Control UI/WebChat: explain compaction boundaries in chat history and link directly to session checkpoint controls so pre-compaction turns no longer look silently lost after refresh. Fixes #76415. Thanks @BunsDev.

src/plugins/install.npm-spec.test.ts

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -344,33 +344,34 @@ describe("installPluginFromNpmSpec", () => {
344344

345345
it.each([
346346
{
347-
spec: "@openclaw/acpx",
347+
packageName: "@openclaw/acpx",
348348
pluginId: "acpx",
349349
indexJs: `import { spawn } from "node:child_process";\nspawn("codex-acp", []);`,
350350
},
351351
{
352-
spec: "@openclaw/codex",
352+
packageName: "@openclaw/codex",
353353
pluginId: "codex",
354354
indexJs: `import { spawn } from "node:child_process";\nspawn("codex", ["app-server"]);`,
355355
},
356356
{
357-
spec: "@openclaw/google-meet",
357+
packageName: "@openclaw/google-meet",
358358
pluginId: "google-meet",
359359
indexJs: `import { spawnSync } from "node:child_process";\nspawnSync("node", ["bridge.js"]);`,
360360
},
361361
{
362-
spec: "@openclaw/voice-call",
362+
packageName: "@openclaw/voice-call",
363363
pluginId: "voice-call",
364364
indexJs: `import { spawn } from "node:child_process";\nspawn("ngrok", ["http", "3000"]);`,
365365
},
366366
])(
367-
"allows official npm plugin $spec with reviewed launch code",
368-
async ({ spec, pluginId, indexJs }) => {
367+
"allows pinned official npm plugin $packageName with reviewed launch code when integrity is provided",
368+
async ({ packageName, pluginId, indexJs }) => {
369369
const npmRoot = path.join(suiteTempRootTracker.makeTempDir(), "npm");
370370
const warnings: string[] = [];
371+
const spec = `${packageName}@2026.5.2`;
371372
mockNpmViewAndInstall({
372373
spec,
373-
packageName: spec,
374+
packageName,
374375
version: "2026.5.2",
375376
pluginId,
376377
npmRoot,
@@ -380,6 +381,7 @@ describe("installPluginFromNpmSpec", () => {
380381
const result = await installPluginFromNpmSpec({
381382
spec,
382383
npmDir: npmRoot,
384+
expectedIntegrity: "sha512-plugin-test",
383385
logger: {
384386
info: () => {},
385387
warn: (msg: string) => warnings.push(msg),
@@ -404,6 +406,49 @@ describe("installPluginFromNpmSpec", () => {
404406
},
405407
);
406408

409+
it.each([
410+
{
411+
label: "bare spec (no version selector)",
412+
spec: "@openclaw/codex",
413+
expectedIntegrity: "sha512-plugin-test" as string | undefined,
414+
},
415+
{
416+
label: "dist-tag spec resolves to latest",
417+
spec: "@openclaw/codex@latest",
418+
expectedIntegrity: "sha512-plugin-test" as string | undefined,
419+
},
420+
{
421+
label: "exact version without expectedIntegrity",
422+
spec: "@openclaw/codex@2026.5.2",
423+
expectedIntegrity: undefined,
424+
},
425+
])(
426+
"blocks official npm plugin install with reviewed launch code when trust prerequisites are missing: $label",
427+
async ({ spec, expectedIntegrity }) => {
428+
const npmRoot = path.join(suiteTempRootTracker.makeTempDir(), "npm");
429+
mockNpmViewAndInstall({
430+
spec,
431+
packageName: "@openclaw/codex",
432+
version: "2026.5.2",
433+
pluginId: "codex",
434+
npmRoot,
435+
indexJs: `import { spawn } from "node:child_process";\nspawn("codex", ["app-server"]);`,
436+
});
437+
438+
const result = await installPluginFromNpmSpec({
439+
spec,
440+
npmDir: npmRoot,
441+
...(expectedIntegrity ? { expectedIntegrity } : {}),
442+
logger: { info: () => {}, warn: () => {} },
443+
});
444+
445+
expect(result.ok).toBe(false);
446+
if (!result.ok) {
447+
expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_BLOCKED);
448+
}
449+
},
450+
);
451+
407452
it("rejects non-registry npm specs", async () => {
408453
const result = await installPluginFromNpmSpec({ spec: "github:evil/evil" });
409454
expect(result.ok).toBe(false);

src/plugins/install.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -209,11 +209,11 @@ function isTrustedOfficialNpmPluginInstall(params: {
209209
if (!requested) {
210210
return false;
211211
}
212-
// Match the policy enforced for trusted catalog installs in
213-
// src/plugins/provider-install-catalog.ts (require exact-version pin and
214-
// a non-empty expectedIntegrity hash). Without these, a name-only spec
215-
// would resolve to npm's "latest" tag and bypass the security scan if the
216-
// upstream package is ever compromised.
212+
// Trusting a name-only or dist-tag spec means npm resolves it to "latest"
213+
// at install time, and the scan bypass would also cover a future malicious
214+
// release if the upstream @openclaw scope is ever compromised. Require the
215+
// caller to have committed to a specific resolved version + integrity hash
216+
// before granting the trusted-source scan bypass.
217217
if (requested.selectorKind !== "exact-version") {
218218
return false;
219219
}

0 commit comments

Comments
 (0)