agent-world 0.11.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/README.md +17 -7
  2. package/dist/cli/commands.d.ts +109 -0
  3. package/dist/cli/commands.js +2024 -0
  4. package/dist/cli/display.d.ts +124 -0
  5. package/dist/cli/display.js +381 -0
  6. package/dist/cli/hitl.d.ts +33 -0
  7. package/dist/cli/hitl.js +81 -0
  8. package/dist/cli/index.d.ts +2 -0
  9. package/dist/cli/stream.d.ts +41 -0
  10. package/dist/cli/stream.js +222 -0
  11. package/dist/core/activity-tracker.d.ts +16 -0
  12. package/dist/core/activity-tracker.d.ts.map +1 -0
  13. package/dist/core/activity-tracker.js +91 -0
  14. package/dist/core/activity-tracker.js.map +1 -0
  15. package/dist/core/ai-commands.d.ts +16 -0
  16. package/dist/core/ai-commands.d.ts.map +1 -0
  17. package/dist/core/ai-commands.js +24 -0
  18. package/dist/core/ai-commands.js.map +1 -0
  19. package/dist/core/ai-sdk-patch.d.ts +24 -0
  20. package/dist/core/ai-sdk-patch.d.ts.map +1 -0
  21. package/dist/core/ai-sdk-patch.js +169 -0
  22. package/dist/core/ai-sdk-patch.js.map +1 -0
  23. package/dist/core/anthropic-direct.d.ts +52 -0
  24. package/dist/core/anthropic-direct.d.ts.map +1 -0
  25. package/dist/core/anthropic-direct.js +301 -0
  26. package/dist/core/anthropic-direct.js.map +1 -0
  27. package/dist/core/approval-cache.d.ts +104 -0
  28. package/dist/core/approval-cache.d.ts.map +1 -0
  29. package/dist/core/approval-cache.js +150 -0
  30. package/dist/core/approval-cache.js.map +1 -0
  31. package/dist/core/chat-constants.d.ts +20 -0
  32. package/dist/core/chat-constants.d.ts.map +1 -0
  33. package/dist/core/chat-constants.js +22 -0
  34. package/dist/core/chat-constants.js.map +1 -0
  35. package/dist/core/create-agent-tool.d.ts +66 -0
  36. package/dist/core/create-agent-tool.d.ts.map +1 -0
  37. package/dist/core/create-agent-tool.js +212 -0
  38. package/dist/core/create-agent-tool.js.map +1 -0
  39. package/dist/core/events/approval-checker.d.ts +61 -0
  40. package/dist/core/events/approval-checker.d.ts.map +1 -0
  41. package/dist/core/events/approval-checker.js +226 -0
  42. package/dist/core/events/approval-checker.js.map +1 -0
  43. package/dist/core/events/index.d.ts +25 -0
  44. package/dist/core/events/index.d.ts.map +1 -0
  45. package/dist/core/events/index.js +30 -0
  46. package/dist/core/events/index.js.map +1 -0
  47. package/dist/core/events/memory-manager.d.ts +73 -0
  48. package/dist/core/events/memory-manager.d.ts.map +1 -0
  49. package/dist/core/events/memory-manager.js +1218 -0
  50. package/dist/core/events/memory-manager.js.map +1 -0
  51. package/dist/core/events/mention-logic.d.ts +39 -0
  52. package/dist/core/events/mention-logic.d.ts.map +1 -0
  53. package/dist/core/events/mention-logic.js +163 -0
  54. package/dist/core/events/mention-logic.js.map +1 -0
  55. package/dist/core/events/orchestrator.d.ts +69 -0
  56. package/dist/core/events/orchestrator.d.ts.map +1 -0
  57. package/dist/core/events/orchestrator.js +883 -0
  58. package/dist/core/events/orchestrator.js.map +1 -0
  59. package/dist/core/events/persistence.d.ts +41 -0
  60. package/dist/core/events/persistence.d.ts.map +1 -0
  61. package/dist/core/events/persistence.js +296 -0
  62. package/dist/core/events/persistence.js.map +1 -0
  63. package/dist/core/events/publishers.d.ts +81 -0
  64. package/dist/core/events/publishers.d.ts.map +1 -0
  65. package/dist/core/events/publishers.js +272 -0
  66. package/dist/core/events/publishers.js.map +1 -0
  67. package/dist/core/events/subscribers.d.ts +45 -0
  68. package/dist/core/events/subscribers.d.ts.map +1 -0
  69. package/dist/core/events/subscribers.js +288 -0
  70. package/dist/core/events/subscribers.js.map +1 -0
  71. package/dist/core/events/tool-bridge-logging.d.ts +28 -0
  72. package/dist/core/events/tool-bridge-logging.d.ts.map +1 -0
  73. package/dist/core/events/tool-bridge-logging.js +94 -0
  74. package/dist/core/events/tool-bridge-logging.js.map +1 -0
  75. package/dist/core/events-metadata.d.ts +72 -0
  76. package/dist/core/events-metadata.d.ts.map +1 -0
  77. package/dist/core/events-metadata.js +167 -0
  78. package/dist/core/events-metadata.js.map +1 -0
  79. package/dist/core/events.d.ts +186 -0
  80. package/dist/core/events.d.ts.map +1 -0
  81. package/dist/core/events.js +1248 -0
  82. package/dist/core/events.js.map +1 -0
  83. package/dist/core/export.d.ts +106 -0
  84. package/dist/core/export.d.ts.map +1 -0
  85. package/dist/core/export.js +705 -0
  86. package/dist/core/export.js.map +1 -0
  87. package/dist/core/file-tools.d.ts +114 -0
  88. package/dist/core/file-tools.d.ts.map +1 -0
  89. package/dist/core/file-tools.js +370 -0
  90. package/dist/core/file-tools.js.map +1 -0
  91. package/dist/core/google-direct.d.ts +58 -0
  92. package/dist/core/google-direct.d.ts.map +1 -0
  93. package/dist/core/google-direct.js +298 -0
  94. package/dist/core/google-direct.js.map +1 -0
  95. package/dist/core/hitl.d.ts +54 -0
  96. package/dist/core/hitl.d.ts.map +1 -0
  97. package/dist/core/hitl.js +153 -0
  98. package/dist/core/hitl.js.map +1 -0
  99. package/dist/core/index.d.ts +59 -0
  100. package/dist/core/index.d.ts.map +1 -0
  101. package/dist/core/index.js +70 -0
  102. package/dist/core/index.js.map +1 -0
  103. package/dist/core/llm-config.d.ts +128 -0
  104. package/dist/core/llm-config.d.ts.map +1 -0
  105. package/dist/core/llm-config.js +164 -0
  106. package/dist/core/llm-config.js.map +1 -0
  107. package/dist/core/llm-manager.d.ts +163 -0
  108. package/dist/core/llm-manager.d.ts.map +1 -0
  109. package/dist/core/llm-manager.js +669 -0
  110. package/dist/core/llm-manager.js.map +1 -0
  111. package/dist/core/load-skill-tool.d.ts +55 -0
  112. package/dist/core/load-skill-tool.d.ts.map +1 -0
  113. package/dist/core/load-skill-tool.js +468 -0
  114. package/dist/core/load-skill-tool.js.map +1 -0
  115. package/dist/core/logger.d.ts +88 -0
  116. package/dist/core/logger.d.ts.map +1 -0
  117. package/dist/core/logger.js +358 -0
  118. package/dist/core/logger.js.map +1 -0
  119. package/dist/core/managers.d.ts +131 -0
  120. package/dist/core/managers.d.ts.map +1 -0
  121. package/dist/core/managers.js +1223 -0
  122. package/dist/core/managers.js.map +1 -0
  123. package/dist/core/mcp-server-registry.d.ts +304 -0
  124. package/dist/core/mcp-server-registry.d.ts.map +1 -0
  125. package/dist/core/mcp-server-registry.js +1769 -0
  126. package/dist/core/mcp-server-registry.js.map +1 -0
  127. package/dist/core/mcp-tools.d.ts +56 -0
  128. package/dist/core/mcp-tools.d.ts.map +1 -0
  129. package/dist/core/mcp-tools.js +186 -0
  130. package/dist/core/mcp-tools.js.map +1 -0
  131. package/dist/core/message-prep.d.ts +81 -0
  132. package/dist/core/message-prep.d.ts.map +1 -0
  133. package/dist/core/message-prep.js +223 -0
  134. package/dist/core/message-prep.js.map +1 -0
  135. package/dist/core/message-processing-control.d.ts +54 -0
  136. package/dist/core/message-processing-control.d.ts.map +1 -0
  137. package/dist/core/message-processing-control.js +139 -0
  138. package/dist/core/message-processing-control.js.map +1 -0
  139. package/dist/core/openai-direct.d.ts +80 -0
  140. package/dist/core/openai-direct.d.ts.map +1 -0
  141. package/dist/core/openai-direct.js +374 -0
  142. package/dist/core/openai-direct.js.map +1 -0
  143. package/dist/core/shell-cmd-tool.d.ts +235 -0
  144. package/dist/core/shell-cmd-tool.d.ts.map +1 -0
  145. package/dist/core/shell-cmd-tool.js +1157 -0
  146. package/dist/core/shell-cmd-tool.js.map +1 -0
  147. package/dist/core/shell-process-registry.d.ts +88 -0
  148. package/dist/core/shell-process-registry.d.ts.map +1 -0
  149. package/dist/core/shell-process-registry.js +309 -0
  150. package/dist/core/shell-process-registry.js.map +1 -0
  151. package/dist/core/skill-registry.d.ts +75 -0
  152. package/dist/core/skill-registry.d.ts.map +1 -0
  153. package/dist/core/skill-registry.js +369 -0
  154. package/dist/core/skill-registry.js.map +1 -0
  155. package/dist/core/skill-script-runner.d.ts +89 -0
  156. package/dist/core/skill-script-runner.d.ts.map +1 -0
  157. package/dist/core/skill-script-runner.js +274 -0
  158. package/dist/core/skill-script-runner.js.map +1 -0
  159. package/dist/core/skill-selector.d.ts +65 -0
  160. package/dist/core/skill-selector.d.ts.map +1 -0
  161. package/dist/core/skill-selector.js +190 -0
  162. package/dist/core/skill-selector.js.map +1 -0
  163. package/dist/core/skill-settings.d.ts +20 -0
  164. package/dist/core/skill-settings.d.ts.map +1 -0
  165. package/dist/core/skill-settings.js +40 -0
  166. package/dist/core/skill-settings.js.map +1 -0
  167. package/dist/core/storage/agent-storage.d.ts +134 -0
  168. package/dist/core/storage/agent-storage.d.ts.map +1 -0
  169. package/dist/core/storage/agent-storage.js +498 -0
  170. package/dist/core/storage/agent-storage.js.map +1 -0
  171. package/dist/core/storage/eventStorage/fileEventStorage.d.ts +100 -0
  172. package/dist/core/storage/eventStorage/fileEventStorage.d.ts.map +1 -0
  173. package/dist/core/storage/eventStorage/fileEventStorage.js +494 -0
  174. package/dist/core/storage/eventStorage/fileEventStorage.js.map +1 -0
  175. package/dist/core/storage/eventStorage/index.d.ts +31 -0
  176. package/dist/core/storage/eventStorage/index.d.ts.map +1 -0
  177. package/dist/core/storage/eventStorage/index.js +31 -0
  178. package/dist/core/storage/eventStorage/index.js.map +1 -0
  179. package/dist/core/storage/eventStorage/memoryEventStorage.d.ts +87 -0
  180. package/dist/core/storage/eventStorage/memoryEventStorage.d.ts.map +1 -0
  181. package/dist/core/storage/eventStorage/memoryEventStorage.js +244 -0
  182. package/dist/core/storage/eventStorage/memoryEventStorage.js.map +1 -0
  183. package/dist/core/storage/eventStorage/sqliteEventStorage.d.ts +45 -0
  184. package/dist/core/storage/eventStorage/sqliteEventStorage.d.ts.map +1 -0
  185. package/dist/core/storage/eventStorage/sqliteEventStorage.js +301 -0
  186. package/dist/core/storage/eventStorage/sqliteEventStorage.js.map +1 -0
  187. package/dist/core/storage/eventStorage/types.d.ts +142 -0
  188. package/dist/core/storage/eventStorage/types.d.ts.map +1 -0
  189. package/dist/core/storage/eventStorage/types.js +43 -0
  190. package/dist/core/storage/eventStorage/types.js.map +1 -0
  191. package/dist/core/storage/eventStorage/validation.d.ts +30 -0
  192. package/dist/core/storage/eventStorage/validation.d.ts.map +1 -0
  193. package/dist/core/storage/eventStorage/validation.js +68 -0
  194. package/dist/core/storage/eventStorage/validation.js.map +1 -0
  195. package/dist/core/storage/legacy-migrations.d.ts +45 -0
  196. package/dist/core/storage/legacy-migrations.d.ts.map +1 -0
  197. package/dist/core/storage/legacy-migrations.js +295 -0
  198. package/dist/core/storage/legacy-migrations.js.map +1 -0
  199. package/dist/core/storage/memory-storage.d.ts +105 -0
  200. package/dist/core/storage/memory-storage.d.ts.map +1 -0
  201. package/dist/core/storage/memory-storage.js +415 -0
  202. package/dist/core/storage/memory-storage.js.map +1 -0
  203. package/dist/core/storage/migration-runner.d.ts +96 -0
  204. package/dist/core/storage/migration-runner.d.ts.map +1 -0
  205. package/dist/core/storage/migration-runner.js +306 -0
  206. package/dist/core/storage/migration-runner.js.map +1 -0
  207. package/dist/core/storage/queue-storage.d.ts +147 -0
  208. package/dist/core/storage/queue-storage.d.ts.map +1 -0
  209. package/dist/core/storage/queue-storage.js +290 -0
  210. package/dist/core/storage/queue-storage.js.map +1 -0
  211. package/dist/core/storage/skill-storage.d.ts +136 -0
  212. package/dist/core/storage/skill-storage.d.ts.map +1 -0
  213. package/dist/core/storage/skill-storage.js +474 -0
  214. package/dist/core/storage/skill-storage.js.map +1 -0
  215. package/dist/core/storage/sqlite-schema.d.ts +95 -0
  216. package/dist/core/storage/sqlite-schema.d.ts.map +1 -0
  217. package/dist/core/storage/sqlite-schema.js +156 -0
  218. package/dist/core/storage/sqlite-schema.js.map +1 -0
  219. package/dist/core/storage/sqlite-storage.d.ts +146 -0
  220. package/dist/core/storage/sqlite-storage.d.ts.map +1 -0
  221. package/dist/core/storage/sqlite-storage.js +709 -0
  222. package/dist/core/storage/sqlite-storage.js.map +1 -0
  223. package/dist/core/storage/storage-factory.d.ts +61 -0
  224. package/dist/core/storage/storage-factory.d.ts.map +1 -0
  225. package/dist/core/storage/storage-factory.js +794 -0
  226. package/dist/core/storage/storage-factory.js.map +1 -0
  227. package/dist/core/storage/validation.d.ts +36 -0
  228. package/dist/core/storage/validation.d.ts.map +1 -0
  229. package/dist/core/storage/validation.js +79 -0
  230. package/dist/core/storage/validation.js.map +1 -0
  231. package/dist/core/storage/world-storage.d.ts +114 -0
  232. package/dist/core/storage/world-storage.d.ts.map +1 -0
  233. package/dist/core/storage/world-storage.js +378 -0
  234. package/dist/core/storage/world-storage.js.map +1 -0
  235. package/dist/core/subscription.d.ts +43 -0
  236. package/dist/core/subscription.d.ts.map +1 -0
  237. package/dist/core/subscription.js +227 -0
  238. package/dist/core/subscription.js.map +1 -0
  239. package/dist/core/tool-utils.d.ts +80 -0
  240. package/dist/core/tool-utils.d.ts.map +1 -0
  241. package/dist/core/tool-utils.js +273 -0
  242. package/dist/core/tool-utils.js.map +1 -0
  243. package/dist/core/types.d.ts +595 -0
  244. package/dist/core/types.d.ts.map +1 -0
  245. package/dist/core/types.js +158 -0
  246. package/dist/core/types.js.map +1 -0
  247. package/dist/core/utils.d.ts +138 -0
  248. package/dist/core/utils.d.ts.map +1 -0
  249. package/dist/core/utils.js +478 -0
  250. package/dist/core/utils.js.map +1 -0
  251. package/dist/core/world-class.d.ts +43 -0
  252. package/dist/core/world-class.d.ts.map +1 -0
  253. package/dist/core/world-class.js +90 -0
  254. package/dist/core/world-class.js.map +1 -0
  255. package/dist/index.d.ts +18 -0
  256. package/dist/public/assets/agent-sprites-DJFgj-zP.png +0 -0
  257. package/dist/public/assets/border-KHK37r8y.svg +83 -0
  258. package/dist/public/assets/index-C9kPXL6G.css +1 -0
  259. package/dist/public/assets/index-DOQEHGWt.js +96 -0
  260. package/dist/public/index.html +21 -0
  261. package/dist/server/api.d.ts +2 -0
  262. package/dist/server/api.js +1124 -0
  263. package/dist/server/index.d.ts +29 -0
  264. package/dist/server/sse-handler.d.ts +62 -0
  265. package/dist/server/sse-handler.js +234 -0
  266. package/package.json +15 -3
  267. package/scripts/launch-electron.js +0 -58
@@ -0,0 +1,1157 @@
1
+ /**
2
+ * Shell Command Tool Module - Built-in LLM tool for executing shell commands
3
+ *
4
+ * Features:
5
+ * - Execute shell commands in child processes with parameter support
6
+ * - Capture stdout and stderr output
7
+ * - Persist command execution history (command, parameters, results, exceptions)
8
+ * - Return results to LLM for further processing
9
+ * - Error handling and exception tracking
10
+ * - Long-running command support with 10-minute default timeout
11
+ * - Trusted working-directory enforcement from world/tool context
12
+ * - Explicit rejection when LLM-supplied directory conflicts with trusted working directory
13
+ * - Graceful error handling for invalid tool calls
14
+ * - Universal parameter validation for consistent execution
15
+ * - Explicit execution safety configuration using structured metadata
16
+ *
17
+ * Implementation Details:
18
+ * - Uses Node.js child_process.spawn for command execution
19
+ * - Executes commands through shell for PATH resolution and shell features
20
+ * - Stores execution history in-memory (can be extended to persistent storage)
21
+ * - Provides MCP-compatible tool interface for LLM integration
22
+ * - Timeout support to prevent hanging processes (default: 10 minutes)
23
+ * - Resource cleanup on process completion
24
+ * - Resolves command cwd from trusted world/tool context, not LLM args
25
+ * - Rejects directory mismatch instead of silently overriding requested path
26
+ * - Returns error results instead of throwing to prevent agent crashes
27
+ * - Uses universal validation framework for consistent parameter checking
28
+ *
29
+ * Recent Changes:
30
+ * - 2026-02-15: Moved core cwd-boundary enforcement into `executeShellCommand` via optional `trustedWorkingDirectory` execution option.
31
+ * - 2026-02-15: Added optional `output_format=json` for machine-readable command results.
32
+ * - 2026-02-15: Added optional `artifact_paths` support with SHA-256 hashing and byte-size metadata for files within trusted scope.
33
+ * - 2026-02-14: Default trusted cwd now falls back to shared core default working directory (user home by default) instead of `./` when world variable is unset.
34
+ * - 2026-02-14: Added inline-script execution guard (e.g. `sh -c`, `node -e`) to prevent embedded path bypass outside trusted cwd.
35
+ * - 2026-02-14: Hardened cwd containment checks by canonicalizing absolute paths and validating additional path argument forms (`./`, `../`, and `--flag=/path`).
36
+ * - 2026-02-13: Updated directory-request validation to allow requested folders inside world working_directory and reject only outside paths.
37
+ * - 2026-02-13: Added command/parameter path scope validation so shell_cmd rejects path targets outside trusted world working_directory.
38
+ * - 2026-02-13: Added strict directory-mismatch guard for shell_cmd; mismatched LLM directory requests now fail with explicit error.
39
+ * - 2026-02-13: Fixed validation error result typing by including `executionId` when formatting failed shell_cmd calls.
40
+ * - 2026-02-13: Stopped trusting LLM-provided `directory`; shell commands now resolve working directory from trusted world/tool context only.
41
+ * - 2026-02-13: Added explicit command-cancellation detection and AbortError propagation in tool execution to prevent post-stop continuation.
42
+ * - 2026-02-13: Added chat-scoped shell process tracking and stop controls for Electron stop-message support.
43
+ * - 2026-02-08: Added streaming callback support for real-time output
44
+ * * Added onStdout and onStderr callbacks to executeShellCommand options
45
+ * * Callbacks invoked in real-time as data arrives from child process
46
+ * * Maintains backwards compatibility - callbacks are optional
47
+ * * Full output still accumulated and returned in CommandExecutionResult
48
+ * - 2026-02-06: Removed legacy manual tool-decision metadata
49
+ * - 2025-11-11: CRITICAL FIX - Quote parameters for shell execution
50
+ * * Parameters with spaces/tabs/newlines now properly quoted before spawn
51
+ * * Prevents shell from splitting multi-word parameters
52
+ * * Applied to both execution AND display formatting
53
+ * - 2025-11-10: Fixed shell execution - enabled shell: true to support PATH resolution and installed commands
54
+ * - Integrated universal parameter validation for consistent tool execution
55
+ * - Enhanced validation to check required parameters and auto-correct types
56
+ * - Replaced custom validation with standardized validation framework
57
+ * - Added graceful error handling for empty commands to prevent agent crashes
58
+ * - Changed validation to return error results instead of throwing exceptions
59
+ * - Updated tests to expect error results rather than thrown errors
60
+ * - Added LLM guidance to ask user for directory when not provided
61
+ * - Made directory parameter required for shell command execution
62
+ * - Increased default timeout from 30s to 10 minutes (600000ms) for long-running commands
63
+ * - Initial implementation for shell_cmd LLM tool
64
+ */
65
+ import { spawn } from 'child_process';
66
+ import { resolve, join, relative } from 'path';
67
+ import { createHash } from 'crypto';
68
+ import { homedir } from 'os';
69
+ import { realpathSync, promises as fsPromises } from 'fs';
70
+ import { createCategoryLogger } from './logger.js';
71
+ import { validateToolParameters } from './tool-utils.js';
72
+ import { publishSSE } from './events/index.js';
73
+ import { getDefaultWorkingDirectory, getEnvValueFromText } from './utils.js';
74
+ import { createShellProcessExecution, transitionShellProcessExecution, attachShellProcessHandle, markShellProcessCancelRequested, listShellProcessExecutions, getShellProcessExecution, cancelShellProcessExecution, deleteShellProcessExecution, stopShellProcessesForChatScope, subscribeShellProcessStatus, clearShellProcessRegistryForTests } from './shell-process-registry.js';
75
+ const logger = createCategoryLogger('shell-cmd');
76
+ /**
77
+ * Resolve directory path, handling tilde expansion and relative paths
78
+ */
79
+ function resolveDirectory(directory) {
80
+ if (directory.startsWith('~/')) {
81
+ return join(homedir(), directory.slice(2));
82
+ }
83
+ if (directory === '~') {
84
+ return homedir();
85
+ }
86
+ return resolve(directory);
87
+ }
88
+ const DEFAULT_MIN_OUTPUT_CHARS = 400;
89
+ function buildOutputSnippet(content, maxOutputChars) {
90
+ if (!content) {
91
+ return { text: '', truncated: false };
92
+ }
93
+ if (maxOutputChars <= 0 || content.length <= maxOutputChars) {
94
+ return { text: content, truncated: false };
95
+ }
96
+ return {
97
+ text: content.slice(0, maxOutputChars),
98
+ truncated: true
99
+ };
100
+ }
101
+ /**
102
+ * In-memory storage for command execution history
103
+ * Can be extended to use persistent storage in the future
104
+ */
105
+ const executionHistory = [];
106
+ const MAX_HISTORY_SIZE = 1000; // Limit history size to prevent memory issues
107
+ export function resolveTrustedShellWorkingDirectory(context) {
108
+ const contextDirectory = typeof context?.workingDirectory === 'string'
109
+ ? context.workingDirectory.trim()
110
+ : '';
111
+ if (contextDirectory) {
112
+ return contextDirectory;
113
+ }
114
+ const worldDirectory = getEnvValueFromText(context?.world?.variables, 'working_directory');
115
+ const trimmedWorldDirectory = typeof worldDirectory === 'string' ? worldDirectory.trim() : '';
116
+ return trimmedWorldDirectory || getDefaultWorkingDirectory();
117
+ }
118
+ export function validateShellDirectoryRequest(requestedDirectory, trustedWorkingDirectory) {
119
+ if (typeof requestedDirectory !== 'string') {
120
+ return { valid: true };
121
+ }
122
+ const requested = requestedDirectory.trim();
123
+ if (!requested) {
124
+ return { valid: true };
125
+ }
126
+ const trusted = String(trustedWorkingDirectory || '').trim() || getDefaultWorkingDirectory();
127
+ if (isPathWithinTrustedDirectory(requested, trusted)) {
128
+ return { valid: true };
129
+ }
130
+ return {
131
+ valid: false,
132
+ error: `Working directory mismatch: requested directory "${requested}" is outside world working directory "${trusted}". Update world working_directory first.`
133
+ };
134
+ }
135
+ function stripWrappingQuotes(value) {
136
+ const trimmed = value.trim();
137
+ if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
138
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
139
+ return trimmed.slice(1, -1).trim();
140
+ }
141
+ return trimmed;
142
+ }
143
+ function trimTrailingSeparators(pathValue) {
144
+ const root = getPathRoot(pathValue);
145
+ if (!root || pathValue === root) {
146
+ return pathValue;
147
+ }
148
+ let trimmed = pathValue;
149
+ while (trimmed.endsWith('/') || trimmed.endsWith('\\')) {
150
+ trimmed = trimmed.slice(0, -1);
151
+ if (trimmed === root) {
152
+ return trimmed;
153
+ }
154
+ }
155
+ return trimmed;
156
+ }
157
+ function getPathRoot(pathValue) {
158
+ const normalized = pathValue.replace(/\\/g, '/');
159
+ const driveMatch = normalized.match(/^[A-Za-z]:\//);
160
+ if (driveMatch) {
161
+ return driveMatch[0];
162
+ }
163
+ if (normalized.startsWith('/')) {
164
+ return '/';
165
+ }
166
+ return '';
167
+ }
168
+ function collapseDotSegments(pathValue) {
169
+ const normalized = pathValue.replace(/\\/g, '/');
170
+ const root = getPathRoot(normalized);
171
+ const segments = normalized.slice(root.length).split('/');
172
+ const collapsed = [];
173
+ for (const segment of segments) {
174
+ if (!segment || segment === '.') {
175
+ continue;
176
+ }
177
+ if (segment === '..') {
178
+ if (collapsed.length > 0 && collapsed[collapsed.length - 1] !== '..') {
179
+ collapsed.pop();
180
+ }
181
+ else if (!root) {
182
+ collapsed.push('..');
183
+ }
184
+ continue;
185
+ }
186
+ collapsed.push(segment);
187
+ }
188
+ const joined = collapsed.join('/');
189
+ if (root) {
190
+ return `${root}${joined}` || root;
191
+ }
192
+ return joined || '.';
193
+ }
194
+ function canonicalizePath(pathValue) {
195
+ const absolute = resolveDirectory(pathValue);
196
+ try {
197
+ const canonical = realpathSync.native ? realpathSync.native(absolute) : realpathSync(absolute);
198
+ return trimTrailingSeparators(collapseDotSegments(canonical));
199
+ }
200
+ catch {
201
+ return trimTrailingSeparators(collapseDotSegments(absolute));
202
+ }
203
+ }
204
+ function normalizeForPlatformComparison(pathValue) {
205
+ return process.platform === 'win32' ? pathValue.toLowerCase() : pathValue;
206
+ }
207
+ function isPathWithinTrustedDirectory(candidatePath, trustedWorkingDirectory) {
208
+ const normalizedCandidate = normalizeForPlatformComparison(canonicalizePath(candidatePath));
209
+ const normalizedTrusted = normalizeForPlatformComparison(canonicalizePath(trustedWorkingDirectory));
210
+ const trustedRoot = normalizeForPlatformComparison(getPathRoot(normalizedTrusted));
211
+ if (normalizedTrusted === trustedRoot) {
212
+ return normalizedCandidate.startsWith(normalizedTrusted);
213
+ }
214
+ return normalizedCandidate === normalizedTrusted ||
215
+ normalizedCandidate.startsWith(`${normalizedTrusted}/`);
216
+ }
217
+ function looksLikePathToken(token) {
218
+ if (!token)
219
+ return false;
220
+ return token === '~' ||
221
+ token === '.' ||
222
+ token.startsWith('~/') ||
223
+ token.startsWith('~\\') ||
224
+ token.startsWith('/') ||
225
+ token.startsWith('\\') ||
226
+ token.startsWith('./') ||
227
+ token.startsWith('.\\') ||
228
+ token === '..' ||
229
+ token.startsWith('../') ||
230
+ token.startsWith('..\\') ||
231
+ token.includes('/') ||
232
+ token.includes('\\');
233
+ }
234
+ function resolveTokenPath(token, trustedWorkingDirectory) {
235
+ if (token.startsWith('~')) {
236
+ return resolveDirectory(token);
237
+ }
238
+ if (token.startsWith('/')) {
239
+ return resolveDirectory(token);
240
+ }
241
+ return resolveDirectory(resolve(trustedWorkingDirectory, token));
242
+ }
243
+ function extractPathTokenFromOptionPrefix(token) {
244
+ if (!token.startsWith('-') || token.includes('=')) {
245
+ return null;
246
+ }
247
+ const pathStart = token.search(/(~|\/|\\|\.|[A-Za-z]:[\\/])/);
248
+ if (pathStart <= 1) {
249
+ return null;
250
+ }
251
+ const optionPart = token.slice(0, pathStart);
252
+ const candidate = token.slice(pathStart);
253
+ if (!/^-{1,2}[A-Za-z][A-Za-z0-9_-]*$/.test(optionPart)) {
254
+ return null;
255
+ }
256
+ if (!looksLikePathToken(candidate)) {
257
+ return null;
258
+ }
259
+ return candidate;
260
+ }
261
+ function extractPathTokenFromOptionAssignment(token) {
262
+ if (!token.startsWith('-')) {
263
+ return null;
264
+ }
265
+ const equalsIndex = token.indexOf('=');
266
+ if (equalsIndex <= 0) {
267
+ return null;
268
+ }
269
+ const assignedValue = stripWrappingQuotes(token.slice(equalsIndex + 1));
270
+ if (!assignedValue || !looksLikePathToken(assignedValue)) {
271
+ return null;
272
+ }
273
+ return assignedValue;
274
+ }
275
+ function extractPathToken(rawToken) {
276
+ const token = stripWrappingQuotes(rawToken);
277
+ if (!token) {
278
+ return null;
279
+ }
280
+ const fromAssignment = extractPathTokenFromOptionAssignment(token);
281
+ if (fromAssignment) {
282
+ return fromAssignment;
283
+ }
284
+ const fromOptionPrefix = extractPathTokenFromOptionPrefix(token);
285
+ if (fromOptionPrefix) {
286
+ return fromOptionPrefix;
287
+ }
288
+ if (token.startsWith('-')) {
289
+ return null;
290
+ }
291
+ return looksLikePathToken(token) ? token : null;
292
+ }
293
+ function tokenizeInlineCommandArgs(command) {
294
+ const tokens = command.match(/"([^"\\]|\\.)*"|'([^'\\]|\\.)*'|[^\s]+/g) ?? [];
295
+ if (tokens.length <= 1)
296
+ return [];
297
+ return tokens.slice(1);
298
+ }
299
+ function tokenizeCommand(command) {
300
+ return command.match(/"([^"\\]|\\.)*"|'([^'\\]|\\.)*'|[^\s]+/g) ?? [];
301
+ }
302
+ function hasDisallowedShellSyntax(value) {
303
+ if (!value)
304
+ return false;
305
+ return value.includes('&&') ||
306
+ value.includes('||') ||
307
+ value.includes('|') ||
308
+ value.includes(';') ||
309
+ value.includes('>') ||
310
+ value.includes('<') ||
311
+ value.includes('$(') ||
312
+ value.includes('`') ||
313
+ value.includes('&') ||
314
+ value.includes('\n') ||
315
+ value.includes('\r');
316
+ }
317
+ function validateSingleCommandContract(command) {
318
+ if (typeof command !== 'string' || !command.trim()) {
319
+ return {
320
+ valid: false,
321
+ error: 'Invalid command: command must be a non-empty string.'
322
+ };
323
+ }
324
+ if (hasDisallowedShellSyntax(command)) {
325
+ return {
326
+ valid: false,
327
+ error: 'Invalid command: shell control syntax is not allowed (`&&`, `||`, `|`, `;`, redirects, command substitution, backgrounding). Provide a single executable in `command` and pass arguments via `parameters`.'
328
+ };
329
+ }
330
+ const commandTokens = tokenizeCommand(command).map(stripWrappingQuotes).filter(Boolean);
331
+ if (commandTokens.length !== 1) {
332
+ return {
333
+ valid: false,
334
+ error: 'Invalid command format: provide a single executable in `command` and pass all arguments as separate `parameters` tokens.'
335
+ };
336
+ }
337
+ const executable = commandTokens[0];
338
+ if (!executable || /\s/.test(executable)) {
339
+ return {
340
+ valid: false,
341
+ error: 'Invalid command executable: `command` must be a single token without whitespace.'
342
+ };
343
+ }
344
+ return { valid: true, executable };
345
+ }
346
+ function getExecutableName(command) {
347
+ const tokens = tokenizeCommand(command);
348
+ if (tokens.length === 0)
349
+ return '';
350
+ const executable = stripWrappingQuotes(tokens[0]).replace(/\\/g, '/');
351
+ const parts = executable.split('/').filter(Boolean);
352
+ return String(parts[parts.length - 1] || executable).toLowerCase();
353
+ }
354
+ function getInterpreterInlineScriptFlags(executable) {
355
+ if (['sh', 'bash', 'zsh', 'dash', 'ksh', 'fish', 'cmd', 'cmd.exe'].includes(executable)) {
356
+ return new Set(['-c', '/c', '/k']);
357
+ }
358
+ if (['powershell', 'powershell.exe', 'pwsh', 'pwsh.exe'].includes(executable)) {
359
+ return new Set(['-c', '-command']);
360
+ }
361
+ if (['node', 'node.exe', 'deno', 'python', 'python3', 'python.exe', 'python3.exe'].includes(executable)) {
362
+ return new Set(['-c', '-e', '--eval']);
363
+ }
364
+ if (['perl', 'ruby', 'php'].includes(executable)) {
365
+ return new Set(['-e', '-r']);
366
+ }
367
+ return new Set();
368
+ }
369
+ function findInlineScriptExecutionFlag(command, parameters) {
370
+ if (typeof command !== 'string' || !command.trim()) {
371
+ return null;
372
+ }
373
+ const commandTokens = tokenizeCommand(command).map(stripWrappingQuotes).filter(Boolean);
374
+ const commandArgs = commandTokens.slice(1);
375
+ const parameterArgs = Array.isArray(parameters)
376
+ ? parameters.filter((p) => typeof p === 'string').map(stripWrappingQuotes).filter(Boolean)
377
+ : [];
378
+ const directExecutable = getExecutableName(command);
379
+ let executable = directExecutable;
380
+ let args = [...commandArgs, ...parameterArgs];
381
+ // Handle wrappers like `env bash -c ...`
382
+ if (directExecutable === 'env') {
383
+ const envArgs = [...args];
384
+ while (envArgs.length > 0 && /^[A-Za-z_][A-Za-z0-9_]*=/.test(envArgs[0])) {
385
+ envArgs.shift();
386
+ }
387
+ if (envArgs.length > 0) {
388
+ executable = getExecutableName(envArgs[0]);
389
+ args = envArgs.slice(1);
390
+ }
391
+ }
392
+ const scriptFlags = getInterpreterInlineScriptFlags(executable);
393
+ if (scriptFlags.size === 0) {
394
+ return null;
395
+ }
396
+ for (const rawArg of args) {
397
+ const arg = rawArg.toLowerCase();
398
+ if (scriptFlags.has(arg)) {
399
+ return { executable, flag: rawArg };
400
+ }
401
+ for (const scriptFlag of scriptFlags) {
402
+ if ((scriptFlag.startsWith('-') || scriptFlag.startsWith('/')) &&
403
+ arg.startsWith(scriptFlag) &&
404
+ arg.length > scriptFlag.length) {
405
+ return { executable, flag: rawArg };
406
+ }
407
+ }
408
+ }
409
+ return null;
410
+ }
411
+ export function validateShellCommandScope(command, parameters, trustedWorkingDirectory) {
412
+ const singleCommandValidation = validateSingleCommandContract(command);
413
+ if (!singleCommandValidation.valid) {
414
+ return singleCommandValidation;
415
+ }
416
+ if (Array.isArray(parameters)) {
417
+ for (const parameter of parameters) {
418
+ if (typeof parameter !== 'string') {
419
+ continue;
420
+ }
421
+ if (hasDisallowedShellSyntax(parameter)) {
422
+ return {
423
+ valid: false,
424
+ error: `Invalid parameter: shell control syntax is not allowed in parameters (received "${parameter}").`
425
+ };
426
+ }
427
+ }
428
+ }
429
+ const inlineScriptUsage = findInlineScriptExecutionFlag(command, parameters);
430
+ if (inlineScriptUsage) {
431
+ return {
432
+ valid: false,
433
+ error: `Working directory mismatch: inline script execution "${inlineScriptUsage.executable} ${inlineScriptUsage.flag}" is not allowed. Use direct command + parameters inside world working directory "${trustedWorkingDirectory}".`
434
+ };
435
+ }
436
+ const tokens = [];
437
+ if (typeof command === 'string' && command.trim()) {
438
+ tokens.push(...tokenizeInlineCommandArgs(command));
439
+ }
440
+ if (Array.isArray(parameters)) {
441
+ for (const parameter of parameters) {
442
+ if (typeof parameter === 'string') {
443
+ tokens.push(parameter);
444
+ }
445
+ }
446
+ }
447
+ for (const rawToken of tokens) {
448
+ const token = extractPathToken(rawToken);
449
+ if (!token)
450
+ continue;
451
+ const resolvedPath = resolveTokenPath(token, trustedWorkingDirectory);
452
+ if (!isPathWithinTrustedDirectory(resolvedPath, trustedWorkingDirectory)) {
453
+ return {
454
+ valid: false,
455
+ error: `Working directory mismatch: path "${token}" is outside world working directory "${trustedWorkingDirectory}".`
456
+ };
457
+ }
458
+ }
459
+ return { valid: true };
460
+ }
461
+ export function stopShellCommandsForChat(worldId, chatId) {
462
+ return stopShellProcessesForChatScope(worldId, chatId);
463
+ }
464
+ export function isCommandExecutionCanceled(result) {
465
+ if (result.canceled)
466
+ return true;
467
+ return result.error === 'Command execution canceled by user';
468
+ }
469
+ /**
470
+ * Execute a shell command with parameters and capture output
471
+ *
472
+ * @param command - The shell command to execute (e.g., 'ls', 'echo', 'cat')
473
+ * @param parameters - Array of parameters for the command (e.g., ['-la', '/tmp'])
474
+ * @param directory - Working directory for command execution (required)
475
+ * @param options - Execution options
476
+ * @returns Promise<CommandExecutionResult> - Execution result with output and metadata
477
+ */
478
+ export async function executeShellCommand(command, parameters = [], directory, options = {}) {
479
+ const startTime = Date.now();
480
+ const timeout = options.timeout || 600000; // Default 10 minute timeout for long-running commands
481
+ const resolvedDirectory = resolveDirectory(directory);
482
+ const executionRecord = createShellProcessExecution({
483
+ command,
484
+ parameters,
485
+ directory: resolvedDirectory,
486
+ worldId: options.worldId,
487
+ chatId: options.chatId
488
+ });
489
+ const executionId = executionRecord.executionId;
490
+ options.onStatusChange?.({
491
+ executionId,
492
+ status: executionRecord.status,
493
+ record: executionRecord
494
+ });
495
+ logger.debug('Executing shell command', {
496
+ command,
497
+ parameters,
498
+ timeout,
499
+ directory,
500
+ resolvedDirectory,
501
+ trustedWorkingDirectory: options.trustedWorkingDirectory || null
502
+ });
503
+ return new Promise((resolve) => {
504
+ let stdout = '';
505
+ let stderr = '';
506
+ let timedOut = false;
507
+ let aborted = false;
508
+ let processExited = false;
509
+ let unsubscribeStatusListener = null;
510
+ const result = {
511
+ executionId,
512
+ command,
513
+ parameters,
514
+ stdout: '',
515
+ stderr: '',
516
+ exitCode: null,
517
+ signal: null,
518
+ executedAt: new Date(),
519
+ duration: 0
520
+ };
521
+ if (options.onStatusChange) {
522
+ unsubscribeStatusListener = subscribeShellProcessStatus((event) => {
523
+ if (event.executionId !== executionId)
524
+ return;
525
+ options.onStatusChange?.(event);
526
+ });
527
+ }
528
+ try {
529
+ const trustedWorkingDirectory = String(options.trustedWorkingDirectory || '').trim();
530
+ if (trustedWorkingDirectory && !isPathWithinTrustedDirectory(resolvedDirectory, trustedWorkingDirectory)) {
531
+ throw new Error(`Working directory mismatch: execution directory "${resolvedDirectory}" is outside trusted working directory "${trustedWorkingDirectory}".`);
532
+ }
533
+ // Quote parameters that contain spaces, tabs, or newlines for shell execution
534
+ const quotedParams = parameters.map(param => {
535
+ if (param.includes(' ') || param.includes('\t') || param.includes('\n') || param.includes('"')) {
536
+ // Escape existing quotes and wrap in quotes
537
+ return `"${param.replace(/"/g, '\\"')}"`;
538
+ }
539
+ return param;
540
+ });
541
+ transitionShellProcessExecution(executionId, 'starting', {
542
+ startedAt: new Date().toISOString()
543
+ });
544
+ // Spawn the child process
545
+ const childProcess = spawn(command, quotedParams, {
546
+ cwd: resolvedDirectory,
547
+ shell: true, // Use shell to enable PATH resolution and shell features
548
+ timeout: timeout
549
+ });
550
+ attachShellProcessHandle(executionId, childProcess);
551
+ transitionShellProcessExecution(executionId, 'running', {
552
+ startedAt: new Date().toISOString()
553
+ });
554
+ // Set up timeout handler
555
+ const timeoutHandle = setTimeout(() => {
556
+ if (!processExited) {
557
+ timedOut = true;
558
+ childProcess.kill('SIGTERM');
559
+ logger.warn('Command execution timeout', { command, parameters, timeout, directory });
560
+ }
561
+ }, timeout);
562
+ const abortHandler = () => {
563
+ if (processExited)
564
+ return;
565
+ aborted = true;
566
+ markShellProcessCancelRequested(executionId);
567
+ childProcess.kill('SIGTERM');
568
+ logger.info('Shell command aborted by request', {
569
+ executionId,
570
+ command,
571
+ parameters,
572
+ directory,
573
+ worldId: options.worldId || null,
574
+ chatId: options.chatId || null
575
+ });
576
+ };
577
+ options.abortSignal?.addEventListener('abort', abortHandler, { once: true });
578
+ // Capture stdout with optional streaming
579
+ childProcess.stdout?.on('data', (data) => {
580
+ const chunk = data.toString();
581
+ stdout += chunk;
582
+ // Call streaming callback if provided (with error handling)
583
+ if (options.onStdout) {
584
+ try {
585
+ options.onStdout(chunk);
586
+ }
587
+ catch (error) {
588
+ logger.warn('Error in stdout streaming callback', {
589
+ error: error instanceof Error ? error.message : error
590
+ });
591
+ }
592
+ }
593
+ });
594
+ // Capture stderr with optional streaming
595
+ childProcess.stderr?.on('data', (data) => {
596
+ const chunk = data.toString();
597
+ stderr += chunk;
598
+ // Call streaming callback if provided (with error handling)
599
+ if (options.onStderr) {
600
+ try {
601
+ options.onStderr(chunk);
602
+ }
603
+ catch (error) {
604
+ logger.warn('Error in stderr streaming callback', {
605
+ error: error instanceof Error ? error.message : error
606
+ });
607
+ }
608
+ }
609
+ });
610
+ // Handle process exit
611
+ childProcess.on('close', (code, signal) => {
612
+ processExited = true;
613
+ clearTimeout(timeoutHandle);
614
+ options.abortSignal?.removeEventListener('abort', abortHandler);
615
+ unsubscribeStatusListener?.();
616
+ unsubscribeStatusListener = null;
617
+ const duration = Date.now() - startTime;
618
+ result.stdout = stdout;
619
+ result.stderr = stderr;
620
+ result.exitCode = code;
621
+ result.signal = signal;
622
+ result.duration = duration;
623
+ const latestRecord = getShellProcessExecution(executionId);
624
+ const canceledByControlRequest = Boolean(latestRecord?.cancelRequested);
625
+ if (timedOut) {
626
+ result.error = `Command execution timed out after ${timeout}ms`;
627
+ result.timedOut = true;
628
+ transitionShellProcessExecution(executionId, 'timed_out', {
629
+ finishedAt: new Date().toISOString(),
630
+ exitCode: code,
631
+ signal,
632
+ stdoutLength: stdout.length,
633
+ stderrLength: stderr.length,
634
+ error: result.error,
635
+ durationMs: duration
636
+ });
637
+ }
638
+ else if (aborted || canceledByControlRequest) {
639
+ result.error = 'Command execution canceled by user';
640
+ result.canceled = true;
641
+ transitionShellProcessExecution(executionId, 'canceled', {
642
+ finishedAt: new Date().toISOString(),
643
+ exitCode: code,
644
+ signal,
645
+ stdoutLength: stdout.length,
646
+ stderrLength: stderr.length,
647
+ error: result.error,
648
+ durationMs: duration
649
+ });
650
+ }
651
+ else if (code !== 0) {
652
+ result.error = `Command exited with code ${code}`;
653
+ transitionShellProcessExecution(executionId, 'failed', {
654
+ finishedAt: new Date().toISOString(),
655
+ exitCode: code,
656
+ signal,
657
+ stdoutLength: stdout.length,
658
+ stderrLength: stderr.length,
659
+ error: result.error,
660
+ durationMs: duration
661
+ });
662
+ }
663
+ else {
664
+ transitionShellProcessExecution(executionId, 'completed', {
665
+ finishedAt: new Date().toISOString(),
666
+ exitCode: code,
667
+ signal,
668
+ stdoutLength: stdout.length,
669
+ stderrLength: stderr.length,
670
+ durationMs: duration,
671
+ error: null
672
+ });
673
+ }
674
+ // Persist to history
675
+ persistExecutionResult(result);
676
+ logger.debug('Command execution completed', {
677
+ command,
678
+ executionId,
679
+ parameters,
680
+ directory,
681
+ exitCode: code,
682
+ signal,
683
+ duration,
684
+ stdoutLength: stdout.length,
685
+ stderrLength: stderr.length,
686
+ error: result.error
687
+ });
688
+ resolve(result);
689
+ });
690
+ // Handle process errors
691
+ childProcess.on('error', (error) => {
692
+ processExited = true;
693
+ clearTimeout(timeoutHandle);
694
+ options.abortSignal?.removeEventListener('abort', abortHandler);
695
+ unsubscribeStatusListener?.();
696
+ unsubscribeStatusListener = null;
697
+ const duration = Date.now() - startTime;
698
+ result.stdout = stdout;
699
+ result.stderr = stderr;
700
+ result.duration = duration;
701
+ result.error = error.message;
702
+ transitionShellProcessExecution(executionId, 'failed', {
703
+ finishedAt: new Date().toISOString(),
704
+ exitCode: null,
705
+ signal: null,
706
+ stdoutLength: stdout.length,
707
+ stderrLength: stderr.length,
708
+ error: result.error,
709
+ durationMs: duration
710
+ });
711
+ // Persist to history
712
+ persistExecutionResult(result);
713
+ logger.warn('Command execution error', {
714
+ command,
715
+ parameters,
716
+ directory,
717
+ error: error.message,
718
+ duration: Date.now() - startTime
719
+ });
720
+ resolve(result);
721
+ });
722
+ }
723
+ catch (error) {
724
+ const duration = Date.now() - startTime;
725
+ unsubscribeStatusListener?.();
726
+ unsubscribeStatusListener = null;
727
+ result.duration = duration;
728
+ result.error = error instanceof Error ? error.message : String(error);
729
+ transitionShellProcessExecution(executionId, 'failed', {
730
+ finishedAt: new Date().toISOString(),
731
+ exitCode: null,
732
+ signal: null,
733
+ stdoutLength: stdout.length,
734
+ stderrLength: stderr.length,
735
+ error: result.error,
736
+ durationMs: duration
737
+ });
738
+ // Persist to history
739
+ persistExecutionResult(result);
740
+ logger.warn('Failed to spawn command', {
741
+ command,
742
+ parameters,
743
+ directory,
744
+ error: result.error,
745
+ duration
746
+ });
747
+ resolve(result);
748
+ }
749
+ });
750
+ }
751
+ export function getProcessExecution(executionId) {
752
+ return getShellProcessExecution(executionId);
753
+ }
754
+ export function listProcessExecutions(options = {}) {
755
+ return listShellProcessExecutions(options);
756
+ }
757
+ export function cancelProcessExecution(executionId) {
758
+ return cancelShellProcessExecution(executionId);
759
+ }
760
+ export function deleteProcessExecution(executionId) {
761
+ const deleteResult = deleteShellProcessExecution(executionId);
762
+ if (deleteResult.outcome !== 'deleted') {
763
+ return {
764
+ executionId,
765
+ outcome: deleteResult.outcome,
766
+ removedHistoryEntries: 0
767
+ };
768
+ }
769
+ let removedHistoryEntries = 0;
770
+ for (let index = executionHistory.length - 1; index >= 0; index -= 1) {
771
+ if (executionHistory[index]?.executionId !== executionId)
772
+ continue;
773
+ executionHistory.splice(index, 1);
774
+ removedHistoryEntries += 1;
775
+ }
776
+ return {
777
+ executionId,
778
+ outcome: 'deleted',
779
+ removedHistoryEntries
780
+ };
781
+ }
782
+ export function subscribeProcessExecutionStatus(listener) {
783
+ return subscribeShellProcessStatus(listener);
784
+ }
785
+ export function clearProcessExecutionStateForTests() {
786
+ const historyCleared = clearExecutionHistory();
787
+ const registry = clearShellProcessRegistryForTests();
788
+ return {
789
+ historyCleared,
790
+ registryExecutionCount: registry.executionCount,
791
+ registryActiveCount: registry.activeCount
792
+ };
793
+ }
794
+ /**
795
+ * Persist command execution result to history
796
+ * Maintains a maximum history size to prevent memory issues
797
+ *
798
+ * @param result - Command execution result to persist
799
+ */
800
+ function persistExecutionResult(result) {
801
+ executionHistory.push(result);
802
+ // Trim history if it exceeds max size
803
+ if (executionHistory.length > MAX_HISTORY_SIZE) {
804
+ const removeCount = executionHistory.length - MAX_HISTORY_SIZE;
805
+ executionHistory.splice(0, removeCount);
806
+ logger.debug('Trimmed execution history', {
807
+ removedCount: removeCount,
808
+ currentSize: executionHistory.length
809
+ });
810
+ }
811
+ logger.trace('Persisted execution result', {
812
+ command: result.command,
813
+ parameters: result.parameters,
814
+ historySize: executionHistory.length
815
+ });
816
+ }
817
+ /**
818
+ * Get command execution history
819
+ *
820
+ * @param limit - Maximum number of results to return (default: 100)
821
+ * @returns Array of command execution results, most recent first
822
+ */
823
+ export function getExecutionHistory(limit = 100) {
824
+ const limitedHistory = executionHistory.slice(-limit).reverse();
825
+ logger.debug('Retrieved execution history', {
826
+ requestedLimit: limit,
827
+ returnedCount: limitedHistory.length,
828
+ totalHistorySize: executionHistory.length
829
+ });
830
+ return limitedHistory;
831
+ }
832
+ /**
833
+ * Clear execution history
834
+ * Useful for testing or memory management
835
+ *
836
+ * @returns Number of entries cleared
837
+ */
838
+ export function clearExecutionHistory() {
839
+ const count = executionHistory.length;
840
+ executionHistory.length = 0;
841
+ logger.info('Cleared execution history', { clearedCount: count });
842
+ return count;
843
+ }
844
+ async function collectCommandArtifacts(artifactPaths, trustedWorkingDirectory) {
845
+ if (!Array.isArray(artifactPaths) || artifactPaths.length === 0) {
846
+ return [];
847
+ }
848
+ const trustedCanonical = canonicalizePath(trustedWorkingDirectory);
849
+ const artifacts = [];
850
+ for (const rawPath of artifactPaths) {
851
+ if (typeof rawPath !== 'string') {
852
+ continue;
853
+ }
854
+ const candidate = stripWrappingQuotes(rawPath);
855
+ if (!candidate) {
856
+ continue;
857
+ }
858
+ const resolvedPath = resolveTokenPath(candidate, trustedWorkingDirectory);
859
+ if (!isPathWithinTrustedDirectory(resolvedPath, trustedWorkingDirectory)) {
860
+ throw new Error(`Working directory mismatch: artifact path "${candidate}" is outside world working directory "${trustedWorkingDirectory}".`);
861
+ }
862
+ const statFn = fsPromises.stat;
863
+ if (typeof statFn === 'function') {
864
+ let stat;
865
+ try {
866
+ stat = await statFn(resolvedPath);
867
+ }
868
+ catch {
869
+ throw new Error(`Artifact not found: "${candidate}"`);
870
+ }
871
+ if (typeof stat?.isFile === 'function' && !stat.isFile()) {
872
+ throw new Error(`Artifact path is not a file: "${candidate}"`);
873
+ }
874
+ }
875
+ const readFileFn = fsPromises.readFile;
876
+ let fileBuffer = '';
877
+ if (typeof readFileFn === 'function') {
878
+ try {
879
+ const readResult = await readFileFn(resolvedPath);
880
+ fileBuffer = (readResult ?? '');
881
+ }
882
+ catch {
883
+ throw new Error(`Artifact not found: "${candidate}"`);
884
+ }
885
+ }
886
+ const sha256 = createHash('sha256').update(fileBuffer).digest('hex');
887
+ const bytes = Buffer.isBuffer(fileBuffer)
888
+ ? fileBuffer.byteLength
889
+ : Buffer.byteLength(fileBuffer);
890
+ const canonicalArtifactPath = canonicalizePath(resolvedPath);
891
+ const relativePath = relative(trustedCanonical, canonicalArtifactPath).replace(/\\/g, '/');
892
+ const isOutsideTrusted = relativePath.startsWith('..') || !relativePath;
893
+ artifacts.push({
894
+ path: isOutsideTrusted ? candidate : relativePath,
895
+ sha256,
896
+ bytes
897
+ });
898
+ }
899
+ return artifacts;
900
+ }
901
+ export function formatStructuredResult(result, artifacts = [], options = {}) {
902
+ const detail = options.detail ?? 'minimal';
903
+ const maxOutputChars = options.maxOutputChars ?? DEFAULT_MIN_OUTPUT_CHARS;
904
+ const stdoutSnippet = detail === 'full'
905
+ ? { text: result.stdout, truncated: false }
906
+ : buildOutputSnippet(result.stdout, maxOutputChars);
907
+ const stderrSnippet = detail === 'full'
908
+ ? { text: result.stderr, truncated: false }
909
+ : buildOutputSnippet(result.stderr, maxOutputChars);
910
+ return {
911
+ exit_code: result.exitCode,
912
+ stdout: stdoutSnippet.text,
913
+ stderr: stderrSnippet.text,
914
+ timed_out: Boolean(result.timedOut || result.error?.includes('timed out')),
915
+ duration_ms: result.duration,
916
+ artifacts,
917
+ ...(stdoutSnippet.truncated ? { stdout_truncated: true } : {}),
918
+ ...(stderrSnippet.truncated ? { stderr_truncated: true } : {})
919
+ };
920
+ }
921
+ /**
922
+ * Format command execution result for LLM consumption
923
+ * Provides a human-readable summary of the execution with improved markdown formatting
924
+ *
925
+ * @param result - Command execution result
926
+ * @returns Formatted markdown string suitable for LLM and display
927
+ */
928
+ export function formatResultForLLM(result, options = {}) {
929
+ const detail = options.detail ?? 'minimal';
930
+ const maxOutputChars = options.maxOutputChars ?? DEFAULT_MIN_OUTPUT_CHARS;
931
+ const stdoutSnippet = detail === 'full'
932
+ ? { text: result.stdout, truncated: false }
933
+ : buildOutputSnippet(result.stdout, maxOutputChars);
934
+ const stderrSnippet = detail === 'full'
935
+ ? { text: result.stderr, truncated: false }
936
+ : buildOutputSnippet(result.stderr, maxOutputChars);
937
+ const parts = [];
938
+ // Command info section
939
+ parts.push('### Command Execution');
940
+ parts.push('');
941
+ // Quote parameters that contain spaces or special characters
942
+ const quotedParams = result.parameters.map(param => {
943
+ if (param.includes(' ') || param.includes('\t') || param.includes('\n')) {
944
+ return `"${param.replace(/"/g, '\\"')}"`;
945
+ }
946
+ return param;
947
+ });
948
+ parts.push(`**Command:** \`${result.command} ${quotedParams.join(' ')}\``);
949
+ parts.push(`**Duration:** ${result.duration}ms`);
950
+ if (detail === 'full') {
951
+ parts.push(`**Executed at:** ${result.executedAt.toISOString()}`);
952
+ }
953
+ if (result.error) {
954
+ parts.push(`**Status:** ❌ Error`);
955
+ parts.push(`**Error:** ${result.error}`);
956
+ }
957
+ else {
958
+ parts.push(`**Status:** ${result.exitCode === 0 ? '✅' : '⚠️'} Exit code ${result.exitCode}`);
959
+ }
960
+ // Standard output section
961
+ if (stdoutSnippet.text) {
962
+ parts.push('');
963
+ parts.push(detail === 'full' ? '### Standard Output' : '### Standard Output (preview)');
964
+ parts.push('');
965
+ parts.push('```');
966
+ parts.push(stdoutSnippet.text);
967
+ parts.push('```');
968
+ if (stdoutSnippet.truncated) {
969
+ parts.push('*(Output truncated to minimum necessary preview. Use `output_detail: "full"` for full output.)*');
970
+ }
971
+ }
972
+ // Standard error section (only show if there's content)
973
+ if (stderrSnippet.text) {
974
+ parts.push('');
975
+ parts.push(detail === 'full' ? '### Standard Error' : '### Standard Error (preview)');
976
+ parts.push('');
977
+ parts.push('```');
978
+ parts.push(stderrSnippet.text);
979
+ parts.push('```');
980
+ if (stderrSnippet.truncated) {
981
+ parts.push('*(Error output truncated to minimum necessary preview. Use `output_detail: "full"` for full output.)*');
982
+ }
983
+ }
984
+ // No output case
985
+ if (!stdoutSnippet.text && !stderrSnippet.text && !result.error) {
986
+ parts.push('');
987
+ parts.push('*(No output)*');
988
+ }
989
+ return parts.join('\n');
990
+ }
991
+ /**
992
+ * Create MCP-compatible tool definition for shell_cmd
993
+ * This tool can be registered with the MCP system for LLM use
994
+ *
995
+ * @returns MCP tool definition object
996
+ */
997
+ export function createShellCmdToolDefinition() {
998
+ return {
999
+ description: 'Execute a user-requested shell command and capture output. Use this only when the user explicitly asks to run a command. Contract: `command` must be a single executable token, and each argument must be a separate `parameters` token (no mini-scripts in `command`). Working directory is resolved from trusted world context (`working_directory`) and defaults to the core default working directory (user home by default) when unset. Optional `directory` is allowed only when it resolves inside trusted scope; outside-scope requests are rejected. Path-like command arguments are scope-validated. Shell control syntax is blocked (`&&`, `||`, pipes, redirects, command substitution, backgrounding), and inline eval modes (for example `sh -c`, `node -e`, `python -c`, `powershell -Command`) are blocked. Execution uses OS shell mode (`shell: true`), so do not pass untrusted text as command content. Optional `output_format` supports `markdown` (default) and `json`; `output_detail` defaults to `minimal` to return minimum necessary output and can be set to `full`; `artifact_paths` can include files to hash and report in output metadata.',
1000
+ parameters: {
1001
+ type: 'object',
1002
+ properties: {
1003
+ command: {
1004
+ type: 'string',
1005
+ description: 'The shell command to execute (e.g., "ls", "echo", "cat", "grep")'
1006
+ },
1007
+ parameters: {
1008
+ type: 'array',
1009
+ items: { type: 'string' },
1010
+ description: 'Array of parameters/arguments for the command (e.g., ["-la", "./src"]). Pass each argument as a separate token.'
1011
+ },
1012
+ directory: {
1013
+ type: 'string',
1014
+ description: 'Optional model-provided target directory. Runtime allows this only when it resolves inside trusted world working-directory scope; outside-scope requests are rejected. If user asks for a target folder, set it here.'
1015
+ },
1016
+ timeout: {
1017
+ type: 'number',
1018
+ description: 'Timeout in milliseconds (default: 600000 = 10 minutes). Command will be terminated if it exceeds this time.'
1019
+ },
1020
+ output_format: {
1021
+ type: 'string',
1022
+ enum: ['markdown', 'json'],
1023
+ description: 'Output format for tool result. Use "markdown" (default) for human-readable output or "json" for structured output.'
1024
+ },
1025
+ output_detail: {
1026
+ type: 'string',
1027
+ enum: ['minimal', 'full'],
1028
+ description: 'Output detail level. `minimal` (default) returns bounded previews and essential metadata only; `full` returns complete stdout/stderr and timestamp fields.'
1029
+ },
1030
+ artifact_paths: {
1031
+ type: 'array',
1032
+ items: { type: 'string' },
1033
+ description: 'Optional file paths (within trusted working-directory scope) to include as hashed artifacts in result output.'
1034
+ }
1035
+ },
1036
+ required: ['command'],
1037
+ additionalProperties: false
1038
+ },
1039
+ execute: async (args, sequenceId, parentToolCall, context) => {
1040
+ // Universal parameter validation
1041
+ const toolSchema = {
1042
+ type: 'object',
1043
+ properties: {
1044
+ command: {
1045
+ type: 'string',
1046
+ description: 'The shell command to execute (e.g., "ls", "echo", "cat", "grep")'
1047
+ },
1048
+ parameters: {
1049
+ type: 'array',
1050
+ items: { type: 'string' },
1051
+ description: 'Array of parameters/arguments for the command (e.g., ["-la", "./src"]). Pass each argument as a separate token.'
1052
+ },
1053
+ directory: {
1054
+ type: 'string',
1055
+ description: 'Optional model-provided target directory. Runtime allows this only when it resolves inside trusted world working-directory scope; outside-scope requests are rejected. If user asks for a target folder, set it here.'
1056
+ },
1057
+ timeout: {
1058
+ type: 'number',
1059
+ description: 'Timeout in milliseconds (default: 600000 = 10 minutes). Command will be terminated if it exceeds this time.'
1060
+ },
1061
+ output_format: {
1062
+ type: 'string',
1063
+ enum: ['markdown', 'json'],
1064
+ description: 'Output format for tool result. Use "markdown" (default) for human-readable output or "json" for structured output.'
1065
+ },
1066
+ output_detail: {
1067
+ type: 'string',
1068
+ enum: ['minimal', 'full'],
1069
+ description: 'Output detail level. `minimal` (default) returns bounded previews and essential metadata only; `full` returns complete stdout/stderr and timestamp fields.'
1070
+ },
1071
+ artifact_paths: {
1072
+ type: 'array',
1073
+ items: { type: 'string' },
1074
+ description: 'Optional file paths (within trusted working-directory scope) to include as hashed artifacts in result output.'
1075
+ }
1076
+ },
1077
+ required: ['command']
1078
+ };
1079
+ const validation = validateToolParameters(args, toolSchema, 'shell_cmd');
1080
+ if (!validation.valid) {
1081
+ return formatResultForLLM({
1082
+ executionId: 'validation-error',
1083
+ command: args?.command || '<invalid>',
1084
+ parameters: [],
1085
+ exitCode: 1,
1086
+ signal: null,
1087
+ error: validation.error,
1088
+ stdout: '',
1089
+ stderr: '',
1090
+ executedAt: new Date(),
1091
+ duration: 0
1092
+ });
1093
+ }
1094
+ const { command, parameters = [], timeout, output_format: outputFormat = 'markdown', output_detail: outputDetail = 'minimal', artifact_paths: artifactPaths = [] } = validation.correctedArgs;
1095
+ // Ensure parameters is always an array
1096
+ const validParameters = Array.isArray(parameters) ?
1097
+ parameters.filter((p) => typeof p === 'string') :
1098
+ [];
1099
+ // Extract world and messageId from context for streaming
1100
+ const world = context?.world;
1101
+ const currentMessageId = context?.toolCallId;
1102
+ const chatId = context?.chatId ? String(context.chatId) : undefined;
1103
+ const abortSignal = context?.abortSignal;
1104
+ const resolvedDirectory = resolveTrustedShellWorkingDirectory(context);
1105
+ const directoryValidation = validateShellDirectoryRequest(validation.correctedArgs.directory, resolvedDirectory);
1106
+ if (!directoryValidation.valid) {
1107
+ throw new Error(directoryValidation.error);
1108
+ }
1109
+ const scopeValidation = validateShellCommandScope(command, validParameters, resolvedDirectory);
1110
+ if (!scopeValidation.valid) {
1111
+ throw new Error(scopeValidation.error);
1112
+ }
1113
+ // Execute command with streaming callbacks if world is available
1114
+ const result = await executeShellCommand(command, validParameters, resolvedDirectory, {
1115
+ timeout,
1116
+ abortSignal,
1117
+ worldId: world?.id,
1118
+ chatId,
1119
+ trustedWorkingDirectory: resolvedDirectory,
1120
+ onStdout: world ? (chunk) => {
1121
+ // Publish streaming events to world event system
1122
+ publishSSE(world, {
1123
+ type: 'tool-stream',
1124
+ toolName: 'shell_cmd',
1125
+ content: chunk,
1126
+ stream: 'stdout',
1127
+ messageId: currentMessageId,
1128
+ agentName: 'shell_cmd'
1129
+ });
1130
+ } : undefined,
1131
+ onStderr: world ? (chunk) => {
1132
+ // Publish streaming events to world event system
1133
+ publishSSE(world, {
1134
+ type: 'tool-stream',
1135
+ toolName: 'shell_cmd',
1136
+ content: chunk,
1137
+ stream: 'stderr',
1138
+ messageId: currentMessageId,
1139
+ agentName: 'shell_cmd'
1140
+ });
1141
+ } : undefined
1142
+ });
1143
+ if (isCommandExecutionCanceled(result)) {
1144
+ throw new DOMException('Shell command execution canceled by user', 'AbortError');
1145
+ }
1146
+ const validatedArtifactPaths = Array.isArray(artifactPaths)
1147
+ ? artifactPaths.filter((artifactPath) => typeof artifactPath === 'string')
1148
+ : [];
1149
+ const artifacts = await collectCommandArtifacts(validatedArtifactPaths, resolvedDirectory);
1150
+ if (outputFormat === 'json') {
1151
+ return JSON.stringify(formatStructuredResult(result, artifacts, { detail: outputDetail }), null, 2);
1152
+ }
1153
+ return formatResultForLLM(result, { detail: outputDetail });
1154
+ }
1155
+ };
1156
+ }
1157
+ //# sourceMappingURL=shell-cmd-tool.js.map