/** * SHELL_HISTORY provider — injects recent shell activity into agent context: the * last commands with stdout/stderr/exit codes, the current and allowed working * directories, and recent file operations. Fires only in terminal/code contexts * and reads its state from ShellService. */ import { addHeader, type IAgentRuntime, logger, type Memory, type Provider, type State, } from "@elizaos/core"; import { requireProviderSpec } from "../generated/specs/spec-helpers"; import type { ShellService } from "../services/shellService"; import type { CommandHistoryEntry, FileOperation } from "../types"; const MAX_OUTPUT_LENGTH = 8000; const TRUNCATE_SEGMENT_LENGTH = 4000; const spec = requireProviderSpec("SHELL_HISTORY"); export const shellHistoryProvider: Provider = { name: spec.name, description: "Provides recent shell command history, current working directory, and file operations within the restricted environment", descriptionCompressed: "Recent shell history, cwd, and file ops in restricted env.", position: 99, contexts: ["terminal", "code"], contextGate: { anyOf: ["terminal", "code"] }, cacheStable: false, cacheScope: "turn", // Shell history / cwd / file ops are host-operator context — admin+ only. // (#12094 item 3: the gate lives on the provider so it can't drift.) roleGate: { minRole: "ADMIN" }, dynamic: true, get: async (runtime: IAgentRuntime, message: Memory, _state: State) => { try { const shellService = runtime.getService("shell"); if (!shellService) { logger.warn("[shellHistoryProvider] Shell service not found"); return { values: { shellHistory: "Shell service is not available", currentWorkingDirectory: "N/A", allowedDirectory: "N/A", }, text: addHeader("# Shell Status", "Shell service is not available"), data: { historyCount: 0, cwd: "N/A", allowedDir: "N/A" }, }; } const conversationId = message.roomId || message.agentId; if (!conversationId) { return { text: "No conversation ID available", values: { historyCount: 0, cwd: "N/A", allowedDir: "N/A" }, data: { historyCount: 0, cwd: "N/A", allowedDir: "N/A" }, }; } const history = shellService.getCommandHistory(conversationId, 10); const cwd = shellService.getCurrentDirectory(conversationId); const allowedDir = shellService.getAllowedDirectory(); let historyText = "No commands in history."; if (history.length > 0) { historyText = history .map((entry: CommandHistoryEntry) => { let entryStr = `[${new Date(entry.timestamp).toISOString()}] ${entry.workingDirectory}> ${entry.command}`; if (entry.stdout) { if (entry.stdout.length > MAX_OUTPUT_LENGTH) { entryStr += `\n Output: ${entry.stdout.substring(0, TRUNCATE_SEGMENT_LENGTH)}\n ... [TRUNCATED] ...\n ${entry.stdout.substring(entry.stdout.length - TRUNCATE_SEGMENT_LENGTH)}`; } else { entryStr += `\n Output: ${entry.stdout}`; } } if (entry.stderr) { if (entry.stderr.length > MAX_OUTPUT_LENGTH) { entryStr += `\n Error: ${entry.stderr.substring(0, TRUNCATE_SEGMENT_LENGTH)}\n ... [TRUNCATED] ...\n ${entry.stderr.substring(entry.stderr.length - TRUNCATE_SEGMENT_LENGTH)}`; } else { entryStr += `\n Error: ${entry.stderr}`; } } entryStr += `\n Exit Code: ${entry.exitCode}`; if (entry.fileOperations && entry.fileOperations.length > 0) { entryStr += "\n File Operations:"; entry.fileOperations.forEach((op: FileOperation) => { if (op.secondaryTarget) { entryStr += `\n - ${op.type}: ${op.target} → ${op.secondaryTarget}`; } else { entryStr += `\n - ${op.type}: ${op.target}`; } }); } return entryStr; }) .join("\n\n"); } const recentFileOps = history .filter( (entry: CommandHistoryEntry) => entry.fileOperations && entry.fileOperations.length > 0 ) .flatMap((entry: CommandHistoryEntry) => entry.fileOperations ?? []) .slice(-5); let fileOpsText = ""; if (recentFileOps.length > 0) { fileOpsText = "\n\n" + addHeader( "# Recent File Operations", recentFileOps .map((op: FileOperation) => { if (op.secondaryTarget) { return `- ${op.type}: ${op.target} → ${op.secondaryTarget}`; } return `- ${op.type}: ${op.target}`; }) .join("\n") ); } const text = `Current Directory: ${cwd} Allowed Directory: ${allowedDir} ${addHeader("# Shell History (Last 10)", historyText)}${fileOpsText}`; return { values: { shellHistory: historyText, currentWorkingDirectory: cwd, allowedDirectory: allowedDir, }, text, data: { historyCount: history.length, cwd, allowedDir, }, }; } catch (error) { // error-policy:J4 designed unavailable render; the provider emits a // distinguishable "Shell history is unavailable: " status AND reports // the failure via reportError — never a healthy-looking empty history. // // Surface the failure through the runtime diagnostic boundary AND as a // model-visible status line. A bare `catch {}` that returned an empty // string here hid real ShellService failures from both the operator logs // and the planner loop, presenting success-shaped empty output instead of // an error the model could react to (#12273/#12799). // // `runtime.reportError` (#12263) is the diagnostic boundary for provider // failures: it logs with a `[scope]` prefix, emits ERROR_REPORTED, feeds // the RECENT_ERRORS provider (so repeated shell-history backend failures // become observable to the agent), and drives owner escalation. It never // throws. Fall back to logger.error on runtimes/test doubles that predate // it so the failure is never silently swallowed. const errMsg = error instanceof Error ? error.message : String(error); if (typeof runtime?.reportError === "function") { runtime.reportError("shellHistoryProvider", error, { roomId: message.roomId, agentId: message.agentId, }); } else { logger.error( { src: "shellHistoryProvider", error }, `[shellHistoryProvider] Failed to build shell history context: ${errMsg}` ); } const statusText = `Shell history is unavailable: ${errMsg}`; return { values: { shellHistory: statusText, currentWorkingDirectory: "N/A", allowedDirectory: "N/A", }, text: addHeader("# Shell Status", statusText), data: { historyCount: 0, cwd: "N/A", allowedDir: "N/A", error: errMsg }, }; } }, }; export default shellHistoryProvider;