Skip to content

Commit c8ebd48

Browse files
SidSid-Qingumadeiras
authored
fix(node-host): sync rawCommand with hardened argv after executable path pinning (#33137)
Merged via squash. Prepared head SHA: a798790 Co-authored-by: Sid-Qin <[email protected]> Co-authored-by: gumadeiras <[email protected]> Reviewed-by: @gumadeiras
1 parent 4fb4049 commit c8ebd48

File tree

4 files changed

+77
-2
lines changed

4 files changed

+77
-2
lines changed

CHANGELOG.md

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

1717
### Fixes
1818

19+
- 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.
1920
- 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.
2021
- 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.
2122
- 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.

src/node-host/invoke-system-run-plan.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import fs from "node:fs";
22
import os from "node:os";
33
import path from "node:path";
44
import { describe, expect, it } from "vitest";
5+
import { formatExecCommand } from "../infra/system-run-command.js";
56
import {
67
buildSystemRunApprovalPlan,
78
hardenApprovedExecutionPaths,
@@ -18,7 +19,9 @@ type HardeningCase = {
1819
shellCommand?: string | null;
1920
withPathToken?: boolean;
2021
expectedArgv: (ctx: { pathToken: PathTokenSetup | null }) => string[];
22+
expectedArgvChanged?: boolean;
2123
expectedCmdText?: string;
24+
checkRawCommandMatchesArgv?: boolean;
2225
};
2326

2427
describe("hardenApprovedExecutionPaths", () => {
@@ -36,6 +39,7 @@ describe("hardenApprovedExecutionPaths", () => {
3639
argv: ["env", "tr", "a", "b"],
3740
shellCommand: null,
3841
expectedArgv: () => ["env", "tr", "a", "b"],
42+
expectedArgvChanged: false,
3943
},
4044
{
4145
name: "pins direct PATH-token executable during approval hardening",
@@ -44,6 +48,7 @@ describe("hardenApprovedExecutionPaths", () => {
4448
shellCommand: null,
4549
withPathToken: true,
4650
expectedArgv: ({ pathToken }) => [pathToken!.expected, "SAFE"],
51+
expectedArgvChanged: true,
4752
},
4853
{
4954
name: "preserves env-wrapper PATH-token argv during approval hardening",
@@ -52,6 +57,15 @@ describe("hardenApprovedExecutionPaths", () => {
5257
shellCommand: null,
5358
withPathToken: true,
5459
expectedArgv: () => ["env", "poccmd", "SAFE"],
60+
expectedArgvChanged: false,
61+
},
62+
{
63+
name: "rawCommand matches hardened argv after executable path pinning",
64+
mode: "build-plan",
65+
argv: ["poccmd", "hello"],
66+
withPathToken: true,
67+
expectedArgv: ({ pathToken }) => [pathToken!.expected, "hello"],
68+
checkRawCommandMatchesArgv: true,
5569
},
5670
];
5771

@@ -82,6 +96,9 @@ describe("hardenApprovedExecutionPaths", () => {
8296
if (testCase.expectedCmdText) {
8397
expect(prepared.cmdText).toBe(testCase.expectedCmdText);
8498
}
99+
if (testCase.checkRawCommandMatchesArgv) {
100+
expect(prepared.plan.rawCommand).toBe(formatExecCommand(prepared.plan.argv));
101+
}
85102
return;
86103
}
87104

@@ -96,6 +113,9 @@ describe("hardenApprovedExecutionPaths", () => {
96113
throw new Error("unreachable");
97114
}
98115
expect(hardened.argv).toEqual(testCase.expectedArgv({ pathToken }));
116+
if (typeof testCase.expectedArgvChanged === "boolean") {
117+
expect(hardened.argvChanged).toBe(testCase.expectedArgvChanged);
118+
}
99119
} finally {
100120
if (testCase.withPathToken) {
101121
if (oldPath === undefined) {

src/node-host/invoke-system-run-plan.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import path from "node:path";
33
import type { SystemRunApprovalPlan } from "../infra/exec-approvals.js";
44
import { resolveCommandResolutionFromArgv } from "../infra/exec-command-resolution.js";
55
import { sameFileIdentity } from "../infra/file-identity.js";
6-
import { resolveSystemRunCommand } from "../infra/system-run-command.js";
6+
import { formatExecCommand, resolveSystemRunCommand } from "../infra/system-run-command.js";
77

88
export type ApprovedCwdSnapshot = {
99
cwd: string;
@@ -144,6 +144,7 @@ export function hardenApprovedExecutionPaths(params: {
144144
| {
145145
ok: true;
146146
argv: string[];
147+
argvChanged: boolean;
147148
cwd: string | undefined;
148149
approvedCwdSnapshot: ApprovedCwdSnapshot | undefined;
149150
}
@@ -152,6 +153,7 @@ export function hardenApprovedExecutionPaths(params: {
152153
return {
153154
ok: true,
154155
argv: params.argv,
156+
argvChanged: false,
155157
cwd: params.cwd,
156158
approvedCwdSnapshot: undefined,
157159
};
@@ -172,6 +174,7 @@ export function hardenApprovedExecutionPaths(params: {
172174
return {
173175
ok: true,
174176
argv: params.argv,
177+
argvChanged: false,
175178
cwd: hardenedCwd,
176179
approvedCwdSnapshot,
177180
};
@@ -190,6 +193,7 @@ export function hardenApprovedExecutionPaths(params: {
190193
return {
191194
ok: true,
192195
argv: params.argv,
196+
argvChanged: false,
193197
cwd: hardenedCwd,
194198
approvedCwdSnapshot,
195199
};
@@ -203,11 +207,22 @@ export function hardenApprovedExecutionPaths(params: {
203207
};
204208
}
205209

210+
if (pinnedExecutable === params.argv[0]) {
211+
return {
212+
ok: true,
213+
argv: params.argv,
214+
argvChanged: false,
215+
cwd: hardenedCwd,
216+
approvedCwdSnapshot,
217+
};
218+
}
219+
206220
const argv = [...params.argv];
207221
argv[0] = pinnedExecutable;
208222
return {
209223
ok: true,
210224
argv,
225+
argvChanged: true,
211226
cwd: hardenedCwd,
212227
approvedCwdSnapshot,
213228
};
@@ -239,12 +254,15 @@ export function buildSystemRunApprovalPlan(params: {
239254
if (!hardening.ok) {
240255
return { ok: false, message: hardening.message };
241256
}
257+
const rawCommand = hardening.argvChanged
258+
? formatExecCommand(hardening.argv) || null
259+
: command.cmdText.trim() || null;
242260
return {
243261
ok: true,
244262
plan: {
245263
argv: hardening.argv,
246264
cwd: hardening.cwd ?? null,
247-
rawCommand: command.cmdText.trim() || null,
265+
rawCommand,
248266
agentId: normalizeString(params.agentId),
249267
sessionKey: normalizeString(params.sessionKey),
250268
},

src/node-host/invoke-system-run.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import path from "node:path";
44
import { describe, expect, it, type Mock, vi } from "vitest";
55
import { saveExecApprovals } from "../infra/exec-approvals.js";
66
import type { ExecHostResponse } from "../infra/exec-host.js";
7+
import { buildSystemRunApprovalPlan } from "./invoke-system-run-plan.js";
78
import { handleSystemRunInvoke, formatSystemRunAllowlistMissMessage } from "./invoke-system-run.js";
89
import type { HandleSystemRunInvokeOptions } from "./invoke-system-run.js";
910

@@ -233,6 +234,7 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
233234
preferMacAppExecHost: boolean;
234235
runViaResponse?: ExecHostResponse | null;
235236
command?: string[];
237+
rawCommand?: string | null;
236238
cwd?: string;
237239
security?: "full" | "allowlist";
238240
ask?: "off" | "on-miss" | "always";
@@ -286,6 +288,7 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
286288
client: {} as never,
287289
params: {
288290
command: params.command ?? ["echo", "ok"],
291+
rawCommand: params.rawCommand,
289292
cwd: params.cwd,
290293
approved: params.approved ?? false,
291294
sessionKey: "agent:main:main",
@@ -492,6 +495,39 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
492495
},
493496
);
494497

498+
it.runIf(process.platform !== "win32")(
499+
"accepts prepared plans after PATH-token hardening rewrites argv",
500+
async () => {
501+
await withPathTokenCommand({
502+
tmpPrefix: "openclaw-prepare-run-path-pin-",
503+
run: async ({ expected }) => {
504+
const prepared = buildSystemRunApprovalPlan({
505+
command: ["poccmd", "hello"],
506+
});
507+
expect(prepared.ok).toBe(true);
508+
if (!prepared.ok) {
509+
throw new Error("unreachable");
510+
}
511+
512+
const { runCommand, sendInvokeResult } = await runSystemInvoke({
513+
preferMacAppExecHost: false,
514+
command: prepared.plan.argv,
515+
rawCommand: prepared.plan.rawCommand,
516+
approved: true,
517+
security: "full",
518+
ask: "off",
519+
});
520+
expectCommandPinnedToCanonicalPath({
521+
runCommand,
522+
expected,
523+
commandTail: ["hello"],
524+
});
525+
expectInvokeOk(sendInvokeResult);
526+
},
527+
});
528+
},
529+
);
530+
495531
it.runIf(process.platform !== "win32")(
496532
"pins PATH-token executable to canonical path for allowlist runs",
497533
async () => {

0 commit comments

Comments
 (0)