From 045145c700824707aafc4d78c1955641c0be7992 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 4 Jun 2026 06:13:45 -0400 Subject: [PATCH] docs: document exec runtime --- ...sh-tools.exec-runtime.pty-fallback.test.ts | 5 +++ src/agents/bash-tools.exec-runtime.test.ts | 5 +++ src/agents/bash-tools.exec-runtime.ts | 34 ++++++++++++++++--- src/agents/bash-tools.exec-types.ts | 10 ++++++ 4 files changed, 50 insertions(+), 4 deletions(-) diff --git a/src/agents/bash-tools.exec-runtime.pty-fallback.test.ts b/src/agents/bash-tools.exec-runtime.pty-fallback.test.ts index 92495428407..907c4edc5ec 100644 --- a/src/agents/bash-tools.exec-runtime.pty-fallback.test.ts +++ b/src/agents/bash-tools.exec-runtime.pty-fallback.test.ts @@ -1,3 +1,8 @@ +/** + * PTY fallback runtime tests. + * Verifies PTY-requested exec sessions can fall back through the supervisor + * path while still emitting diagnostics and registry state. + */ import { afterEach, beforeAll, beforeEach, expect, test, vi } from "vitest"; import { onInternalDiagnosticEvent, diff --git a/src/agents/bash-tools.exec-runtime.test.ts b/src/agents/bash-tools.exec-runtime.test.ts index e582980518b..15c909626b1 100644 --- a/src/agents/bash-tools.exec-runtime.test.ts +++ b/src/agents/bash-tools.exec-runtime.test.ts @@ -1,3 +1,8 @@ +/** + * Exec runtime tests. + * Covers target resolution, cursor mode tracking, exit outcome classification, + * system events, and process lifecycle behavior. + */ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { MAX_SAFE_TIMEOUT_DELAY_MS } from "../utils/timer-delay.js"; diff --git a/src/agents/bash-tools.exec-runtime.ts b/src/agents/bash-tools.exec-runtime.ts index c425e0dccb0..c85b07ffbbd 100644 --- a/src/agents/bash-tools.exec-runtime.ts +++ b/src/agents/bash-tools.exec-runtime.ts @@ -1,3 +1,8 @@ +/** + * Bash exec runtime. + * Spawns host/sandbox processes, manages session updates/backgrounding, + * approval messaging constants, environment safety, and exit outcome shaping. + */ import path from "node:path"; import { normalizeStringEntries } from "@openclaw/normalization-core/string-normalization"; import { emitDiagnosticEvent } from "../infra/diagnostic-events.js"; @@ -84,8 +89,7 @@ export function detectCursorKeyMode(raw: string): "application" | "normal" | nul return lastSmkx > lastRmkx ? "application" : "normal"; } -// Sanitize inherited host env before merge so dangerous variables from process.env -// are not propagated into non-sandboxed executions. +/** Removes dangerous inherited host env vars before non-sandboxed execution. */ export function sanitizeHostBaseEnv(env: Record): Record { const sanitized: Record = {}; for (const [key, value] of Object.entries(env)) { @@ -101,8 +105,7 @@ export function sanitizeHostBaseEnv(env: Record): Record): void { for (const key of Object.keys(env)) { const upperKey = key.toUpperCase(); @@ -123,27 +126,34 @@ export function validateHostEnv(env: Record): void { } } } +/** Default retained aggregate output cap for exec sessions. */ export const DEFAULT_MAX_OUTPUT = clampWithDefault( readEnvInt("OPENCLAW_BASH_MAX_OUTPUT_CHARS", "PI_BASH_MAX_OUTPUT_CHARS"), 200_000, 1_000, 200_000, ); +/** Default pending output cap for poll/update buffers. */ export const DEFAULT_PENDING_MAX_OUTPUT = clampWithDefault( readEnvInt("OPENCLAW_BASH_PENDING_MAX_OUTPUT_CHARS"), 30_000, 1_000, 200_000, ); +/** Fallback PATH used when the process environment has no PATH. */ export const DEFAULT_PATH = process.env.PATH ?? "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"; +/** Tail length used in background completion notifications. */ export const DEFAULT_NOTIFY_TAIL_CHARS = 400; const DEFAULT_NOTIFY_SNIPPET_CHARS = 180; +/** Default time an approval can remain pending. */ export const DEFAULT_APPROVAL_TIMEOUT_MS = DEFAULT_EXEC_APPROVAL_TIMEOUT_MS; +/** Gateway request timeout for approval registration/wait calls. */ export const DEFAULT_APPROVAL_REQUEST_TIMEOUT_MS = DEFAULT_APPROVAL_TIMEOUT_MS + 10_000; const DEFAULT_APPROVAL_RUNNING_NOTICE_MS = 10_000; const APPROVAL_SLUG_LENGTH = 8; +/** Failure categories used to explain exec process exits. */ export type ExecProcessFailureKind = | "shell-command-not-found" | "shell-not-executable" @@ -155,6 +165,7 @@ export type ExecProcessFailureKind = type ExecExitFailureKind = Exclude; +/** Normalized result of a spawned exec process. */ export type ExecProcessOutcome = | { status: "completed"; @@ -175,6 +186,7 @@ export type ExecProcessOutcome = reason: string; }; +/** Live handle returned after an exec process has started. */ export type ExecProcessHandle = { session: ProcessSession; startedAt: number; @@ -219,14 +231,17 @@ function emitExecProcessCompleted(params: { }); } +/** Renders a host label for user-facing exec policy messages. */ export function renderExecHostLabel(host: ExecHost) { return host === "sandbox" ? "sandbox" : host === "gateway" ? "gateway" : "node"; } +/** Renders an exec target label, preserving `auto`. */ export function renderExecTargetLabel(target: ExecTarget) { return target === "auto" ? "auto" : renderExecHostLabel(target); } +/** Returns true when a per-call target override is allowed by configured policy. */ export function isRequestedExecTargetAllowed(params: { configuredTarget: ExecTarget; requestedTarget: ExecTarget; @@ -247,6 +262,7 @@ export function isRequestedExecTargetAllowed(params: { return false; } +/** Resolves configured/requested/elevated exec target into an effective host. */ export function resolveExecTarget(params: { configuredTarget?: ExecTarget; requestedTarget?: ExecTarget | null; @@ -296,6 +312,7 @@ export function resolveExecTarget(params: { }; } +/** Normalizes notification snippets to a compact single-line form. */ export function normalizeNotifyOutput(value: string) { return value.replace(/\s+/g, " ").trim(); } @@ -312,6 +329,7 @@ function compactNotifyOutput(value: string, maxChars = DEFAULT_NOTIFY_SNIPPET_CH return `${normalized.slice(0, safe)}…`; } +/** Merges shell-discovered PATH entries into an exec environment. */ export function applyShellPath(env: Record, shellPath?: string | null) { if (!shellPath) { return; @@ -377,10 +395,12 @@ function maybeNotifyOnExit(session: ProcessSession, status: "completed" | "faile } } +/** Creates the short approval id shown in `/approve` prompts. */ export function createApprovalSlug(id: string) { return id.slice(0, APPROVAL_SLUG_LENGTH); } +/** Builds the user-facing approval-pending message for foreground exec. */ export function buildApprovalPendingMessage(params: { warningText?: string; approvalSlug: string; @@ -427,6 +447,7 @@ export function buildApprovalPendingMessage(params: { return lines.join("\n"); } +/** Normalizes the delay before showing a running approval notice. */ export function resolveApprovalRunningNoticeMs(value?: number) { if (typeof value !== "number" || !Number.isFinite(value)) { return DEFAULT_APPROVAL_RUNNING_NOTICE_MS; @@ -437,6 +458,7 @@ export function resolveApprovalRunningNoticeMs(value?: number) { return Math.floor(value); } +/** Emits an exec system event and wakes the routed session when appropriate. */ export function emitExecSystemEvent( text: string, opts: { @@ -510,6 +532,7 @@ function classifyExecFailureKind(params: { return "aborted"; } +/** Formats a user-facing reason for a failed exec process exit. */ export function formatExecFailureReason(params: { failureKind: ExecExitFailureKind; exitSignal: NodeJS.Signals | number | null; @@ -534,6 +557,7 @@ export function formatExecFailureReason(params: { throw new Error("Unsupported exec failure kind"); } +/** Converts a supervisor exit record into a normalized exec process outcome. */ export function buildExecExitOutcome(params: { exit: RunExit; aggregated: string; @@ -579,6 +603,7 @@ export function buildExecExitOutcome(params: { }; } +/** Converts spawn/runtime errors into a normalized failed exec outcome. */ export function buildExecRuntimeErrorOutcome(params: { error: unknown; aggregated: string; @@ -631,6 +656,7 @@ function wrapPosixCommandWithPathPrepend( return `export PATH="\${OPENCLAW_PREPEND_PATH}\${PATH:+:$PATH}"; unset OPENCLAW_PREPEND_PATH; ${command}`; } +/** Starts a host or sandbox exec process and registers it for polling/backgrounding. */ export async function runExecProcess(opts: { command: string; // Execute this instead of `command` (which is kept for display/session/logging). diff --git a/src/agents/bash-tools.exec-types.ts b/src/agents/bash-tools.exec-types.ts index 95a0367a446..25c20ff6cdc 100644 --- a/src/agents/bash-tools.exec-types.ts +++ b/src/agents/bash-tools.exec-types.ts @@ -1,3 +1,8 @@ +/** + * Shared type contracts for bash exec tools. + * Defines defaults, approval follow-up payloads, elevated policy defaults, and + * tool result details consumed across exec hosts and process controls. + */ import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { EventSessionRoutingPolicy } from "../infra/event-session-routing.js"; import type { ExecApprovalDecision } from "../infra/exec-approvals.js"; @@ -14,6 +19,7 @@ import type { BashSandboxConfig } from "./bash-tools.shared.js"; import type { EmbeddedFullAccessBlockedReason } from "./embedded-agent-runner/types.js"; import type { ExecReviewerConfig } from "./exec-auto-reviewer.js"; +/** Runtime defaults passed into exec/process tool factories. */ export type ExecToolDefaults = { hasCronTool?: boolean; host?: ExecTarget; @@ -63,6 +69,7 @@ export type ExecToolDefaults = { cwd?: string; }; +/** Outcome passed to approval follow-up factories after approved async exec. */ export type ExecApprovalFollowupOutcome = { status: "completed" | "failed"; exitCode: number | null; @@ -78,10 +85,12 @@ type ExecApprovalFollowupContext = { outcome: ExecApprovalFollowupOutcome; }; +/** Hook that can append domain-specific text to approval follow-up messages. */ export type ExecApprovalFollowupFactory = ( context: ExecApprovalFollowupContext, ) => string | undefined | Promise; +/** Effective elevated-exec defaults derived from config/runtime policy. */ export type ExecElevatedDefaults = { enabled: boolean; allowed: boolean; @@ -90,6 +99,7 @@ export type ExecElevatedDefaults = { fullAccessBlockedReason?: EmbeddedFullAccessBlockedReason; }; +/** Structured details returned by exec tool calls. */ export type ExecToolDetails = | { status: "running";