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.
- package/README.md +17 -7
- package/dist/cli/commands.d.ts +109 -0
- package/dist/cli/commands.js +2024 -0
- package/dist/cli/display.d.ts +124 -0
- package/dist/cli/display.js +381 -0
- package/dist/cli/hitl.d.ts +33 -0
- package/dist/cli/hitl.js +81 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/stream.d.ts +41 -0
- package/dist/cli/stream.js +222 -0
- package/dist/core/activity-tracker.d.ts +16 -0
- package/dist/core/activity-tracker.d.ts.map +1 -0
- package/dist/core/activity-tracker.js +91 -0
- package/dist/core/activity-tracker.js.map +1 -0
- package/dist/core/ai-commands.d.ts +16 -0
- package/dist/core/ai-commands.d.ts.map +1 -0
- package/dist/core/ai-commands.js +24 -0
- package/dist/core/ai-commands.js.map +1 -0
- package/dist/core/ai-sdk-patch.d.ts +24 -0
- package/dist/core/ai-sdk-patch.d.ts.map +1 -0
- package/dist/core/ai-sdk-patch.js +169 -0
- package/dist/core/ai-sdk-patch.js.map +1 -0
- package/dist/core/anthropic-direct.d.ts +52 -0
- package/dist/core/anthropic-direct.d.ts.map +1 -0
- package/dist/core/anthropic-direct.js +301 -0
- package/dist/core/anthropic-direct.js.map +1 -0
- package/dist/core/approval-cache.d.ts +104 -0
- package/dist/core/approval-cache.d.ts.map +1 -0
- package/dist/core/approval-cache.js +150 -0
- package/dist/core/approval-cache.js.map +1 -0
- package/dist/core/chat-constants.d.ts +20 -0
- package/dist/core/chat-constants.d.ts.map +1 -0
- package/dist/core/chat-constants.js +22 -0
- package/dist/core/chat-constants.js.map +1 -0
- package/dist/core/create-agent-tool.d.ts +66 -0
- package/dist/core/create-agent-tool.d.ts.map +1 -0
- package/dist/core/create-agent-tool.js +212 -0
- package/dist/core/create-agent-tool.js.map +1 -0
- package/dist/core/events/approval-checker.d.ts +61 -0
- package/dist/core/events/approval-checker.d.ts.map +1 -0
- package/dist/core/events/approval-checker.js +226 -0
- package/dist/core/events/approval-checker.js.map +1 -0
- package/dist/core/events/index.d.ts +25 -0
- package/dist/core/events/index.d.ts.map +1 -0
- package/dist/core/events/index.js +30 -0
- package/dist/core/events/index.js.map +1 -0
- package/dist/core/events/memory-manager.d.ts +73 -0
- package/dist/core/events/memory-manager.d.ts.map +1 -0
- package/dist/core/events/memory-manager.js +1218 -0
- package/dist/core/events/memory-manager.js.map +1 -0
- package/dist/core/events/mention-logic.d.ts +39 -0
- package/dist/core/events/mention-logic.d.ts.map +1 -0
- package/dist/core/events/mention-logic.js +163 -0
- package/dist/core/events/mention-logic.js.map +1 -0
- package/dist/core/events/orchestrator.d.ts +69 -0
- package/dist/core/events/orchestrator.d.ts.map +1 -0
- package/dist/core/events/orchestrator.js +883 -0
- package/dist/core/events/orchestrator.js.map +1 -0
- package/dist/core/events/persistence.d.ts +41 -0
- package/dist/core/events/persistence.d.ts.map +1 -0
- package/dist/core/events/persistence.js +296 -0
- package/dist/core/events/persistence.js.map +1 -0
- package/dist/core/events/publishers.d.ts +81 -0
- package/dist/core/events/publishers.d.ts.map +1 -0
- package/dist/core/events/publishers.js +272 -0
- package/dist/core/events/publishers.js.map +1 -0
- package/dist/core/events/subscribers.d.ts +45 -0
- package/dist/core/events/subscribers.d.ts.map +1 -0
- package/dist/core/events/subscribers.js +288 -0
- package/dist/core/events/subscribers.js.map +1 -0
- package/dist/core/events/tool-bridge-logging.d.ts +28 -0
- package/dist/core/events/tool-bridge-logging.d.ts.map +1 -0
- package/dist/core/events/tool-bridge-logging.js +94 -0
- package/dist/core/events/tool-bridge-logging.js.map +1 -0
- package/dist/core/events-metadata.d.ts +72 -0
- package/dist/core/events-metadata.d.ts.map +1 -0
- package/dist/core/events-metadata.js +167 -0
- package/dist/core/events-metadata.js.map +1 -0
- package/dist/core/events.d.ts +186 -0
- package/dist/core/events.d.ts.map +1 -0
- package/dist/core/events.js +1248 -0
- package/dist/core/events.js.map +1 -0
- package/dist/core/export.d.ts +106 -0
- package/dist/core/export.d.ts.map +1 -0
- package/dist/core/export.js +705 -0
- package/dist/core/export.js.map +1 -0
- package/dist/core/file-tools.d.ts +114 -0
- package/dist/core/file-tools.d.ts.map +1 -0
- package/dist/core/file-tools.js +370 -0
- package/dist/core/file-tools.js.map +1 -0
- package/dist/core/google-direct.d.ts +58 -0
- package/dist/core/google-direct.d.ts.map +1 -0
- package/dist/core/google-direct.js +298 -0
- package/dist/core/google-direct.js.map +1 -0
- package/dist/core/hitl.d.ts +54 -0
- package/dist/core/hitl.d.ts.map +1 -0
- package/dist/core/hitl.js +153 -0
- package/dist/core/hitl.js.map +1 -0
- package/dist/core/index.d.ts +59 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +70 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/llm-config.d.ts +128 -0
- package/dist/core/llm-config.d.ts.map +1 -0
- package/dist/core/llm-config.js +164 -0
- package/dist/core/llm-config.js.map +1 -0
- package/dist/core/llm-manager.d.ts +163 -0
- package/dist/core/llm-manager.d.ts.map +1 -0
- package/dist/core/llm-manager.js +669 -0
- package/dist/core/llm-manager.js.map +1 -0
- package/dist/core/load-skill-tool.d.ts +55 -0
- package/dist/core/load-skill-tool.d.ts.map +1 -0
- package/dist/core/load-skill-tool.js +468 -0
- package/dist/core/load-skill-tool.js.map +1 -0
- package/dist/core/logger.d.ts +88 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +358 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/managers.d.ts +131 -0
- package/dist/core/managers.d.ts.map +1 -0
- package/dist/core/managers.js +1223 -0
- package/dist/core/managers.js.map +1 -0
- package/dist/core/mcp-server-registry.d.ts +304 -0
- package/dist/core/mcp-server-registry.d.ts.map +1 -0
- package/dist/core/mcp-server-registry.js +1769 -0
- package/dist/core/mcp-server-registry.js.map +1 -0
- package/dist/core/mcp-tools.d.ts +56 -0
- package/dist/core/mcp-tools.d.ts.map +1 -0
- package/dist/core/mcp-tools.js +186 -0
- package/dist/core/mcp-tools.js.map +1 -0
- package/dist/core/message-prep.d.ts +81 -0
- package/dist/core/message-prep.d.ts.map +1 -0
- package/dist/core/message-prep.js +223 -0
- package/dist/core/message-prep.js.map +1 -0
- package/dist/core/message-processing-control.d.ts +54 -0
- package/dist/core/message-processing-control.d.ts.map +1 -0
- package/dist/core/message-processing-control.js +139 -0
- package/dist/core/message-processing-control.js.map +1 -0
- package/dist/core/openai-direct.d.ts +80 -0
- package/dist/core/openai-direct.d.ts.map +1 -0
- package/dist/core/openai-direct.js +374 -0
- package/dist/core/openai-direct.js.map +1 -0
- package/dist/core/shell-cmd-tool.d.ts +235 -0
- package/dist/core/shell-cmd-tool.d.ts.map +1 -0
- package/dist/core/shell-cmd-tool.js +1157 -0
- package/dist/core/shell-cmd-tool.js.map +1 -0
- package/dist/core/shell-process-registry.d.ts +88 -0
- package/dist/core/shell-process-registry.d.ts.map +1 -0
- package/dist/core/shell-process-registry.js +309 -0
- package/dist/core/shell-process-registry.js.map +1 -0
- package/dist/core/skill-registry.d.ts +75 -0
- package/dist/core/skill-registry.d.ts.map +1 -0
- package/dist/core/skill-registry.js +369 -0
- package/dist/core/skill-registry.js.map +1 -0
- package/dist/core/skill-script-runner.d.ts +89 -0
- package/dist/core/skill-script-runner.d.ts.map +1 -0
- package/dist/core/skill-script-runner.js +274 -0
- package/dist/core/skill-script-runner.js.map +1 -0
- package/dist/core/skill-selector.d.ts +65 -0
- package/dist/core/skill-selector.d.ts.map +1 -0
- package/dist/core/skill-selector.js +190 -0
- package/dist/core/skill-selector.js.map +1 -0
- package/dist/core/skill-settings.d.ts +20 -0
- package/dist/core/skill-settings.d.ts.map +1 -0
- package/dist/core/skill-settings.js +40 -0
- package/dist/core/skill-settings.js.map +1 -0
- package/dist/core/storage/agent-storage.d.ts +134 -0
- package/dist/core/storage/agent-storage.d.ts.map +1 -0
- package/dist/core/storage/agent-storage.js +498 -0
- package/dist/core/storage/agent-storage.js.map +1 -0
- package/dist/core/storage/eventStorage/fileEventStorage.d.ts +100 -0
- package/dist/core/storage/eventStorage/fileEventStorage.d.ts.map +1 -0
- package/dist/core/storage/eventStorage/fileEventStorage.js +494 -0
- package/dist/core/storage/eventStorage/fileEventStorage.js.map +1 -0
- package/dist/core/storage/eventStorage/index.d.ts +31 -0
- package/dist/core/storage/eventStorage/index.d.ts.map +1 -0
- package/dist/core/storage/eventStorage/index.js +31 -0
- package/dist/core/storage/eventStorage/index.js.map +1 -0
- package/dist/core/storage/eventStorage/memoryEventStorage.d.ts +87 -0
- package/dist/core/storage/eventStorage/memoryEventStorage.d.ts.map +1 -0
- package/dist/core/storage/eventStorage/memoryEventStorage.js +244 -0
- package/dist/core/storage/eventStorage/memoryEventStorage.js.map +1 -0
- package/dist/core/storage/eventStorage/sqliteEventStorage.d.ts +45 -0
- package/dist/core/storage/eventStorage/sqliteEventStorage.d.ts.map +1 -0
- package/dist/core/storage/eventStorage/sqliteEventStorage.js +301 -0
- package/dist/core/storage/eventStorage/sqliteEventStorage.js.map +1 -0
- package/dist/core/storage/eventStorage/types.d.ts +142 -0
- package/dist/core/storage/eventStorage/types.d.ts.map +1 -0
- package/dist/core/storage/eventStorage/types.js +43 -0
- package/dist/core/storage/eventStorage/types.js.map +1 -0
- package/dist/core/storage/eventStorage/validation.d.ts +30 -0
- package/dist/core/storage/eventStorage/validation.d.ts.map +1 -0
- package/dist/core/storage/eventStorage/validation.js +68 -0
- package/dist/core/storage/eventStorage/validation.js.map +1 -0
- package/dist/core/storage/legacy-migrations.d.ts +45 -0
- package/dist/core/storage/legacy-migrations.d.ts.map +1 -0
- package/dist/core/storage/legacy-migrations.js +295 -0
- package/dist/core/storage/legacy-migrations.js.map +1 -0
- package/dist/core/storage/memory-storage.d.ts +105 -0
- package/dist/core/storage/memory-storage.d.ts.map +1 -0
- package/dist/core/storage/memory-storage.js +415 -0
- package/dist/core/storage/memory-storage.js.map +1 -0
- package/dist/core/storage/migration-runner.d.ts +96 -0
- package/dist/core/storage/migration-runner.d.ts.map +1 -0
- package/dist/core/storage/migration-runner.js +306 -0
- package/dist/core/storage/migration-runner.js.map +1 -0
- package/dist/core/storage/queue-storage.d.ts +147 -0
- package/dist/core/storage/queue-storage.d.ts.map +1 -0
- package/dist/core/storage/queue-storage.js +290 -0
- package/dist/core/storage/queue-storage.js.map +1 -0
- package/dist/core/storage/skill-storage.d.ts +136 -0
- package/dist/core/storage/skill-storage.d.ts.map +1 -0
- package/dist/core/storage/skill-storage.js +474 -0
- package/dist/core/storage/skill-storage.js.map +1 -0
- package/dist/core/storage/sqlite-schema.d.ts +95 -0
- package/dist/core/storage/sqlite-schema.d.ts.map +1 -0
- package/dist/core/storage/sqlite-schema.js +156 -0
- package/dist/core/storage/sqlite-schema.js.map +1 -0
- package/dist/core/storage/sqlite-storage.d.ts +146 -0
- package/dist/core/storage/sqlite-storage.d.ts.map +1 -0
- package/dist/core/storage/sqlite-storage.js +709 -0
- package/dist/core/storage/sqlite-storage.js.map +1 -0
- package/dist/core/storage/storage-factory.d.ts +61 -0
- package/dist/core/storage/storage-factory.d.ts.map +1 -0
- package/dist/core/storage/storage-factory.js +794 -0
- package/dist/core/storage/storage-factory.js.map +1 -0
- package/dist/core/storage/validation.d.ts +36 -0
- package/dist/core/storage/validation.d.ts.map +1 -0
- package/dist/core/storage/validation.js +79 -0
- package/dist/core/storage/validation.js.map +1 -0
- package/dist/core/storage/world-storage.d.ts +114 -0
- package/dist/core/storage/world-storage.d.ts.map +1 -0
- package/dist/core/storage/world-storage.js +378 -0
- package/dist/core/storage/world-storage.js.map +1 -0
- package/dist/core/subscription.d.ts +43 -0
- package/dist/core/subscription.d.ts.map +1 -0
- package/dist/core/subscription.js +227 -0
- package/dist/core/subscription.js.map +1 -0
- package/dist/core/tool-utils.d.ts +80 -0
- package/dist/core/tool-utils.d.ts.map +1 -0
- package/dist/core/tool-utils.js +273 -0
- package/dist/core/tool-utils.js.map +1 -0
- package/dist/core/types.d.ts +595 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +158 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/utils.d.ts +138 -0
- package/dist/core/utils.d.ts.map +1 -0
- package/dist/core/utils.js +478 -0
- package/dist/core/utils.js.map +1 -0
- package/dist/core/world-class.d.ts +43 -0
- package/dist/core/world-class.d.ts.map +1 -0
- package/dist/core/world-class.js +90 -0
- package/dist/core/world-class.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/public/assets/agent-sprites-DJFgj-zP.png +0 -0
- package/dist/public/assets/border-KHK37r8y.svg +83 -0
- package/dist/public/assets/index-C9kPXL6G.css +1 -0
- package/dist/public/assets/index-DOQEHGWt.js +96 -0
- package/dist/public/index.html +21 -0
- package/dist/server/api.d.ts +2 -0
- package/dist/server/api.js +1124 -0
- package/dist/server/index.d.ts +29 -0
- package/dist/server/sse-handler.d.ts +62 -0
- package/dist/server/sse-handler.js +234 -0
- package/package.json +15 -3
- 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
|