mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-29 16:54:30 +00:00
fix(node-host): harden pnpm approval binding
This commit is contained in:
@@ -25,7 +25,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Config/web fetch: restore runtime validation for documented `tools.web.fetch.readability` and `tools.web.fetch.firecrawl` settings so valid web fetch configs no longer fail with unrecognized-key errors. (#42583) Thanks @stim64045-spec.
|
||||
- Signal/config validation: add `channels.signal.groups` schema support so per-group `requireMention`, `tools`, and `toolsBySender` overrides no longer get rejected during config validation. (#27199) Thanks @unisone.
|
||||
- Config/discovery: accept `discovery.wideArea.domain` in strict config validation so unicast DNS-SD gateway configs no longer fail with an unrecognized-key error. (#35615) Thanks @ingyukoh.
|
||||
|
||||
- Security/exec approvals: unwrap more `pnpm` runtime forms during approval binding, including `pnpm --reporter ... exec` and direct `pnpm node` file runs, with matching regression coverage and docs updates.
|
||||
## 2026.3.12
|
||||
|
||||
### Changes
|
||||
|
||||
@@ -271,6 +271,8 @@ Approval-backed interpreter/runtime runs are intentionally conservative:
|
||||
- Exact argv/cwd/env context is always bound.
|
||||
- Direct shell script and direct runtime file forms are best-effort bound to one concrete local
|
||||
file snapshot.
|
||||
- Common package-manager wrapper forms that still resolve to one direct local file (for example
|
||||
`pnpm exec`, `pnpm node`, `npm exec`, `npx`) are unwrapped before binding.
|
||||
- If OpenClaw cannot identify exactly one concrete local file for an interpreter/runtime command
|
||||
(for example package scripts, eval forms, runtime-specific loader chains, or ambiguous multi-file
|
||||
forms), approval-backed execution is denied instead of claiming semantic coverage it does not
|
||||
|
||||
@@ -40,6 +40,7 @@ type RuntimeFixture = {
|
||||
initialBody: string;
|
||||
expectedArgvIndex: number;
|
||||
binName?: string;
|
||||
binNames?: string[];
|
||||
};
|
||||
|
||||
function createScriptOperandFixture(tmp: string, fixture?: RuntimeFixture): ScriptOperandFixture {
|
||||
@@ -356,6 +357,20 @@ describe("hardenApprovedExecutionPaths", () => {
|
||||
initialBody: 'console.log("SAFE");\n',
|
||||
expectedArgvIndex: 3,
|
||||
},
|
||||
{
|
||||
name: "pnpm reporter exec tsx file",
|
||||
argv: ["pnpm", "--reporter", "silent", "exec", "tsx", "./run.ts"],
|
||||
scriptName: "run.ts",
|
||||
initialBody: 'console.log("SAFE");\n',
|
||||
expectedArgvIndex: 5,
|
||||
},
|
||||
{
|
||||
name: "pnpm reporter-equals exec tsx file",
|
||||
argv: ["pnpm", "--reporter=silent", "exec", "tsx", "./run.ts"],
|
||||
scriptName: "run.ts",
|
||||
initialBody: 'console.log("SAFE");\n',
|
||||
expectedArgvIndex: 4,
|
||||
},
|
||||
{
|
||||
name: "pnpm js shim exec tsx file",
|
||||
argv: ["./pnpm.js", "exec", "tsx", "./run.ts"],
|
||||
@@ -370,6 +385,22 @@ describe("hardenApprovedExecutionPaths", () => {
|
||||
initialBody: 'console.log("SAFE");\n',
|
||||
expectedArgvIndex: 4,
|
||||
},
|
||||
{
|
||||
name: "pnpm node file",
|
||||
argv: ["pnpm", "node", "./run.js"],
|
||||
scriptName: "run.js",
|
||||
initialBody: 'console.log("SAFE");\n',
|
||||
expectedArgvIndex: 2,
|
||||
binNames: ["pnpm", "node"],
|
||||
},
|
||||
{
|
||||
name: "pnpm node double-dash file",
|
||||
argv: ["pnpm", "node", "--", "./run.js"],
|
||||
scriptName: "run.js",
|
||||
initialBody: 'console.log("SAFE");\n',
|
||||
expectedArgvIndex: 3,
|
||||
binNames: ["pnpm", "node"],
|
||||
},
|
||||
{
|
||||
name: "npx tsx file",
|
||||
argv: ["npx", "tsx", "./run.ts"],
|
||||
@@ -395,9 +426,9 @@ describe("hardenApprovedExecutionPaths", () => {
|
||||
|
||||
for (const runtimeCase of mutableOperandCases) {
|
||||
it(`captures mutable ${runtimeCase.name} operands in approval plans`, () => {
|
||||
const binNames = runtimeCase.binName
|
||||
? [runtimeCase.binName]
|
||||
: ["bunx", "pnpm", "npm", "npx", "tsx"];
|
||||
const binNames =
|
||||
runtimeCase.binNames ??
|
||||
(runtimeCase.binName ? [runtimeCase.binName] : ["bunx", "pnpm", "npm", "npx", "tsx"]);
|
||||
withFakeRuntimeBins({
|
||||
binNames,
|
||||
run: () => {
|
||||
|
||||
@@ -164,6 +164,26 @@ const NPM_EXEC_FLAG_OPTIONS = new Set([
|
||||
"-y",
|
||||
]);
|
||||
|
||||
const PNPM_OPTIONS_WITH_VALUE = new Set([
|
||||
"--config",
|
||||
"--dir",
|
||||
"--filter",
|
||||
"--reporter",
|
||||
"--stream",
|
||||
"--test-pattern",
|
||||
"--workspace-concurrency",
|
||||
"-C",
|
||||
]);
|
||||
|
||||
const PNPM_FLAG_OPTIONS = new Set([
|
||||
"--aggregate-output",
|
||||
"--color",
|
||||
"--recursive",
|
||||
"--silent",
|
||||
"--workspace-root",
|
||||
"-r",
|
||||
]);
|
||||
|
||||
type FileOperandCollection = {
|
||||
hits: number[];
|
||||
sawOptionValueFile: boolean;
|
||||
@@ -299,6 +319,8 @@ function normalizePackageManagerExecToken(token: string): string {
|
||||
if (!normalized) {
|
||||
return normalized;
|
||||
}
|
||||
// Approval binding only promises best-effort recovery of the effective runtime
|
||||
// command for common package-manager shims; it is not full package-manager semantics.
|
||||
return normalized.replace(/\.(?:c|m)?js$/i, "");
|
||||
}
|
||||
|
||||
@@ -315,17 +337,30 @@ function unwrapPnpmExecInvocation(argv: string[]): string[] | null {
|
||||
continue;
|
||||
}
|
||||
if (!token.startsWith("-")) {
|
||||
if (token !== "exec" || idx + 1 >= argv.length) {
|
||||
return null;
|
||||
if (token === "exec") {
|
||||
if (idx + 1 >= argv.length) {
|
||||
return null;
|
||||
}
|
||||
const tail = argv.slice(idx + 1);
|
||||
return tail[0] === "--" ? (tail.length > 1 ? tail.slice(1) : null) : tail;
|
||||
}
|
||||
const tail = argv.slice(idx + 1);
|
||||
return tail[0] === "--" ? (tail.length > 1 ? tail.slice(1) : null) : tail;
|
||||
if (token === "node") {
|
||||
const tail = argv.slice(idx + 1);
|
||||
const normalizedTail = tail[0] === "--" ? tail.slice(1) : tail;
|
||||
return ["node", ...normalizedTail];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if ((token === "-C" || token === "--dir" || token === "--filter") && !token.includes("=")) {
|
||||
idx += 2;
|
||||
const [flag] = token.toLowerCase().split("=", 2);
|
||||
if (PNPM_OPTIONS_WITH_VALUE.has(flag)) {
|
||||
idx += token.includes("=") ? 1 : 2;
|
||||
continue;
|
||||
}
|
||||
idx += 1;
|
||||
if (PNPM_FLAG_OPTIONS.has(flag)) {
|
||||
idx += 1;
|
||||
continue;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user