mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 12:08:37 +00:00
node-host: make prepared rawCommand sync explicit
This commit is contained in:
@@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
|
|
||||||
### Fixes
|
### 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.
|
- 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.
|
- 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.
|
- 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.
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ type HardeningCase = {
|
|||||||
shellCommand?: string | null;
|
shellCommand?: string | null;
|
||||||
withPathToken?: boolean;
|
withPathToken?: boolean;
|
||||||
expectedArgv: (ctx: { pathToken: PathTokenSetup | null }) => string[];
|
expectedArgv: (ctx: { pathToken: PathTokenSetup | null }) => string[];
|
||||||
|
expectedArgvChanged?: boolean;
|
||||||
expectedCmdText?: string;
|
expectedCmdText?: string;
|
||||||
checkRawCommandMatchesArgv?: boolean;
|
checkRawCommandMatchesArgv?: boolean;
|
||||||
};
|
};
|
||||||
@@ -38,6 +39,7 @@ describe("hardenApprovedExecutionPaths", () => {
|
|||||||
argv: ["env", "tr", "a", "b"],
|
argv: ["env", "tr", "a", "b"],
|
||||||
shellCommand: null,
|
shellCommand: null,
|
||||||
expectedArgv: () => ["env", "tr", "a", "b"],
|
expectedArgv: () => ["env", "tr", "a", "b"],
|
||||||
|
expectedArgvChanged: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "pins direct PATH-token executable during approval hardening",
|
name: "pins direct PATH-token executable during approval hardening",
|
||||||
@@ -46,6 +48,7 @@ describe("hardenApprovedExecutionPaths", () => {
|
|||||||
shellCommand: null,
|
shellCommand: null,
|
||||||
withPathToken: true,
|
withPathToken: true,
|
||||||
expectedArgv: ({ pathToken }) => [pathToken!.expected, "SAFE"],
|
expectedArgv: ({ pathToken }) => [pathToken!.expected, "SAFE"],
|
||||||
|
expectedArgvChanged: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "preserves env-wrapper PATH-token argv during approval hardening",
|
name: "preserves env-wrapper PATH-token argv during approval hardening",
|
||||||
@@ -54,6 +57,7 @@ describe("hardenApprovedExecutionPaths", () => {
|
|||||||
shellCommand: null,
|
shellCommand: null,
|
||||||
withPathToken: true,
|
withPathToken: true,
|
||||||
expectedArgv: () => ["env", "poccmd", "SAFE"],
|
expectedArgv: () => ["env", "poccmd", "SAFE"],
|
||||||
|
expectedArgvChanged: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "rawCommand matches hardened argv after executable path pinning",
|
name: "rawCommand matches hardened argv after executable path pinning",
|
||||||
@@ -109,6 +113,9 @@ describe("hardenApprovedExecutionPaths", () => {
|
|||||||
throw new Error("unreachable");
|
throw new Error("unreachable");
|
||||||
}
|
}
|
||||||
expect(hardened.argv).toEqual(testCase.expectedArgv({ pathToken }));
|
expect(hardened.argv).toEqual(testCase.expectedArgv({ pathToken }));
|
||||||
|
if (typeof testCase.expectedArgvChanged === "boolean") {
|
||||||
|
expect(hardened.argvChanged).toBe(testCase.expectedArgvChanged);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (testCase.withPathToken) {
|
if (testCase.withPathToken) {
|
||||||
if (oldPath === undefined) {
|
if (oldPath === undefined) {
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ export function hardenApprovedExecutionPaths(params: {
|
|||||||
| {
|
| {
|
||||||
ok: true;
|
ok: true;
|
||||||
argv: string[];
|
argv: string[];
|
||||||
|
argvChanged: boolean;
|
||||||
cwd: string | undefined;
|
cwd: string | undefined;
|
||||||
approvedCwdSnapshot: ApprovedCwdSnapshot | undefined;
|
approvedCwdSnapshot: ApprovedCwdSnapshot | undefined;
|
||||||
}
|
}
|
||||||
@@ -152,6 +153,7 @@ export function hardenApprovedExecutionPaths(params: {
|
|||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
argv: params.argv,
|
argv: params.argv,
|
||||||
|
argvChanged: false,
|
||||||
cwd: params.cwd,
|
cwd: params.cwd,
|
||||||
approvedCwdSnapshot: undefined,
|
approvedCwdSnapshot: undefined,
|
||||||
};
|
};
|
||||||
@@ -172,6 +174,7 @@ export function hardenApprovedExecutionPaths(params: {
|
|||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
argv: params.argv,
|
argv: params.argv,
|
||||||
|
argvChanged: false,
|
||||||
cwd: hardenedCwd,
|
cwd: hardenedCwd,
|
||||||
approvedCwdSnapshot,
|
approvedCwdSnapshot,
|
||||||
};
|
};
|
||||||
@@ -190,6 +193,7 @@ export function hardenApprovedExecutionPaths(params: {
|
|||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
argv: params.argv,
|
argv: params.argv,
|
||||||
|
argvChanged: false,
|
||||||
cwd: hardenedCwd,
|
cwd: hardenedCwd,
|
||||||
approvedCwdSnapshot,
|
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];
|
const argv = [...params.argv];
|
||||||
argv[0] = pinnedExecutable;
|
argv[0] = pinnedExecutable;
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
argv,
|
argv,
|
||||||
|
argvChanged: true,
|
||||||
cwd: hardenedCwd,
|
cwd: hardenedCwd,
|
||||||
approvedCwdSnapshot,
|
approvedCwdSnapshot,
|
||||||
};
|
};
|
||||||
@@ -239,10 +254,9 @@ export function buildSystemRunApprovalPlan(params: {
|
|||||||
if (!hardening.ok) {
|
if (!hardening.ok) {
|
||||||
return { ok: false, message: hardening.message };
|
return { ok: false, message: hardening.message };
|
||||||
}
|
}
|
||||||
const rawCommand =
|
const rawCommand = hardening.argvChanged
|
||||||
hardening.argv === command.argv
|
? formatExecCommand(hardening.argv) || null
|
||||||
? command.cmdText.trim() || null
|
: command.cmdText.trim() || null;
|
||||||
: formatExecCommand(hardening.argv) || null;
|
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
plan: {
|
plan: {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import path from "node:path";
|
|||||||
import { describe, expect, it, type Mock, vi } from "vitest";
|
import { describe, expect, it, type Mock, vi } from "vitest";
|
||||||
import { saveExecApprovals } from "../infra/exec-approvals.js";
|
import { saveExecApprovals } from "../infra/exec-approvals.js";
|
||||||
import type { ExecHostResponse } from "../infra/exec-host.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 { handleSystemRunInvoke, formatSystemRunAllowlistMissMessage } from "./invoke-system-run.js";
|
||||||
import type { HandleSystemRunInvokeOptions } 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;
|
preferMacAppExecHost: boolean;
|
||||||
runViaResponse?: ExecHostResponse | null;
|
runViaResponse?: ExecHostResponse | null;
|
||||||
command?: string[];
|
command?: string[];
|
||||||
|
rawCommand?: string | null;
|
||||||
cwd?: string;
|
cwd?: string;
|
||||||
security?: "full" | "allowlist";
|
security?: "full" | "allowlist";
|
||||||
ask?: "off" | "on-miss" | "always";
|
ask?: "off" | "on-miss" | "always";
|
||||||
@@ -286,6 +288,7 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
|
|||||||
client: {} as never,
|
client: {} as never,
|
||||||
params: {
|
params: {
|
||||||
command: params.command ?? ["echo", "ok"],
|
command: params.command ?? ["echo", "ok"],
|
||||||
|
rawCommand: params.rawCommand,
|
||||||
cwd: params.cwd,
|
cwd: params.cwd,
|
||||||
approved: params.approved ?? false,
|
approved: params.approved ?? false,
|
||||||
sessionKey: "agent:main:main",
|
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")(
|
it.runIf(process.platform !== "win32")(
|
||||||
"pins PATH-token executable to canonical path for allowlist runs",
|
"pins PATH-token executable to canonical path for allowlist runs",
|
||||||
async () => {
|
async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user