fix(node-host): sync rawCommand with hardened argv after executable path pinning (#33137)
Merged via squash. Prepared head SHA: a7987905f7ad6cf5fee286ffa81ceaad8297174f Co-authored-by: Sid-Qin <201593046+Sid-Qin@users.noreply.github.com> Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com> Reviewed-by: @gumadeiras
This commit is contained in:
@@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Nodes/system.run approval hardening: use explicit argv-mutation signaling when regenerating prepared `rawCommand`, and cover the `system.run.prepare -> system.run` handoff so direct PATH-based `nodes.run` commands no longer fail with `rawCommand does not match command`. (#33137) thanks @Sid-Qin.
|
||||
- Models/custom provider headers: propagate `models.providers.<name>.headers` across inline, fallback, and registry-found model resolution so header-authenticated proxies consistently receive configured request headers. (#27490) thanks @Sid-Qin.
|
||||
- Daemon/systemd install robustness: treat `systemctl --user is-enabled` exit-code-4 `not-found` responses as not-enabled by combining stderr/stdout detail parsing, so Ubuntu fresh installs no longer fail with `systemctl is-enabled unavailable`. (#33634) Thanks @Yuandiaodiaodiao.
|
||||
- Slack/system-event session routing: resolve reaction/member/pin/interaction system-event session keys through channel/account bindings (with sender-aware DM routing) so inbound Slack events target the correct agent session in multi-account setups instead of defaulting to `agent:main`. (#34045) Thanks @paulomcg, @daht-mad and @vincentkoc.
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { formatExecCommand } from "../infra/system-run-command.js";
|
||||
import {
|
||||
buildSystemRunApprovalPlan,
|
||||
hardenApprovedExecutionPaths,
|
||||
@@ -18,7 +19,9 @@ type HardeningCase = {
|
||||
shellCommand?: string | null;
|
||||
withPathToken?: boolean;
|
||||
expectedArgv: (ctx: { pathToken: PathTokenSetup | null }) => string[];
|
||||
expectedArgvChanged?: boolean;
|
||||
expectedCmdText?: string;
|
||||
checkRawCommandMatchesArgv?: boolean;
|
||||
};
|
||||
|
||||
describe("hardenApprovedExecutionPaths", () => {
|
||||
@@ -36,6 +39,7 @@ describe("hardenApprovedExecutionPaths", () => {
|
||||
argv: ["env", "tr", "a", "b"],
|
||||
shellCommand: null,
|
||||
expectedArgv: () => ["env", "tr", "a", "b"],
|
||||
expectedArgvChanged: false,
|
||||
},
|
||||
{
|
||||
name: "pins direct PATH-token executable during approval hardening",
|
||||
@@ -44,6 +48,7 @@ describe("hardenApprovedExecutionPaths", () => {
|
||||
shellCommand: null,
|
||||
withPathToken: true,
|
||||
expectedArgv: ({ pathToken }) => [pathToken!.expected, "SAFE"],
|
||||
expectedArgvChanged: true,
|
||||
},
|
||||
{
|
||||
name: "preserves env-wrapper PATH-token argv during approval hardening",
|
||||
@@ -52,6 +57,15 @@ describe("hardenApprovedExecutionPaths", () => {
|
||||
shellCommand: null,
|
||||
withPathToken: true,
|
||||
expectedArgv: () => ["env", "poccmd", "SAFE"],
|
||||
expectedArgvChanged: false,
|
||||
},
|
||||
{
|
||||
name: "rawCommand matches hardened argv after executable path pinning",
|
||||
mode: "build-plan",
|
||||
argv: ["poccmd", "hello"],
|
||||
withPathToken: true,
|
||||
expectedArgv: ({ pathToken }) => [pathToken!.expected, "hello"],
|
||||
checkRawCommandMatchesArgv: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -82,6 +96,9 @@ describe("hardenApprovedExecutionPaths", () => {
|
||||
if (testCase.expectedCmdText) {
|
||||
expect(prepared.cmdText).toBe(testCase.expectedCmdText);
|
||||
}
|
||||
if (testCase.checkRawCommandMatchesArgv) {
|
||||
expect(prepared.plan.rawCommand).toBe(formatExecCommand(prepared.plan.argv));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -96,6 +113,9 @@ describe("hardenApprovedExecutionPaths", () => {
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
expect(hardened.argv).toEqual(testCase.expectedArgv({ pathToken }));
|
||||
if (typeof testCase.expectedArgvChanged === "boolean") {
|
||||
expect(hardened.argvChanged).toBe(testCase.expectedArgvChanged);
|
||||
}
|
||||
} finally {
|
||||
if (testCase.withPathToken) {
|
||||
if (oldPath === undefined) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import path from "node:path";
|
||||
import type { SystemRunApprovalPlan } from "../infra/exec-approvals.js";
|
||||
import { resolveCommandResolutionFromArgv } from "../infra/exec-command-resolution.js";
|
||||
import { sameFileIdentity } from "../infra/file-identity.js";
|
||||
import { resolveSystemRunCommand } from "../infra/system-run-command.js";
|
||||
import { formatExecCommand, resolveSystemRunCommand } from "../infra/system-run-command.js";
|
||||
|
||||
export type ApprovedCwdSnapshot = {
|
||||
cwd: string;
|
||||
@@ -144,6 +144,7 @@ export function hardenApprovedExecutionPaths(params: {
|
||||
| {
|
||||
ok: true;
|
||||
argv: string[];
|
||||
argvChanged: boolean;
|
||||
cwd: string | undefined;
|
||||
approvedCwdSnapshot: ApprovedCwdSnapshot | undefined;
|
||||
}
|
||||
@@ -152,6 +153,7 @@ export function hardenApprovedExecutionPaths(params: {
|
||||
return {
|
||||
ok: true,
|
||||
argv: params.argv,
|
||||
argvChanged: false,
|
||||
cwd: params.cwd,
|
||||
approvedCwdSnapshot: undefined,
|
||||
};
|
||||
@@ -172,6 +174,7 @@ export function hardenApprovedExecutionPaths(params: {
|
||||
return {
|
||||
ok: true,
|
||||
argv: params.argv,
|
||||
argvChanged: false,
|
||||
cwd: hardenedCwd,
|
||||
approvedCwdSnapshot,
|
||||
};
|
||||
@@ -190,6 +193,7 @@ export function hardenApprovedExecutionPaths(params: {
|
||||
return {
|
||||
ok: true,
|
||||
argv: params.argv,
|
||||
argvChanged: false,
|
||||
cwd: hardenedCwd,
|
||||
approvedCwdSnapshot,
|
||||
};
|
||||
@@ -203,11 +207,22 @@ export function hardenApprovedExecutionPaths(params: {
|
||||
};
|
||||
}
|
||||
|
||||
if (pinnedExecutable === params.argv[0]) {
|
||||
return {
|
||||
ok: true,
|
||||
argv: params.argv,
|
||||
argvChanged: false,
|
||||
cwd: hardenedCwd,
|
||||
approvedCwdSnapshot,
|
||||
};
|
||||
}
|
||||
|
||||
const argv = [...params.argv];
|
||||
argv[0] = pinnedExecutable;
|
||||
return {
|
||||
ok: true,
|
||||
argv,
|
||||
argvChanged: true,
|
||||
cwd: hardenedCwd,
|
||||
approvedCwdSnapshot,
|
||||
};
|
||||
@@ -239,12 +254,15 @@ export function buildSystemRunApprovalPlan(params: {
|
||||
if (!hardening.ok) {
|
||||
return { ok: false, message: hardening.message };
|
||||
}
|
||||
const rawCommand = hardening.argvChanged
|
||||
? formatExecCommand(hardening.argv) || null
|
||||
: command.cmdText.trim() || null;
|
||||
return {
|
||||
ok: true,
|
||||
plan: {
|
||||
argv: hardening.argv,
|
||||
cwd: hardening.cwd ?? null,
|
||||
rawCommand: command.cmdText.trim() || null,
|
||||
rawCommand,
|
||||
agentId: normalizeString(params.agentId),
|
||||
sessionKey: normalizeString(params.sessionKey),
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@ import path from "node:path";
|
||||
import { describe, expect, it, type Mock, vi } from "vitest";
|
||||
import { saveExecApprovals } from "../infra/exec-approvals.js";
|
||||
import type { ExecHostResponse } from "../infra/exec-host.js";
|
||||
import { buildSystemRunApprovalPlan } from "./invoke-system-run-plan.js";
|
||||
import { handleSystemRunInvoke, formatSystemRunAllowlistMissMessage } from "./invoke-system-run.js";
|
||||
import type { HandleSystemRunInvokeOptions } from "./invoke-system-run.js";
|
||||
|
||||
@@ -233,6 +234,7 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
|
||||
preferMacAppExecHost: boolean;
|
||||
runViaResponse?: ExecHostResponse | null;
|
||||
command?: string[];
|
||||
rawCommand?: string | null;
|
||||
cwd?: string;
|
||||
security?: "full" | "allowlist";
|
||||
ask?: "off" | "on-miss" | "always";
|
||||
@@ -286,6 +288,7 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
|
||||
client: {} as never,
|
||||
params: {
|
||||
command: params.command ?? ["echo", "ok"],
|
||||
rawCommand: params.rawCommand,
|
||||
cwd: params.cwd,
|
||||
approved: params.approved ?? false,
|
||||
sessionKey: "agent:main:main",
|
||||
@@ -492,6 +495,39 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it.runIf(process.platform !== "win32")(
|
||||
"accepts prepared plans after PATH-token hardening rewrites argv",
|
||||
async () => {
|
||||
await withPathTokenCommand({
|
||||
tmpPrefix: "openclaw-prepare-run-path-pin-",
|
||||
run: async ({ expected }) => {
|
||||
const prepared = buildSystemRunApprovalPlan({
|
||||
command: ["poccmd", "hello"],
|
||||
});
|
||||
expect(prepared.ok).toBe(true);
|
||||
if (!prepared.ok) {
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
|
||||
const { runCommand, sendInvokeResult } = await runSystemInvoke({
|
||||
preferMacAppExecHost: false,
|
||||
command: prepared.plan.argv,
|
||||
rawCommand: prepared.plan.rawCommand,
|
||||
approved: true,
|
||||
security: "full",
|
||||
ask: "off",
|
||||
});
|
||||
expectCommandPinnedToCanonicalPath({
|
||||
runCommand,
|
||||
expected,
|
||||
commandTail: ["hello"],
|
||||
});
|
||||
expectInvokeOk(sendInvokeResult);
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.runIf(process.platform !== "win32")(
|
||||
"pins PATH-token executable to canonical path for allowlist runs",
|
||||
async () => {
|
||||
|
||||
Reference in New Issue
Block a user