mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-24 13:19:35 +00:00
docs: document exec tool entry
This commit is contained in:
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Foreground exec failure tests.
|
||||
* Verifies failed process outcomes surface useful text/details for shell
|
||||
* errors, timeouts, signals, and runtime failures.
|
||||
*/
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { SpawnInput } from "../process/supervisor/index.js";
|
||||
import { captureEnv } from "../test-utils/env.js";
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Gateway exec approval E2E tests.
|
||||
* Exercises a real gateway server approval flow, approval follow-up text, and
|
||||
* approval timeout behavior in an isolated temp config.
|
||||
*/
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
@@ -62,117 +67,121 @@ describe("gateway-hosted exec approvals", () => {
|
||||
clearSessionStoreCacheForTest();
|
||||
});
|
||||
|
||||
it("lets OpenClaw-style gateway tool calls request and wait for approval over separate connections", async () => {
|
||||
const envSnapshot = captureEnv(TEST_ENV_KEYS);
|
||||
cleanup.push(() => envSnapshot.restore());
|
||||
it(
|
||||
"lets OpenClaw-style gateway tool calls request and wait for approval over separate connections",
|
||||
async () => {
|
||||
const envSnapshot = captureEnv(TEST_ENV_KEYS);
|
||||
cleanup.push(() => envSnapshot.restore());
|
||||
|
||||
const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-exec-approval-e2e-"));
|
||||
cleanup.push(() => fs.rm(tempHome, { recursive: true, force: true, maxRetries: 5 }));
|
||||
const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-exec-approval-e2e-"));
|
||||
cleanup.push(() => fs.rm(tempHome, { recursive: true, force: true, maxRetries: 5 }));
|
||||
|
||||
const stateDir = path.join(tempHome, ".openclaw");
|
||||
const workspaceDir = path.join(tempHome, "workspace");
|
||||
await fs.mkdir(workspaceDir, { recursive: true });
|
||||
const stateDir = path.join(tempHome, ".openclaw");
|
||||
const workspaceDir = path.join(tempHome, "workspace");
|
||||
await fs.mkdir(workspaceDir, { recursive: true });
|
||||
|
||||
const port = await getFreeGatewayPort();
|
||||
const token = "exec-approval-e2e-token";
|
||||
const configPath = path.join(stateDir, "openclaw.json");
|
||||
await fs.mkdir(stateDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
configPath,
|
||||
`${JSON.stringify(
|
||||
{
|
||||
gateway: {
|
||||
port,
|
||||
auth: { mode: "token", token },
|
||||
},
|
||||
tools: {
|
||||
exec: {
|
||||
host: "gateway",
|
||||
security: "allowlist",
|
||||
ask: "always",
|
||||
const port = await getFreeGatewayPort();
|
||||
const token = "exec-approval-e2e-token";
|
||||
const configPath = path.join(stateDir, "openclaw.json");
|
||||
await fs.mkdir(stateDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
configPath,
|
||||
`${JSON.stringify(
|
||||
{
|
||||
gateway: {
|
||||
port,
|
||||
auth: { mode: "token", token },
|
||||
},
|
||||
tools: {
|
||||
exec: {
|
||||
host: "gateway",
|
||||
security: "allowlist",
|
||||
ask: "always",
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
|
||||
process.env.HOME = tempHome;
|
||||
process.env.OPENCLAW_STATE_DIR = stateDir;
|
||||
process.env.OPENCLAW_CONFIG_PATH = configPath;
|
||||
process.env.OPENCLAW_GATEWAY_TOKEN = token;
|
||||
process.env.OPENCLAW_GATEWAY_PORT = String(port);
|
||||
process.env.OPENCLAW_SKIP_CHANNELS = "1";
|
||||
process.env.OPENCLAW_SKIP_GMAIL_WATCHER = "1";
|
||||
process.env.OPENCLAW_SKIP_CRON = "1";
|
||||
process.env.OPENCLAW_SKIP_CANVAS_HOST = "1";
|
||||
process.env.OPENCLAW_SKIP_BROWSER_CONTROL_SERVER = "1";
|
||||
process.env.OPENCLAW_SKIP_PROVIDERS = "1";
|
||||
process.env.OPENCLAW_TEST_MINIMAL_GATEWAY = "1";
|
||||
clearRuntimeConfigSnapshot();
|
||||
clearConfigCache();
|
||||
clearSessionStoreCacheForTest();
|
||||
|
||||
const server = await startGatewayServer(port, {
|
||||
bind: "loopback",
|
||||
auth: { mode: "token", token },
|
||||
controlUiEnabled: false,
|
||||
deferStartupSidecars: true,
|
||||
});
|
||||
cleanup.push(() => server.close());
|
||||
|
||||
const operator = await connectGatewayClient({
|
||||
url: `ws://127.0.0.1:${port}`,
|
||||
token,
|
||||
clientName: GATEWAY_CLIENT_NAMES.TEST,
|
||||
clientDisplayName: "approval operator",
|
||||
mode: GATEWAY_CLIENT_MODES.TEST,
|
||||
scopes: [ADMIN_SCOPE],
|
||||
requestTimeoutMs: GATEWAY_CONNECT_TIMEOUT_MS,
|
||||
timeoutMs: GATEWAY_CONNECT_TIMEOUT_MS,
|
||||
});
|
||||
cleanup.push(() => disconnectGatewayClient(operator));
|
||||
|
||||
let resolveOutcome: (outcome: ExecApprovalFollowupOutcome) => void = () => {};
|
||||
const outcomePromise = new Promise<ExecApprovalFollowupOutcome>((resolve) => {
|
||||
resolveOutcome = resolve;
|
||||
});
|
||||
|
||||
const tool = createExecTool({
|
||||
host: "gateway",
|
||||
security: "allowlist",
|
||||
ask: "always",
|
||||
cwd: workspaceDir,
|
||||
approvalRunningNoticeMs: 0,
|
||||
approvalFollowupMode: "direct",
|
||||
approvalFollowup: ({ outcome }) => {
|
||||
resolveOutcome(outcome);
|
||||
return undefined;
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
});
|
||||
|
||||
process.env.HOME = tempHome;
|
||||
process.env.OPENCLAW_STATE_DIR = stateDir;
|
||||
process.env.OPENCLAW_CONFIG_PATH = configPath;
|
||||
process.env.OPENCLAW_GATEWAY_TOKEN = token;
|
||||
process.env.OPENCLAW_GATEWAY_PORT = String(port);
|
||||
process.env.OPENCLAW_SKIP_CHANNELS = "1";
|
||||
process.env.OPENCLAW_SKIP_GMAIL_WATCHER = "1";
|
||||
process.env.OPENCLAW_SKIP_CRON = "1";
|
||||
process.env.OPENCLAW_SKIP_CANVAS_HOST = "1";
|
||||
process.env.OPENCLAW_SKIP_BROWSER_CONTROL_SERVER = "1";
|
||||
process.env.OPENCLAW_SKIP_PROVIDERS = "1";
|
||||
process.env.OPENCLAW_TEST_MINIMAL_GATEWAY = "1";
|
||||
clearRuntimeConfigSnapshot();
|
||||
clearConfigCache();
|
||||
clearSessionStoreCacheForTest();
|
||||
const pending = await tool.execute("exec-approval-e2e", {
|
||||
command: "printf 'smoke\\n'",
|
||||
workdir: workspaceDir,
|
||||
timeout: 5,
|
||||
});
|
||||
|
||||
const server = await startGatewayServer(port, {
|
||||
bind: "loopback",
|
||||
auth: { mode: "token", token },
|
||||
controlUiEnabled: false,
|
||||
deferStartupSidecars: true,
|
||||
});
|
||||
cleanup.push(() => server.close());
|
||||
expect(pending.details.status).toBe("approval-pending");
|
||||
if (pending.details.status !== "approval-pending") {
|
||||
throw new Error("expected approval-pending exec result");
|
||||
}
|
||||
|
||||
const operator = await connectGatewayClient({
|
||||
url: `ws://127.0.0.1:${port}`,
|
||||
token,
|
||||
clientName: GATEWAY_CLIENT_NAMES.TEST,
|
||||
clientDisplayName: "approval operator",
|
||||
mode: GATEWAY_CLIENT_MODES.TEST,
|
||||
scopes: [ADMIN_SCOPE],
|
||||
requestTimeoutMs: GATEWAY_CONNECT_TIMEOUT_MS,
|
||||
timeoutMs: GATEWAY_CONNECT_TIMEOUT_MS,
|
||||
});
|
||||
cleanup.push(() => disconnectGatewayClient(operator));
|
||||
await operator.request(
|
||||
"exec.approval.resolve",
|
||||
{ id: pending.details.approvalId, decision: "allow-once" },
|
||||
{ timeoutMs: 10_000 },
|
||||
);
|
||||
|
||||
let resolveOutcome: (outcome: ExecApprovalFollowupOutcome) => void = () => {};
|
||||
const outcomePromise = new Promise<ExecApprovalFollowupOutcome>((resolve) => {
|
||||
resolveOutcome = resolve;
|
||||
});
|
||||
|
||||
const tool = createExecTool({
|
||||
host: "gateway",
|
||||
security: "allowlist",
|
||||
ask: "always",
|
||||
cwd: workspaceDir,
|
||||
approvalRunningNoticeMs: 0,
|
||||
approvalFollowupMode: "direct",
|
||||
approvalFollowup: ({ outcome }) => {
|
||||
resolveOutcome(outcome);
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const pending = await tool.execute("exec-approval-e2e", {
|
||||
command: "printf 'smoke\\n'",
|
||||
workdir: workspaceDir,
|
||||
timeout: 5,
|
||||
});
|
||||
|
||||
expect(pending.details.status).toBe("approval-pending");
|
||||
if (pending.details.status !== "approval-pending") {
|
||||
throw new Error("expected approval-pending exec result");
|
||||
}
|
||||
|
||||
await operator.request(
|
||||
"exec.approval.resolve",
|
||||
{ id: pending.details.approvalId, decision: "allow-once" },
|
||||
{ timeoutMs: 10_000 },
|
||||
);
|
||||
|
||||
const outcome = await withTimeout(outcomePromise, 15_000, "approved exec outcome");
|
||||
expect(outcome.status).toBe("completed");
|
||||
expect(outcome.exitCode).toBe(0);
|
||||
expect(outcome.aggregated).toBe("smoke");
|
||||
}, EXEC_APPROVAL_E2E_TIMEOUT_MS);
|
||||
const outcome = await withTimeout(outcomePromise, 15_000, "approved exec outcome");
|
||||
expect(outcome.status).toBe("completed");
|
||||
expect(outcome.exitCode).toBe(0);
|
||||
expect(outcome.aggregated).toBe("smoke");
|
||||
},
|
||||
EXEC_APPROVAL_E2E_TIMEOUT_MS,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Exec approval id routing tests.
|
||||
* Covers approval registration ids, follow-up idempotency, and approved
|
||||
* node/gateway invocation behavior.
|
||||
*/
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Exec background abort tests.
|
||||
* Ensures agent-turn aborts stop foreground execs but do not kill already
|
||||
* backgrounded sessions.
|
||||
*/
|
||||
import { afterEach, beforeAll, beforeEach, expect, test, vi } from "vitest";
|
||||
import { killProcessTree } from "../process/kill-tree.js";
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Exec PATH handling tests.
|
||||
* Covers shell snapshot PATH merging, pathPrepend behavior, and host env
|
||||
* sanitization for gateway/sandbox execution.
|
||||
*/
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Exec PTY integration tests.
|
||||
* Starts PTY sessions, polls them through the process tool, and verifies
|
||||
* terminal input/output handling.
|
||||
*/
|
||||
import { afterEach, expect, test } from "vitest";
|
||||
import { markBackgrounded, resetProcessRegistryForTests } from "./bash-process-registry.js";
|
||||
import { runExecProcess } from "./bash-tools.exec-runtime.js";
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Resolve-exec-env hook tests.
|
||||
* Verifies plugin-provided env values are filtered and forwarded to the chosen
|
||||
* exec host without leaking unsafe overrides.
|
||||
*/
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { OPENCLAW_CLI_ENV_VALUE } from "../infra/openclaw-exec-env.js";
|
||||
import type { ExtensionContext } from "./sessions/index.js";
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Exec script preflight tests.
|
||||
* Covers Python/Node script file validation, shell-bleed detection, and
|
||||
* symlink/path race handling before execution.
|
||||
*/
|
||||
import { constants as fsConstants } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Exec security floor tests.
|
||||
* Verifies tool config and exec-approvals policy combine by tightening
|
||||
* security/ask rather than silently broadening execution.
|
||||
*/
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Exec tool factory and request pipeline.
|
||||
* Resolves host/sandbox/node target, policy, approval, env, script preflight,
|
||||
* process launch, foreground result, and background session handoff.
|
||||
*/
|
||||
import { constants as fsConstants } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
@@ -1312,6 +1317,7 @@ function resolveExecReviewerDefaults(params: { defaults?: ExecToolDefaults; agen
|
||||
return agentExec?.reviewer ?? cfg?.tools?.exec?.reviewer;
|
||||
}
|
||||
|
||||
/** Creates an exec tool instance with runtime defaults and approval policy wiring. */
|
||||
export function createExecTool(
|
||||
defaults?: ExecToolDefaults,
|
||||
): AgentToolWithMeta<typeof execSchema, ExecToolDetails> {
|
||||
@@ -1974,8 +1980,10 @@ export function createExecTool(
|
||||
};
|
||||
}
|
||||
|
||||
/** Default exec tool instance used by agent tool registries. */
|
||||
export const execTool = createExecTool();
|
||||
|
||||
/** Test-only seams for parser/preflight helpers. */
|
||||
export const testing = {
|
||||
parseOpenClawChannelsLoginShellCommand,
|
||||
validateScriptFileForShellBleed,
|
||||
|
||||
Reference in New Issue
Block a user