docs: document agent command exec helpers

This commit is contained in:
Peter Steinberger
2026-06-03 22:09:31 -04:00
parent 233666366f
commit 2e89655a03
5 changed files with 27 additions and 0 deletions

View File

@@ -1,17 +1,20 @@
import type { AgentMessage } from "../runtime/index.js";
/** Mutable lifecycle flags observed while a single agent attempt runs. */
export type AgentAttemptLifecycleState = {
currentTurnUserMessagePersisted: boolean;
lifecycleFinishing: boolean;
lifecycleEnded: boolean;
};
/** Event shape emitted by runtimes during an agent attempt. */
export type AgentAttemptLifecycleEvent = {
stream: string;
data?: Record<string, unknown>;
sessionKey?: string;
};
/** Creates callbacks that update lifecycle flags for persistence decisions. */
export function createAgentAttemptLifecycleCallbacks(state: AgentAttemptLifecycleState): {
onUserMessagePersisted: (message: Extract<AgentMessage, { role: "user" }>) => void;
onAgentEvent: (evt: AgentAttemptLifecycleEvent) => void;
@@ -24,6 +27,8 @@ export function createAgentAttemptLifecycleCallbacks(state: AgentAttemptLifecycl
if (evt.stream !== "lifecycle" || typeof evt.data?.phase !== "string") {
return;
}
// Finishing means output ended but transcript/session persistence may still
// need to run; end/error means the runtime lifecycle is complete.
if (evt.data.phase === "finishing") {
state.lifecycleFinishing = true;
return;

View File

@@ -6,6 +6,8 @@ import { normalizeOptionalString } from "@openclaw/normalization-core/string-coe
const CLAUDE_PROJECTS_DIRNAME = path.join(".claude", "projects");
const MAX_SANITIZED_PROJECT_LENGTH = 200;
// Claude CLI stores project state under a sanitized workspace key. Add a stable
// hash when the key is truncated so long paths do not collide silently.
function simpleHash36(input: string): string {
let hash = 0;
for (let index = 0; index < input.length; index += 1) {
@@ -22,6 +24,8 @@ function sanitizeClaudeCliProjectKey(workspaceDir: string): string {
return `${sanitized.slice(0, MAX_SANITIZED_PROJECT_LENGTH)}-${simpleHash36(workspaceDir)}`;
}
// Realpath when possible so symlinked workspaces reuse the same Claude project
// directory as their canonical path.
function canonicalizeWorkspaceDir(workspaceDir: string): string {
const resolved = path.resolve(workspaceDir).normalize("NFC");
try {
@@ -31,6 +35,7 @@ function canonicalizeWorkspaceDir(workspaceDir: string): string {
}
}
/** Resolves Claude CLI's per-workspace project directory. */
export function resolveClaudeCliProjectDirForWorkspace(params: {
workspaceDir: string;
homeDir?: string;

View File

@@ -1,2 +1,4 @@
// Runtime barrel for session-store writes; keeps command modules from importing
// config/session persistence until an agent run needs to save state.
export { updateSessionStoreAfterAgentRun } from "./session-store.js";
export { loadSessionStore } from "../../config/sessions.js";

View File

@@ -21,6 +21,8 @@ import { resolveAgentConfig, resolveSessionAgentId } from "./agent-scope.js";
import { isRequestedExecTargetAllowed, resolveExecTarget } from "./bash-tools.exec-runtime.js";
import { resolveSandboxRuntimeStatus } from "./sandbox/runtime-status.js";
// Resolved exec config layers come from global config, agent config, legacy
// session fields, and per-call overrides.
type ResolvedExecConfig = {
host?: ExecTarget;
mode?: ExecMode;
@@ -31,10 +33,14 @@ type ResolvedExecConfig = {
type ExecOverridesConfig = Omit<ResolvedExecConfig, "mode">;
// Legacy security/ask values remain accepted on existing sessions/config, but
// mode wins when present because it expands to a complete policy tuple.
function hasLegacyExecPolicyOverride(exec?: ResolvedExecConfig): boolean {
return exec?.security !== undefined || exec?.ask !== undefined;
}
// Layering keeps the most specific mode/security/ask while preserving policy
// bounds from approvals and sandbox availability later in resolution.
type LayeredExecPolicy = {
mode?: ExecMode;
security: ExecSecurity;
@@ -78,6 +84,8 @@ function applySessionLegacyExecPolicyLayer(
return base;
}
// Gather the shared config state once so canExecRequestNode and
// resolveExecDefaults stay aligned on agent/global/session precedence.
function resolveExecConfigState(params: {
cfg?: OpenClawConfig;
sessionEntry?: SessionEntry;
@@ -133,6 +141,7 @@ function resolveExecSandboxAvailability(params: {
);
}
/** Returns whether the current exec policy allows requesting host node execution. */
export function canExecRequestNode(params: {
cfg?: OpenClawConfig;
sessionEntry?: SessionEntry;
@@ -153,6 +162,7 @@ export function canExecRequestNode(params: {
});
}
/** Resolves effective exec host, mode, approval policy, and node availability. */
export function resolveExecDefaults(params: {
cfg?: OpenClawConfig;
sessionEntry?: SessionEntry;
@@ -211,6 +221,8 @@ export function resolveExecDefaults(params: {
params.execOverrides,
);
const modePolicy = resolveExecModePolicy(layeredPolicy);
// Approval files are safety bounds: they can only reduce security/ask from
// config-derived policy, never grant a less restrictive effective mode.
const security =
approvalDefaults?.security !== undefined
? minSecurity(modePolicy.security, approvalDefaults.security)

View File

@@ -2,11 +2,13 @@ import { promises as fs } from "node:fs";
import path from "node:path";
import { resolveStateDir } from "../config/paths.js";
/** Resolves the private transcript path for an internal session-effect run. */
export function resolveInternalSessionEffectsTranscriptPath(runId: string): string {
const safeRunId = runId.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 120) || "run";
return path.join(resolveStateDir(), "internal-agent-runs", `${safeRunId}.jsonl`);
}
/** Copies or creates a private transcript for internal session-effect recovery. */
export async function prepareInternalSessionEffectsTranscript(params: {
sessionFile?: string;
runId: string;
@@ -34,6 +36,7 @@ export async function prepareInternalSessionEffectsTranscript(params: {
return sessionFile;
}
/** Removes an internal session-effect transcript if it is inside the owned dir. */
export async function removeInternalSessionEffectsTranscript(
sessionFile: string | undefined,
): Promise<void> {