From 2feb81249fbc2008da76e77d582cb2c419077dbb Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 4 Jun 2026 06:16:34 -0400 Subject: [PATCH] docs: document exec tool entry --- ...ash-tools.exec-foreground-failures.test.ts | 5 + ...sh-tools.exec-gateway-approval.e2e.test.ts | 215 +++++++++--------- .../bash-tools.exec.approval-id.test.ts | 5 + .../bash-tools.exec.background-abort.test.ts | 5 + src/agents/bash-tools.exec.path.test.ts | 5 + src/agents/bash-tools.exec.pty.test.ts | 5 + .../bash-tools.exec.resolve-env-hook.test.ts | 5 + .../bash-tools.exec.script-preflight.test.ts | 5 + .../bash-tools.exec.security-floor.test.ts | 5 + src/agents/bash-tools.exec.ts | 8 + 10 files changed, 160 insertions(+), 103 deletions(-) diff --git a/src/agents/bash-tools.exec-foreground-failures.test.ts b/src/agents/bash-tools.exec-foreground-failures.test.ts index 22dbc26b3dc..a79a2e46e23 100644 --- a/src/agents/bash-tools.exec-foreground-failures.test.ts +++ b/src/agents/bash-tools.exec-foreground-failures.test.ts @@ -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"; diff --git a/src/agents/bash-tools.exec-gateway-approval.e2e.test.ts b/src/agents/bash-tools.exec-gateway-approval.e2e.test.ts index 83e81d3b346..e6038693601 100644 --- a/src/agents/bash-tools.exec-gateway-approval.e2e.test.ts +++ b/src/agents/bash-tools.exec-gateway-approval.e2e.test.ts @@ -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((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((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, + ); }); diff --git a/src/agents/bash-tools.exec.approval-id.test.ts b/src/agents/bash-tools.exec.approval-id.test.ts index 58a868ce0e0..7cb07f636af 100644 --- a/src/agents/bash-tools.exec.approval-id.test.ts +++ b/src/agents/bash-tools.exec.approval-id.test.ts @@ -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"; diff --git a/src/agents/bash-tools.exec.background-abort.test.ts b/src/agents/bash-tools.exec.background-abort.test.ts index a4d78639992..64026f989fc 100644 --- a/src/agents/bash-tools.exec.background-abort.test.ts +++ b/src/agents/bash-tools.exec.background-abort.test.ts @@ -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"; diff --git a/src/agents/bash-tools.exec.path.test.ts b/src/agents/bash-tools.exec.path.test.ts index 81cc232fca8..63be07d53ed 100644 --- a/src/agents/bash-tools.exec.path.test.ts +++ b/src/agents/bash-tools.exec.path.test.ts @@ -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"; diff --git a/src/agents/bash-tools.exec.pty.test.ts b/src/agents/bash-tools.exec.pty.test.ts index a90b98d8fb0..d814bf93a34 100644 --- a/src/agents/bash-tools.exec.pty.test.ts +++ b/src/agents/bash-tools.exec.pty.test.ts @@ -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"; diff --git a/src/agents/bash-tools.exec.resolve-env-hook.test.ts b/src/agents/bash-tools.exec.resolve-env-hook.test.ts index fb5f7139edf..f82d9ab4a8d 100644 --- a/src/agents/bash-tools.exec.resolve-env-hook.test.ts +++ b/src/agents/bash-tools.exec.resolve-env-hook.test.ts @@ -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"; diff --git a/src/agents/bash-tools.exec.script-preflight.test.ts b/src/agents/bash-tools.exec.script-preflight.test.ts index e188b27f648..0ded93c138d 100644 --- a/src/agents/bash-tools.exec.script-preflight.test.ts +++ b/src/agents/bash-tools.exec.script-preflight.test.ts @@ -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"; diff --git a/src/agents/bash-tools.exec.security-floor.test.ts b/src/agents/bash-tools.exec.security-floor.test.ts index 94db0630529..530934e61ff 100644 --- a/src/agents/bash-tools.exec.security-floor.test.ts +++ b/src/agents/bash-tools.exec.security-floor.test.ts @@ -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"; diff --git a/src/agents/bash-tools.exec.ts b/src/agents/bash-tools.exec.ts index 1b56b89a77d..907587b0e25 100644 --- a/src/agents/bash-tools.exec.ts +++ b/src/agents/bash-tools.exec.ts @@ -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 { @@ -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,