mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-24 03:15:46 -06:00
fix(agents): pin agent openclaw invocations to gateway CLI (#122765)
This commit is contained in:
committed by
GitHub
parent
1ec4e4582e
commit
55d7f8a418
@@ -17,6 +17,7 @@ import type { GroupToolPolicyConfig } from "../config/types.tools.js";
|
||||
import type { DiagnosticTraceContext } from "../infra/diagnostic-trace-context.js";
|
||||
import { resolveEventSessionRoutingPolicy } from "../infra/event-session-routing.js";
|
||||
import { applyExecPolicyLayer } from "../infra/exec-policy.js";
|
||||
import { mergeGatewayAgentCliPath } from "../infra/openclaw-cli-shim.js";
|
||||
import { logWarn } from "../logger.js";
|
||||
import type {
|
||||
PluginHookChannelContext,
|
||||
@@ -574,7 +575,7 @@ function createOpenClawCodingToolsInternal(options?: OpenClawCodingToolsOptions)
|
||||
reviewer: options?.exec?.reviewer ?? execConfig.reviewer,
|
||||
trigger: options?.trigger,
|
||||
node: options?.exec?.node ?? execConfig.node,
|
||||
pathPrepend: options?.exec?.pathPrepend ?? execConfig.pathPrepend,
|
||||
pathPrepend: mergeGatewayAgentCliPath(options?.exec?.pathPrepend ?? execConfig.pathPrepend),
|
||||
safeBins: options?.exec?.safeBins ?? execConfig.safeBins,
|
||||
strictInlineEval: options?.exec?.strictInlineEval ?? execConfig.strictInlineEval,
|
||||
commandHighlighting: options?.exec?.commandHighlighting ?? execConfig.commandHighlighting,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { resolveExecCommandHighlighting } from "../config/exec-command-highlight
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { applyExecPolicyLayer } from "../infra/exec-policy.js";
|
||||
import { resolveMergedSafeBinProfileFixtures } from "../infra/exec-safe-bin-runtime-policy.js";
|
||||
import { mergeGatewayAgentCliPath } from "../infra/openclaw-cli-shim.js";
|
||||
import { createLazyImportLoader } from "../shared/lazy-promise.js";
|
||||
import { resolveAgentConfig } from "./agent-scope.js";
|
||||
import { describeExecTool } from "./bash-tools.descriptions.js";
|
||||
@@ -74,7 +75,7 @@ export function resolveExecToolConfig(params: { cfg?: OpenClawConfig; agentId?:
|
||||
security: layeredPolicy.security,
|
||||
ask: layeredPolicy.ask,
|
||||
node: agentExec?.node ?? globalExec?.node,
|
||||
pathPrepend: agentExec?.pathPrepend ?? globalExec?.pathPrepend,
|
||||
pathPrepend: mergeGatewayAgentCliPath(agentExec?.pathPrepend ?? globalExec?.pathPrepend),
|
||||
safeBins: agentExec?.safeBins ?? globalExec?.safeBins,
|
||||
strictInlineEval: agentExec?.strictInlineEval ?? globalExec?.strictInlineEval,
|
||||
commandHighlighting: resolveExecCommandHighlighting({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { isNixMode } from "../config/paths.js";
|
||||
import { clearGatewayAgentCliShim } from "../infra/openclaw-cli-shim.js";
|
||||
import { ensureOpenClawCliOnPath } from "../infra/path-env.js";
|
||||
import { createSubsystemLogger, runtimeForLogger } from "../logging/subsystem.js";
|
||||
import { clearPluginMetadataLifecycleCaches } from "../plugins/plugin-metadata-lifecycle.js";
|
||||
@@ -162,6 +163,7 @@ export async function createGatewayKernel(port = 18789, opts: GatewayServerOptio
|
||||
if (lifecycleRuntime) {
|
||||
await lifecycleRuntime.closeOnStartupFailure();
|
||||
} else {
|
||||
clearGatewayAgentCliShim();
|
||||
clearSecretsRuntimeSnapshotState();
|
||||
clearPluginMetadataLifecycleCaches();
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
setDiagnosticsEnabledForProcess,
|
||||
} from "../infra/diagnostic-events.js";
|
||||
import { isVitestRuntimeEnv, logAcceptedEnvOption } from "../infra/env.js";
|
||||
import { prepareGatewayAgentCliShim } from "../infra/openclaw-cli-shim.js";
|
||||
import { readGatewayRestartHandoffSync } from "../infra/restart-handoff.js";
|
||||
import { setGatewaySigusr1RestartPolicy, setPreRestartDeferralCheck } from "../infra/restart.js";
|
||||
import { enqueueSystemEvent } from "../infra/system-events.js";
|
||||
@@ -152,6 +153,9 @@ export async function prepareGatewayServerBootstrap(input: {
|
||||
]);
|
||||
}
|
||||
const startupTrace = createGatewayStartupTrace(log);
|
||||
if (!minimalTestGateway) {
|
||||
await startupTrace.measure("runtime.agent-cli", () => prepareGatewayAgentCliShim());
|
||||
}
|
||||
const startupConfigModulePromise = import("./server-startup-config.js");
|
||||
const loadStartupPluginsModule = createLazyPromise(() => import("./server-startup-plugins.js"), {
|
||||
cacheRejections: true,
|
||||
|
||||
@@ -4,7 +4,10 @@ import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { withTempDir } from "../test-utils/temp-dir.js";
|
||||
import { resolveCurrentOpenClawCliInvocation } from "./openclaw-cli-invocation.js";
|
||||
import {
|
||||
filterOpenClawChildExecArgv,
|
||||
resolveCurrentOpenClawCliInvocation,
|
||||
} from "./openclaw-cli-invocation.js";
|
||||
|
||||
const requireFromHere = createRequire(import.meta.url);
|
||||
const repoRoot = process.cwd();
|
||||
@@ -13,6 +16,21 @@ const trustedTsxLoader = requireFromHere.resolve("tsx", { paths: [repoRoot] });
|
||||
const commandArgs = ["sessions", "export-trajectory"];
|
||||
|
||||
describe("resolveCurrentOpenClawCliInvocation", () => {
|
||||
it("keeps child runtime flags without inheriting debugger ownership", () => {
|
||||
expect(
|
||||
filterOpenClawChildExecArgv([
|
||||
"--import",
|
||||
"/loader.mjs",
|
||||
"--inspect",
|
||||
"127.0.0.1:9231",
|
||||
"--inspect-brk=0",
|
||||
"--inspect-port",
|
||||
"9230",
|
||||
"--trace-warnings",
|
||||
]),
|
||||
).toEqual(["--import", "/loader.mjs", "--trace-warnings"]);
|
||||
});
|
||||
|
||||
it("uses the source entry for a Node-hosted checkout harness", () => {
|
||||
expect(
|
||||
resolveCurrentOpenClawCliInvocation(commandArgs, {
|
||||
|
||||
@@ -15,12 +15,46 @@ const OPENCLAW_PACKAGE_ENTRY_PATHS = new Set([
|
||||
path.join("src", "entry.ts"),
|
||||
]);
|
||||
|
||||
type OpenClawCliInvocation = Readonly<{
|
||||
export type OpenClawCliInvocation = Readonly<{
|
||||
command: string;
|
||||
args: string[];
|
||||
cwd: string;
|
||||
}>;
|
||||
|
||||
/** Keep child CLI launches on the parent's loader/runtime flags without inheriting its debugger. */
|
||||
export function filterOpenClawChildExecArgv(execArgv: readonly string[]): string[] {
|
||||
const filtered: string[] = [];
|
||||
for (let index = 0; index < execArgv.length; index += 1) {
|
||||
const arg = execArgv[index] ?? "";
|
||||
if (
|
||||
arg === "--inspect" ||
|
||||
arg.startsWith("--inspect=") ||
|
||||
arg === "--inspect-brk" ||
|
||||
arg.startsWith("--inspect-brk=") ||
|
||||
arg === "--inspect-wait" ||
|
||||
arg.startsWith("--inspect-wait=")
|
||||
) {
|
||||
const next = execArgv[index + 1];
|
||||
if (!arg.includes("=") && typeof next === "string" && !next.startsWith("-")) {
|
||||
index += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (arg === "--inspect-port") {
|
||||
const next = execArgv[index + 1];
|
||||
if (typeof next === "string" && !next.startsWith("-")) {
|
||||
index += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith("--inspect-port=")) {
|
||||
continue;
|
||||
}
|
||||
filtered.push(arg);
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
function resolveTrustedTsxLoader(packageRoot: string): string | null {
|
||||
try {
|
||||
return requireFromHere.resolve("tsx", { paths: [packageRoot] });
|
||||
@@ -53,7 +87,7 @@ export function resolveCurrentOpenClawCliInvocation(
|
||||
} = {},
|
||||
): OpenClawCliInvocation {
|
||||
const execPath = options.execPath ?? process.execPath;
|
||||
const execArgv = options.execArgv ?? process.execArgv;
|
||||
const execArgv = filterOpenClawChildExecArgv(options.execArgv ?? process.execArgv);
|
||||
const entry = (options.argv1 ?? process.argv[1])?.trim();
|
||||
const cwd = options.cwd ?? tryProcessCwd();
|
||||
const entryPackageRoot = entry ? resolveOpenClawPackageRootSync({ argv1: entry }) : null;
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { createExecTool } from "../agents/bash-tools.js";
|
||||
import { resolveExecToolConfig } from "../agents/lazy-exec-tool.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { captureEnv } from "../test-utils/env.js";
|
||||
import { withTempDir } from "../test-utils/temp-dir.js";
|
||||
import { clearGatewayAgentCliShim, prepareGatewayAgentCliShim } from "./openclaw-cli-shim.js";
|
||||
|
||||
const envSnapshot = captureEnv(["OPENCLAW_EXEC_SHELL_SNAPSHOT", "OPENCLAW_PROFILE", "PATH"]);
|
||||
|
||||
afterEach(() => {
|
||||
clearGatewayAgentCliShim();
|
||||
envSnapshot.restore();
|
||||
});
|
||||
|
||||
function readExecText(result: Awaited<ReturnType<ReturnType<typeof createExecTool>["execute"]>>) {
|
||||
return result.content.find((entry) => entry.type === "text")?.text?.trim() ?? "";
|
||||
}
|
||||
|
||||
describe.skipIf(process.platform === "win32")("Gateway agent CLI shim", () => {
|
||||
it.each([
|
||||
{ profile: "work", expectedArgs: ["--profile", "work", "probe"] },
|
||||
{ profile: undefined, expectedArgs: ["probe"] },
|
||||
])("pins the running CLI before configured PATH entries (profile=$profile)", async (testCase) => {
|
||||
await withTempDir("openclaw-agent-cli-shim-", async (root) => {
|
||||
const entryPath = path.join(root, "gateway-entry.mjs");
|
||||
const staleBinDir = path.join(root, "stale-bin");
|
||||
const staleCliPath = path.join(staleBinDir, "openclaw");
|
||||
const stateDir = path.join(root, "state");
|
||||
await fs.mkdir(staleBinDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
entryPath,
|
||||
'console.log(JSON.stringify({ source: "gateway", args: process.argv.slice(2), pathHead: process.env.PATH?.split(":")[0] }));\n',
|
||||
);
|
||||
await fs.writeFile(staleCliPath, "#!/bin/sh\nprintf '%s\\n' '{\"source\":\"stale\"}'\n", {
|
||||
mode: 0o700,
|
||||
});
|
||||
|
||||
const shim = await prepareGatewayAgentCliShim({
|
||||
env: testCase.profile ? { OPENCLAW_PROFILE: testCase.profile } : {},
|
||||
invocation: { command: process.execPath, args: [entryPath], cwd: root },
|
||||
stateDir,
|
||||
});
|
||||
const config = {
|
||||
tools: { exec: { pathPrepend: [staleBinDir] } },
|
||||
} satisfies OpenClawConfig;
|
||||
const execConfig = resolveExecToolConfig({ cfg: config });
|
||||
expect(execConfig.pathPrepend?.slice(0, 2)).toEqual([shim.binDir, staleBinDir]);
|
||||
|
||||
process.env.OPENCLAW_EXEC_SHELL_SNAPSHOT = "0";
|
||||
process.env.PATH = `${staleBinDir}${path.delimiter}${process.env.PATH ?? ""}`;
|
||||
delete process.env.OPENCLAW_PROFILE;
|
||||
const tool = createExecTool({
|
||||
...execConfig,
|
||||
host: "gateway",
|
||||
security: "full",
|
||||
ask: "off",
|
||||
cwd: root,
|
||||
notifyOnExit: false,
|
||||
});
|
||||
const result = await tool.execute("gateway-cli-version-probe", {
|
||||
command: "openclaw probe",
|
||||
yieldMs: 120_000,
|
||||
});
|
||||
expect(JSON.parse(readExecText(result))).toEqual({
|
||||
source: "gateway",
|
||||
args: testCase.expectedArgs,
|
||||
pathHead: shim.binDir,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("renders a Windows PATH launcher for the running CLI", async () => {
|
||||
await withTempDir("openclaw-agent-cli-shim-win-", async (root) => {
|
||||
const result = await prepareGatewayAgentCliShim({
|
||||
env: { OPENCLAW_PROFILE: "work" },
|
||||
invocation: {
|
||||
command: "C:\\Program Files\\nodejs\\node.exe",
|
||||
args: ["C:\\OpenClaw\\dist\\index.js"],
|
||||
cwd: "C:\\OpenClaw",
|
||||
},
|
||||
platform: "win32",
|
||||
stateDir: root,
|
||||
});
|
||||
|
||||
expect(path.basename(result.executablePath)).toBe("openclaw.cmd");
|
||||
expect(await fs.readFile(result.executablePath, "utf8")).toBe(
|
||||
'@echo off\r\n"C:\\Program Files\\nodejs\\node.exe" C:\\OpenClaw\\dist\\index.js --profile work %*\r\n',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { normalizeUniqueStringEntries } from "@openclaw/normalization-core/string-normalization";
|
||||
import { normalizeProfileName } from "../cli/profile-utils.js";
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
import { quoteCmdScriptArg } from "../daemon/cmd-argv.js";
|
||||
import { resolveGlobalSingleton } from "../shared/global-singleton.js";
|
||||
import { writeTextAtomic } from "./json-files.js";
|
||||
import {
|
||||
resolveCurrentOpenClawCliInvocation,
|
||||
type OpenClawCliInvocation,
|
||||
} from "./openclaw-cli-invocation.js";
|
||||
|
||||
const AGENT_CLI_BIN_DIR = path.join("tmp", "agent-cli");
|
||||
const GATEWAY_AGENT_CLI_STATE_KEY = Symbol.for("openclaw.gatewayAgentCliShim");
|
||||
const gatewayAgentCliState = resolveGlobalSingleton(
|
||||
GATEWAY_AGENT_CLI_STATE_KEY,
|
||||
() => ({ binDir: undefined as string | undefined }),
|
||||
(state) => {
|
||||
state.binDir = undefined;
|
||||
},
|
||||
);
|
||||
|
||||
function quotePosixArgument(value: string): string {
|
||||
return /^[A-Za-z0-9_@%+=:,./-]+$/u.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
|
||||
}
|
||||
|
||||
function renderPosixShim(invocation: OpenClawCliInvocation, profile: string | null): string {
|
||||
const args = [...invocation.args, ...(profile ? ["--profile", profile] : [])];
|
||||
return `#!/bin/sh
|
||||
set -eu
|
||||
exec ${[invocation.command, ...args].map(quotePosixArgument).join(" ")} "$@"
|
||||
`;
|
||||
}
|
||||
|
||||
function renderWindowsShim(invocation: OpenClawCliInvocation, profile: string | null): string {
|
||||
const args = [...invocation.args, ...(profile ? ["--profile", profile] : [])];
|
||||
return `@echo off\r\n${[invocation.command, ...args].map(quoteCmdScriptArg).join(" ")} %*\r\n`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Materialize the exact running Gateway CLI as an agent-visible PATH command.
|
||||
* The generated launcher is a runtime tool contract, not persisted product state.
|
||||
*/
|
||||
export async function prepareGatewayAgentCliShim(
|
||||
options: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
invocation?: OpenClawCliInvocation;
|
||||
platform?: NodeJS.Platform;
|
||||
stateDir?: string;
|
||||
} = {},
|
||||
): Promise<{ binDir: string; executablePath: string }> {
|
||||
const env = options.env ?? process.env;
|
||||
const platform = options.platform ?? process.platform;
|
||||
const invocation = options.invocation ?? resolveCurrentOpenClawCliInvocation([]);
|
||||
const profile = normalizeProfileName(env.OPENCLAW_PROFILE);
|
||||
const binDir = path.join(options.stateDir ?? resolveStateDir(env), AGENT_CLI_BIN_DIR);
|
||||
const executablePath = path.join(binDir, platform === "win32" ? "openclaw.cmd" : "openclaw");
|
||||
const content =
|
||||
platform === "win32"
|
||||
? renderWindowsShim(invocation, profile)
|
||||
: renderPosixShim(invocation, profile);
|
||||
|
||||
await fs.mkdir(binDir, { recursive: true, mode: 0o700 });
|
||||
await fs.chmod(binDir, 0o700).catch(() => undefined);
|
||||
await writeTextAtomic(executablePath, content, {
|
||||
mode: 0o700,
|
||||
dirMode: 0o700,
|
||||
durable: false,
|
||||
tempPrefix: "openclaw-agent-cli",
|
||||
});
|
||||
gatewayAgentCliState.binDir = binDir;
|
||||
return { binDir, executablePath };
|
||||
}
|
||||
|
||||
/** Clear a prepared launcher after startup failure; normal Gateway close resets it globally. */
|
||||
export function clearGatewayAgentCliShim(): void {
|
||||
gatewayAgentCliState.binDir = undefined;
|
||||
}
|
||||
|
||||
/** Prepend the prepared Gateway CLI ahead of operator-configured exec PATH entries. */
|
||||
export function mergeGatewayAgentCliPath(configured?: string[]): string[] | undefined {
|
||||
const merged = normalizeUniqueStringEntries([
|
||||
...(gatewayAgentCliState.binDir ? [gatewayAgentCliState.binDir] : []),
|
||||
...(configured ?? []),
|
||||
]);
|
||||
return merged.length > 0 ? merged : undefined;
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
export function filterTuiExecArgv(execArgv: readonly string[]): string[] {
|
||||
const filtered: string[] = [];
|
||||
for (let index = 0; index < execArgv.length; index += 1) {
|
||||
const arg = execArgv[index] ?? "";
|
||||
// Strip inspector flags so TUI-owned children cannot contend with or pause beneath
|
||||
// the parent debugger.
|
||||
if (
|
||||
arg === "--inspect" ||
|
||||
arg.startsWith("--inspect=") ||
|
||||
arg === "--inspect-brk" ||
|
||||
arg.startsWith("--inspect-brk=") ||
|
||||
arg === "--inspect-wait" ||
|
||||
arg.startsWith("--inspect-wait=")
|
||||
) {
|
||||
const next = execArgv[index + 1];
|
||||
if (!arg.includes("=") && typeof next === "string" && !next.startsWith("-")) {
|
||||
index += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (arg === "--inspect-port") {
|
||||
const next = execArgv[index + 1];
|
||||
if (typeof next === "string" && !next.startsWith("-")) {
|
||||
index += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith("--inspect-port=")) {
|
||||
continue;
|
||||
}
|
||||
filtered.push(arg);
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import { filterOpenClawChildExecArgv } from "../infra/openclaw-cli-invocation.js";
|
||||
import { attachChildProcessBridge } from "../process/child-process-bridge.js";
|
||||
import { filterTuiExecArgv } from "./tui-exec-argv.js";
|
||||
import type { TuiOptions } from "./tui.js";
|
||||
|
||||
function appendOption(args: string[], flag: string, value: string | number | undefined): void {
|
||||
@@ -22,7 +22,11 @@ function buildCurrentCliEntryArgs(): string[] {
|
||||
}
|
||||
|
||||
function buildTuiCliArgs(opts: TuiOptions): string[] {
|
||||
const args = [...filterTuiExecArgv(process.execArgv), ...buildCurrentCliEntryArgs(), "tui"];
|
||||
const args = [
|
||||
...filterOpenClawChildExecArgv(process.execArgv),
|
||||
...buildCurrentCliEntryArgs(),
|
||||
"tui",
|
||||
];
|
||||
if (opts.local) {
|
||||
args.push("--local");
|
||||
}
|
||||
|
||||
+1
-2
@@ -46,7 +46,6 @@ import { sanitizeAutocompleteProvider } from "./tui-autocomplete.js";
|
||||
import type { TuiBackend } from "./tui-backend.js";
|
||||
import { createCommandHandlers } from "./tui-command-handlers.js";
|
||||
import { createEventHandlers } from "./tui-event-handlers.js";
|
||||
import { filterTuiExecArgv } from "./tui-exec-argv.js";
|
||||
import {
|
||||
formatTuiErrorMessage,
|
||||
formatTuiFooter,
|
||||
@@ -170,7 +169,7 @@ export function resolveTuiLocalAuthCliInvocation(params: {
|
||||
return resolveCurrentOpenClawCliInvocation(
|
||||
["models", "auth", "login", ...(provider ? ["--provider", provider] : [])],
|
||||
{
|
||||
execArgv: filterTuiExecArgv(params.execArgv ?? process.execArgv),
|
||||
execArgv: params.execArgv ?? process.execArgv,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user