fix(security): harden npm plugin and hook install integrity flow

This commit is contained in:
Peter Steinberger
2026-02-19 15:10:57 +01:00
parent 2777d8ad93
commit 5dc50b8a3f
23 changed files with 1047 additions and 183 deletions

View File

@@ -1,9 +1,10 @@
import type { Command } from "commander";
import fs from "node:fs";
import fsp from "node:fs/promises";
import path from "node:path";
import type { Command } from "commander";
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
import type { OpenClawConfig } from "../config/config.js";
import type { HookEntry } from "../hooks/types.js";
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
import { loadConfig, writeConfigFile } from "../config/io.js";
import {
buildWorkspaceHookStatus,
@@ -16,7 +17,6 @@ import {
resolveHookInstallDir,
} from "../hooks/install.js";
import { recordHookInstall } from "../hooks/installs.js";
import type { HookEntry } from "../hooks/types.js";
import { loadWorkspaceHookEntries } from "../hooks/workspace.js";
import { resolveArchiveKind } from "../infra/archive.js";
import { buildPluginStatusReport } from "../plugins/status.js";
@@ -26,6 +26,7 @@ import { renderTable } from "../terminal/table.js";
import { theme } from "../terminal/theme.js";
import { resolveUserPath, shortenHomePath } from "../utils.js";
import { formatCliCommand } from "./command-format.js";
import { promptYesNo } from "./prompt.js";
export type HooksListOptions = {
json?: boolean;
@@ -550,7 +551,8 @@ export function registerHooksCli(program: Command): void {
.description("Install a hook pack (path, archive, or npm spec)")
.argument("<path-or-spec>", "Path to a hook pack or npm package spec")
.option("-l, --link", "Link a local path instead of copying", false)
.action(async (raw: string, opts: { link?: boolean }) => {
.option("--pin", "Record npm installs as exact resolved <name>@<version>", false)
.action(async (raw: string, opts: { link?: boolean; pin?: boolean }) => {
const resolved = resolveUserPath(raw);
const cfg = loadConfig();
@@ -658,13 +660,29 @@ export function registerHooksCli(program: Command): void {
}
let next = enableInternalHookEntries(cfg, result.hooks);
const resolvedSpec = result.npmResolution?.resolvedSpec;
const recordSpec = opts.pin && resolvedSpec ? resolvedSpec : raw;
if (opts.pin && !resolvedSpec) {
defaultRuntime.log(
theme.warn("Could not resolve exact npm version for --pin; storing original npm spec."),
);
}
if (opts.pin && resolvedSpec) {
defaultRuntime.log(`Pinned npm install record to ${resolvedSpec}.`);
}
next = recordHookInstall(next, {
hookId: result.hookPackId,
source: "npm",
spec: raw,
spec: recordSpec,
installPath: result.targetDir,
version: result.version,
resolvedName: result.npmResolution?.name,
resolvedVersion: result.npmResolution?.version,
resolvedSpec: result.npmResolution?.resolvedSpec,
integrity: result.npmResolution?.integrity,
shasum: result.npmResolution?.shasum,
resolvedAt: result.npmResolution?.resolvedAt,
hooks: result.hooks,
});
await writeConfigFile(next);
@@ -721,6 +739,18 @@ export function registerHooksCli(program: Command): void {
mode: "update",
dryRun: true,
expectedHookPackId: hookId,
expectedIntegrity: record.integrity,
onIntegrityDrift: async (drift) => {
const specLabel = drift.resolution.resolvedSpec ?? drift.spec;
defaultRuntime.log(
theme.warn(
`Integrity drift detected for "${hookId}" (${specLabel})` +
`\nExpected: ${drift.expectedIntegrity}` +
`\nActual: ${drift.actualIntegrity}`,
),
);
return true;
},
logger: createInstallLogger(),
});
if (!probe.ok) {
@@ -742,6 +772,18 @@ export function registerHooksCli(program: Command): void {
spec: record.spec,
mode: "update",
expectedHookPackId: hookId,
expectedIntegrity: record.integrity,
onIntegrityDrift: async (drift) => {
const specLabel = drift.resolution.resolvedSpec ?? drift.spec;
defaultRuntime.log(
theme.warn(
`Integrity drift detected for "${hookId}" (${specLabel})` +
`\nExpected: ${drift.expectedIntegrity}` +
`\nActual: ${drift.actualIntegrity}`,
),
);
return await promptYesNo(`Continue updating "${hookId}" with this artifact?`);
},
logger: createInstallLogger(),
});
if (!result.ok) {
@@ -756,6 +798,12 @@ export function registerHooksCli(program: Command): void {
spec: record.spec,
installPath: result.targetDir,
version: nextVersion,
resolvedName: result.npmResolution?.name,
resolvedVersion: result.npmResolution?.version,
resolvedSpec: result.npmResolution?.resolvedSpec,
integrity: result.npmResolution?.integrity,
shasum: result.npmResolution?.shasum,
resolvedAt: result.npmResolution?.resolvedAt,
hooks: result.hooks,
});
updatedCount += 1;