fix(node-host): sync rawCommand with hardened argv after executable path pinning (#33137)

Merged via squash.

Prepared head SHA: a7987905f7
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:
Sid 2026-03-05 00:30:33 +08:00 committed by GitHub
parent 4fb40497d4
commit c8ebd48e0f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 77 additions and 2 deletions

View File

@ -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.

View File

@ -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) {

View File

@ -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),
},

View File

@ -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 () => {