fixo-cli 1.0.4 → 2.0.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.
Potentially problematic release.
This version of fixo-cli might be problematic. Click here for more details.
- package/CHANGELOG.md +62 -0
- package/README.md +18 -14
- package/dist/agent/agent-client.d.ts +28 -6
- package/dist/agent/agent-client.d.ts.map +1 -1
- package/dist/agent/agent-client.js +118 -39
- package/dist/agent/agent-client.js.map +1 -1
- package/dist/agent/agent-pool.d.ts +55 -6
- package/dist/agent/agent-pool.d.ts.map +1 -1
- package/dist/agent/agent-pool.js +120 -20
- package/dist/agent/agent-pool.js.map +1 -1
- package/dist/agent/auto-verifier.d.ts +55 -0
- package/dist/agent/auto-verifier.d.ts.map +1 -0
- package/dist/agent/auto-verifier.js +50 -0
- package/dist/agent/auto-verifier.js.map +1 -0
- package/dist/agent/command-parser.d.ts.map +1 -1
- package/dist/agent/command-parser.js +176 -0
- package/dist/agent/command-parser.js.map +1 -1
- package/dist/agent/context-builder.d.ts +24 -0
- package/dist/agent/context-builder.d.ts.map +1 -0
- package/dist/agent/context-builder.js +197 -0
- package/dist/agent/context-builder.js.map +1 -0
- package/dist/agent/conversation.d.ts +14 -1
- package/dist/agent/conversation.d.ts.map +1 -1
- package/dist/agent/conversation.js +53 -7
- package/dist/agent/conversation.js.map +1 -1
- package/dist/agent/mcp-bridge.js +1 -1
- package/dist/agent/mcp-bridge.js.map +1 -1
- package/dist/agent/orchestrator.d.ts +45 -0
- package/dist/agent/orchestrator.d.ts.map +1 -1
- package/dist/agent/orchestrator.js +140 -3
- package/dist/agent/orchestrator.js.map +1 -1
- package/dist/agent/parser-adapter.d.ts +17 -0
- package/dist/agent/parser-adapter.d.ts.map +1 -1
- package/dist/agent/parser-adapter.js +254 -2
- package/dist/agent/parser-adapter.js.map +1 -1
- package/dist/agent/predictive-gate.d.ts.map +1 -1
- package/dist/agent/predictive-gate.js +4 -1
- package/dist/agent/predictive-gate.js.map +1 -1
- package/dist/agent/providers-manager.d.ts +5 -0
- package/dist/agent/providers-manager.d.ts.map +1 -1
- package/dist/agent/providers-manager.js +119 -8
- package/dist/agent/providers-manager.js.map +1 -1
- package/dist/agent/repo-map.d.ts +18 -1
- package/dist/agent/repo-map.d.ts.map +1 -1
- package/dist/agent/repo-map.js +144 -54
- package/dist/agent/repo-map.js.map +1 -1
- package/dist/agent/retry.js +1 -2
- package/dist/agent/retry.js.map +1 -1
- package/dist/agent/single-agent.d.ts.map +1 -1
- package/dist/agent/single-agent.js +129 -22
- package/dist/agent/single-agent.js.map +1 -1
- package/dist/agent/skills.d.ts.map +1 -1
- package/dist/agent/skills.js +2 -1
- package/dist/agent/skills.js.map +1 -1
- package/dist/agent/subagent.js +2 -2
- package/dist/agent/subagent.js.map +1 -1
- package/dist/agent/task-router.d.ts +46 -0
- package/dist/agent/task-router.d.ts.map +1 -0
- package/dist/agent/task-router.js +352 -0
- package/dist/agent/task-router.js.map +1 -0
- package/dist/agent/telemetry.d.ts +29 -1
- package/dist/agent/telemetry.d.ts.map +1 -1
- package/dist/agent/telemetry.js +25 -10
- package/dist/agent/telemetry.js.map +1 -1
- package/dist/agent/tool-definitions.d.ts +3 -0
- package/dist/agent/tool-definitions.d.ts.map +1 -0
- package/dist/agent/tool-definitions.js +519 -0
- package/dist/agent/tool-definitions.js.map +1 -0
- package/dist/agent/tool-executor.d.ts +6 -1
- package/dist/agent/tool-executor.d.ts.map +1 -1
- package/dist/agent/tool-executor.js +99 -553
- package/dist/agent/tool-executor.js.map +1 -1
- package/dist/agent/tools/command-tools.d.ts +6 -0
- package/dist/agent/tools/command-tools.d.ts.map +1 -0
- package/dist/agent/tools/command-tools.js +104 -0
- package/dist/agent/tools/command-tools.js.map +1 -0
- package/dist/agent/tools/file-tools.d.ts +15 -0
- package/dist/agent/tools/file-tools.d.ts.map +1 -0
- package/dist/agent/tools/file-tools.js +551 -0
- package/dist/agent/tools/file-tools.js.map +1 -0
- package/dist/agent/tools/todo-tools.d.ts +3 -0
- package/dist/agent/tools/todo-tools.d.ts.map +1 -0
- package/dist/agent/tools/todo-tools.js +70 -0
- package/dist/agent/tools/todo-tools.js.map +1 -0
- package/dist/agent/web-impl.d.ts.map +1 -1
- package/dist/agent/web-impl.js +45 -0
- package/dist/agent/web-impl.js.map +1 -1
- package/dist/agent/worker-agent.d.ts +3 -1
- package/dist/agent/worker-agent.d.ts.map +1 -1
- package/dist/agent/worker-agent.js +51 -14
- package/dist/agent/worker-agent.js.map +1 -1
- package/dist/config.d.ts +242 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +79 -0
- package/dist/config.js.map +1 -1
- package/dist/git/git-manager.d.ts +33 -2
- package/dist/git/git-manager.d.ts.map +1 -1
- package/dist/git/git-manager.js +111 -15
- package/dist/git/git-manager.js.map +1 -1
- package/dist/git/git-ops.d.ts.map +1 -1
- package/dist/git/git-ops.js +2 -1
- package/dist/git/git-ops.js.map +1 -1
- package/dist/index.js +85 -8
- package/dist/index.js.map +1 -1
- package/dist/lsp/lsp-manager.js +1 -1
- package/dist/lsp/lsp-manager.js.map +1 -1
- package/dist/model-outcomes.d.ts.map +1 -1
- package/dist/model-outcomes.js +2 -1
- package/dist/model-outcomes.js.map +1 -1
- package/dist/planner.d.ts +0 -9
- package/dist/planner.d.ts.map +1 -1
- package/dist/planner.js +0 -9
- package/dist/planner.js.map +1 -1
- package/dist/project-memory.d.ts +12 -1
- package/dist/project-memory.d.ts.map +1 -1
- package/dist/project-memory.js +8 -6
- package/dist/project-memory.js.map +1 -1
- package/dist/runtime/loop-mitigation.d.ts +78 -7
- package/dist/runtime/loop-mitigation.d.ts.map +1 -1
- package/dist/runtime/loop-mitigation.js +122 -9
- package/dist/runtime/loop-mitigation.js.map +1 -1
- package/dist/runtime/os-sandbox.d.ts +100 -0
- package/dist/runtime/os-sandbox.d.ts.map +1 -0
- package/dist/runtime/os-sandbox.js +246 -0
- package/dist/runtime/os-sandbox.js.map +1 -0
- package/dist/runtime/run-inventory.d.ts +17 -0
- package/dist/runtime/run-inventory.d.ts.map +1 -0
- package/dist/runtime/run-inventory.js +49 -0
- package/dist/runtime/run-inventory.js.map +1 -0
- package/dist/runtime/staging.d.ts.map +1 -1
- package/dist/runtime/staging.js +4 -1
- package/dist/runtime/staging.js.map +1 -1
- package/dist/runtime/task-session.d.ts +14 -0
- package/dist/runtime/task-session.d.ts.map +1 -1
- package/dist/runtime/task-session.js +26 -0
- package/dist/runtime/task-session.js.map +1 -1
- package/dist/setup-wizard.d.ts +11 -3
- package/dist/setup-wizard.d.ts.map +1 -1
- package/dist/setup-wizard.js +113 -15
- package/dist/setup-wizard.js.map +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/commands/context-commands.d.ts +7 -0
- package/dist/ui/commands/context-commands.d.ts.map +1 -0
- package/dist/ui/commands/context-commands.js +241 -0
- package/dist/ui/commands/context-commands.js.map +1 -0
- package/dist/ui/commands/index.d.ts +3 -0
- package/dist/ui/commands/index.d.ts.map +1 -0
- package/dist/ui/commands/index.js +46 -0
- package/dist/ui/commands/index.js.map +1 -0
- package/dist/ui/commands/info-commands.d.ts +15 -0
- package/dist/ui/commands/info-commands.d.ts.map +1 -0
- package/dist/ui/commands/info-commands.js +122 -0
- package/dist/ui/commands/info-commands.js.map +1 -0
- package/dist/ui/commands/model-commands.d.ts +5 -0
- package/dist/ui/commands/model-commands.d.ts.map +1 -0
- package/dist/ui/commands/model-commands.js +417 -0
- package/dist/ui/commands/model-commands.js.map +1 -0
- package/dist/ui/commands/session-commands.d.ts +5 -0
- package/dist/ui/commands/session-commands.d.ts.map +1 -0
- package/dist/ui/commands/session-commands.js +154 -0
- package/dist/ui/commands/session-commands.js.map +1 -0
- package/dist/ui/commands/task-commands.d.ts +8 -0
- package/dist/ui/commands/task-commands.d.ts.map +1 -0
- package/dist/ui/commands/task-commands.js +152 -0
- package/dist/ui/commands/task-commands.js.map +1 -0
- package/dist/ui/commands/types.d.ts +46 -0
- package/dist/ui/commands/types.d.ts.map +1 -0
- package/dist/ui/commands/types.js +2 -0
- package/dist/ui/commands/types.js.map +1 -0
- package/dist/ui/commands/workspace-commands.d.ts +8 -0
- package/dist/ui/commands/workspace-commands.d.ts.map +1 -0
- package/dist/ui/commands/workspace-commands.js +131 -0
- package/dist/ui/commands/workspace-commands.js.map +1 -0
- package/dist/ui/loading-animation.d.ts +24 -0
- package/dist/ui/loading-animation.d.ts.map +1 -0
- package/dist/ui/loading-animation.js +123 -0
- package/dist/ui/loading-animation.js.map +1 -0
- package/dist/ui/markdown-stream.js +2 -2
- package/dist/ui/markdown-stream.js.map +1 -1
- package/dist/ui/prompt.d.ts +7 -0
- package/dist/ui/prompt.d.ts.map +1 -1
- package/dist/ui/prompt.js +435 -1214
- package/dist/ui/prompt.js.map +1 -1
- package/dist/ui/render-primitives.d.ts +6 -0
- package/dist/ui/render-primitives.d.ts.map +1 -1
- package/dist/ui/render-primitives.js +30 -13
- package/dist/ui/render-primitives.js.map +1 -1
- package/dist/ui/render.d.ts.map +1 -1
- package/dist/ui/render.js +2 -0
- package/dist/ui/render.js.map +1 -1
- package/package.json +17 -3
- package/scripts/check-vendor-wasm.js +11 -0
- package/vendor/tree-sitter-go.wasm +0 -0
- package/vendor/tree-sitter-javascript.wasm +0 -0
- package/vendor/tree-sitter-python.wasm +0 -0
- package/vendor/tree-sitter-rust.wasm +0 -0
- package/vendor/tree-sitter-tsx.wasm +0 -0
- package/vendor/tree-sitter-typescript.wasm +0 -0
|
@@ -2,18 +2,23 @@
|
|
|
2
2
|
* Tool definitions and executor for the single-agent tool-calling loop.
|
|
3
3
|
* Provides: read_file, write_file, run_command, search_code, list_dir
|
|
4
4
|
*/
|
|
5
|
+
import { TOOL_DEFINITIONS } from './tool-definitions.js';
|
|
6
|
+
export { TOOL_DEFINITIONS };
|
|
5
7
|
import fs from 'fs';
|
|
6
8
|
import path from 'path';
|
|
7
9
|
import { spawnSync } from 'child_process';
|
|
10
|
+
import { randomBytes } from 'node:crypto';
|
|
8
11
|
import { colors } from '../ui/colors.js';
|
|
9
12
|
import { renderToolCall, startInlineToolSpinner, } from '../ui/render-primitives.js';
|
|
10
13
|
import { WorkspaceGuard } from '../workspace-guard.js';
|
|
11
14
|
import { classifyCommand } from '../runtime/policy.js';
|
|
12
15
|
import { checkPermission } from './permissions.js';
|
|
13
16
|
import { redactedEnv, redactSecrets } from '../runtime/redaction.js';
|
|
17
|
+
import { runSandboxed, SandboxUnavailableError } from '../runtime/os-sandbox.js';
|
|
14
18
|
import { McpManager } from './mcp-manager.js';
|
|
15
19
|
import { estimateReadCost, shouldDeferRead, formatPredictiveGateDirective, DEFAULT_PREDICTIVE_BUDGET_PCT } from './predictive-gate.js';
|
|
16
20
|
import { createBranch, commitChanges, pushBranch, createPullRequest } from '../git/git-ops.js';
|
|
21
|
+
import { GitManager } from '../git/git-manager.js';
|
|
17
22
|
import { pathToFileURL } from 'url';
|
|
18
23
|
import * as p from '@clack/prompts';
|
|
19
24
|
import { loadConfig, saveConfig } from '../config.js';
|
|
@@ -29,6 +34,7 @@ import { recordTelemetry, telemetry } from './telemetry.js';
|
|
|
29
34
|
import { applyModifiedArgs, fireHooks } from './hooks.js';
|
|
30
35
|
import { BackgroundJobRegistry } from '../runtime/background-jobs.js';
|
|
31
36
|
import { ParserFactory, languageIdFromExtension, } from './parser-adapter.js';
|
|
37
|
+
import { getRunInventory } from '../runtime/run-inventory.js';
|
|
32
38
|
export const mcpManager = new McpManager();
|
|
33
39
|
export const mcpBridgeManager = new McpBridgeManager();
|
|
34
40
|
let lspManagerInstance = null;
|
|
@@ -192,524 +198,6 @@ export function classifyExecutionRole(task) {
|
|
|
192
198
|
}
|
|
193
199
|
return 'BUILD';
|
|
194
200
|
}
|
|
195
|
-
export const TOOL_DEFINITIONS = [
|
|
196
|
-
{
|
|
197
|
-
type: 'function',
|
|
198
|
-
function: {
|
|
199
|
-
name: 'read_file',
|
|
200
|
-
description: 'Read the full text contents of a file at the given path. Use this to understand existing code before making changes. Returns the file contents as a string. Files larger than the large-file gate (15 KiB / 350 lines by default) will return a [Context-Budget Guard] synthetic directive telling you to call extract_symbols or extract_imports first.',
|
|
201
|
-
parameters: {
|
|
202
|
-
type: 'object',
|
|
203
|
-
properties: {
|
|
204
|
-
path: {
|
|
205
|
-
type: 'string',
|
|
206
|
-
description: 'The file path to read, relative to the workspace root or absolute.',
|
|
207
|
-
},
|
|
208
|
-
},
|
|
209
|
-
required: ['path'],
|
|
210
|
-
},
|
|
211
|
-
},
|
|
212
|
-
},
|
|
213
|
-
{
|
|
214
|
-
type: 'function',
|
|
215
|
-
function: {
|
|
216
|
-
name: 'extract_symbols',
|
|
217
|
-
description: 'Extract symbol declarations (classes, functions, interfaces, types, consts) from a file. Output is capped at 100 entries. Cheaper than read_file for large files because it skips the body content.',
|
|
218
|
-
parameters: {
|
|
219
|
-
type: 'object',
|
|
220
|
-
properties: {
|
|
221
|
-
path: {
|
|
222
|
-
type: 'string',
|
|
223
|
-
description: 'The file path to inspect, relative to the workspace root or absolute.',
|
|
224
|
-
},
|
|
225
|
-
},
|
|
226
|
-
required: ['path'],
|
|
227
|
-
},
|
|
228
|
-
},
|
|
229
|
-
},
|
|
230
|
-
{
|
|
231
|
-
type: 'function',
|
|
232
|
-
function: {
|
|
233
|
-
name: 'extract_imports',
|
|
234
|
-
description: 'Extract import statements from a file. Output is capped at 100 entries. Cheaper than read_file for large files because it skips the body content.',
|
|
235
|
-
parameters: {
|
|
236
|
-
type: 'object',
|
|
237
|
-
properties: {
|
|
238
|
-
path: {
|
|
239
|
-
type: 'string',
|
|
240
|
-
description: 'The file path to inspect, relative to the workspace root or absolute.',
|
|
241
|
-
},
|
|
242
|
-
},
|
|
243
|
-
required: ['path'],
|
|
244
|
-
},
|
|
245
|
-
},
|
|
246
|
-
},
|
|
247
|
-
{
|
|
248
|
-
type: 'function',
|
|
249
|
-
function: {
|
|
250
|
-
name: 'apply_patch',
|
|
251
|
-
description: 'Apply a unified diff patch to files in the workspace. Prefer this over write_file for editing existing files, and NEVER substitute it with `sed -i`, `patch < file`, or `python3 -c` — shell file-writing is sandbox-blocked.',
|
|
252
|
-
parameters: {
|
|
253
|
-
type: 'object',
|
|
254
|
-
properties: {
|
|
255
|
-
patch: { type: 'string', description: 'Unified diff patch text.' },
|
|
256
|
-
},
|
|
257
|
-
required: ['patch'],
|
|
258
|
-
},
|
|
259
|
-
},
|
|
260
|
-
},
|
|
261
|
-
{
|
|
262
|
-
type: 'function',
|
|
263
|
-
function: {
|
|
264
|
-
name: 'replace_range',
|
|
265
|
-
description: 'Replace inclusive 1-based line range in a file. Requires reading the file first.',
|
|
266
|
-
parameters: {
|
|
267
|
-
type: 'object',
|
|
268
|
-
properties: {
|
|
269
|
-
path: { type: 'string' },
|
|
270
|
-
startLine: { type: 'string' },
|
|
271
|
-
endLine: { type: 'string' },
|
|
272
|
-
content: { type: 'string' },
|
|
273
|
-
},
|
|
274
|
-
required: ['path', 'startLine', 'endLine', 'content'],
|
|
275
|
-
},
|
|
276
|
-
},
|
|
277
|
-
},
|
|
278
|
-
{
|
|
279
|
-
type: 'function',
|
|
280
|
-
function: {
|
|
281
|
-
name: 'insert_after',
|
|
282
|
-
description: 'Insert content after the first exact anchor match in a file. Requires reading the file first.',
|
|
283
|
-
parameters: {
|
|
284
|
-
type: 'object',
|
|
285
|
-
properties: {
|
|
286
|
-
path: { type: 'string' },
|
|
287
|
-
anchor: { type: 'string' },
|
|
288
|
-
content: { type: 'string' },
|
|
289
|
-
},
|
|
290
|
-
required: ['path', 'anchor', 'content'],
|
|
291
|
-
},
|
|
292
|
-
},
|
|
293
|
-
},
|
|
294
|
-
{
|
|
295
|
-
type: 'function',
|
|
296
|
-
function: {
|
|
297
|
-
name: 'rename_file',
|
|
298
|
-
description: 'Rename or move a workspace file.',
|
|
299
|
-
parameters: {
|
|
300
|
-
type: 'object',
|
|
301
|
-
properties: {
|
|
302
|
-
from: { type: 'string' },
|
|
303
|
-
to: { type: 'string' },
|
|
304
|
-
},
|
|
305
|
-
required: ['from', 'to'],
|
|
306
|
-
},
|
|
307
|
-
},
|
|
308
|
-
},
|
|
309
|
-
{
|
|
310
|
-
type: 'function',
|
|
311
|
-
function: {
|
|
312
|
-
name: 'write_file',
|
|
313
|
-
description: 'Write a complete file to disk. Use ONLY for new files or full rewrites where the prior content is irrelevant. Never use `run_command` with `cat > file`, heredocs, `tee`, `sed -i`, or `python3 -c`/`node -e` to write files — those are sandbox-blocked. For ANY change to an existing file you MUST use `str_replace` (single hunk) or `apply_patch` (multi-region). Rewriting an existing file with write_file when str_replace would do wastes tokens, defeats LSP granularity, and risks clobbering concurrent edits. Creates parent directories if missing.',
|
|
314
|
-
parameters: {
|
|
315
|
-
type: 'object',
|
|
316
|
-
properties: {
|
|
317
|
-
path: {
|
|
318
|
-
type: 'string',
|
|
319
|
-
description: 'The file path to write, relative to the workspace root or absolute.',
|
|
320
|
-
},
|
|
321
|
-
content: {
|
|
322
|
-
type: 'string',
|
|
323
|
-
description: 'The full file content to write.',
|
|
324
|
-
},
|
|
325
|
-
},
|
|
326
|
-
required: ['path', 'content'],
|
|
327
|
-
},
|
|
328
|
-
},
|
|
329
|
-
},
|
|
330
|
-
{
|
|
331
|
-
type: 'function',
|
|
332
|
-
function: {
|
|
333
|
-
name: 'run_command',
|
|
334
|
-
description: 'Execute a shell command and return its stdout and stderr output. Use this to run tests, build projects, install dependencies, or verify changes. Commands run in the workspace directory. DO NOT use this tool to write or edit files — shell-based file writing is sandbox-blocked, including: `>` / `>>` redirects, `cat > file <<EOF` heredocs, `tee`, `sed -i`, `mv`/`cp` into source paths, and interpreter payloads like `python3 -c "open(...,\'w\')"`, `node -e "fs.writeFileSync(...)"`. Attempts to write files this way will be rejected with a security error. For file writes use `write_file` (new files / full rewrites), `str_replace` (single-region edit), or `apply_patch` (multi-region diff) — they all go through the same atomic staging + LSP pre-save pipeline.',
|
|
335
|
-
parameters: {
|
|
336
|
-
type: 'object',
|
|
337
|
-
properties: {
|
|
338
|
-
command: {
|
|
339
|
-
type: 'string',
|
|
340
|
-
description: 'The shell command to execute.',
|
|
341
|
-
},
|
|
342
|
-
cwd: {
|
|
343
|
-
type: 'string',
|
|
344
|
-
description: 'Working directory for the command (optional, defaults to workspace root).',
|
|
345
|
-
},
|
|
346
|
-
},
|
|
347
|
-
required: ['command'],
|
|
348
|
-
},
|
|
349
|
-
},
|
|
350
|
-
},
|
|
351
|
-
{
|
|
352
|
-
type: 'function',
|
|
353
|
-
function: {
|
|
354
|
-
name: 'run_command_async',
|
|
355
|
-
description: 'Spawn a long-running command in the background and return immediately with a jobId. Use poll_command_status to retrieve output and kill_command to terminate. Output streams cap at 64 KiB each; the larger totalByte counters survive the cap. Rejected in PLAN mode. Workspace-escape and sensitive-file commands are blocked at spawn time by the same command-parser used by run_command.',
|
|
356
|
-
parameters: {
|
|
357
|
-
type: 'object',
|
|
358
|
-
properties: {
|
|
359
|
-
cmd: { type: 'string', description: 'The binary to spawn.' },
|
|
360
|
-
args: {
|
|
361
|
-
type: 'array',
|
|
362
|
-
items: { type: 'string' },
|
|
363
|
-
description: 'Argument vector.',
|
|
364
|
-
},
|
|
365
|
-
cwd: {
|
|
366
|
-
type: 'string',
|
|
367
|
-
description: 'Working directory (defaults to workspace root).',
|
|
368
|
-
},
|
|
369
|
-
},
|
|
370
|
-
required: ['cmd'],
|
|
371
|
-
},
|
|
372
|
-
},
|
|
373
|
-
},
|
|
374
|
-
{
|
|
375
|
-
type: 'function',
|
|
376
|
-
function: {
|
|
377
|
-
name: 'poll_command_status',
|
|
378
|
-
description: 'Read a snapshot of a background job. The snapshot includes status, exitCode, startedAt/exitedAt, and the (capped) stdout and stderr streams. tailLines truncates each stream to its last N lines; sinceBytes returns only the bytes after the given offset on stdout/stderr (delta fields).',
|
|
379
|
-
parameters: {
|
|
380
|
-
type: 'object',
|
|
381
|
-
properties: {
|
|
382
|
-
jobId: { type: 'string', description: 'The jobId returned by run_command_async.' },
|
|
383
|
-
tailLines: { type: 'integer', description: 'Truncate each stream to last N lines.' },
|
|
384
|
-
sinceBytes: { type: 'integer', description: 'Return only bytes after this offset.' },
|
|
385
|
-
},
|
|
386
|
-
required: ['jobId'],
|
|
387
|
-
},
|
|
388
|
-
},
|
|
389
|
-
},
|
|
390
|
-
{
|
|
391
|
-
type: 'function',
|
|
392
|
-
function: {
|
|
393
|
-
name: 'kill_command',
|
|
394
|
-
description: 'Send SIGTERM to a background job. No-op if the job has already exited.',
|
|
395
|
-
parameters: {
|
|
396
|
-
type: 'object',
|
|
397
|
-
properties: {
|
|
398
|
-
jobId: { type: 'string', description: 'The jobId to terminate.' },
|
|
399
|
-
},
|
|
400
|
-
required: ['jobId'],
|
|
401
|
-
},
|
|
402
|
-
},
|
|
403
|
-
},
|
|
404
|
-
{
|
|
405
|
-
type: 'function',
|
|
406
|
-
function: {
|
|
407
|
-
name: 'search_code',
|
|
408
|
-
description: 'Search for a text or regex pattern in workspace files. Returns matching lines with file paths and line numbers. Use this to find where functions, classes, or variables are defined or used.',
|
|
409
|
-
parameters: {
|
|
410
|
-
type: 'object',
|
|
411
|
-
properties: {
|
|
412
|
-
query: {
|
|
413
|
-
type: 'string',
|
|
414
|
-
description: 'The search pattern (plain text or regex).',
|
|
415
|
-
},
|
|
416
|
-
path: {
|
|
417
|
-
type: 'string',
|
|
418
|
-
description: 'Directory or file to search in (optional, defaults to workspace root).',
|
|
419
|
-
},
|
|
420
|
-
file_pattern: {
|
|
421
|
-
type: 'string',
|
|
422
|
-
description: 'Glob pattern to filter files, e.g., "*.ts" or "*.py" (optional).',
|
|
423
|
-
},
|
|
424
|
-
},
|
|
425
|
-
required: ['query'],
|
|
426
|
-
},
|
|
427
|
-
},
|
|
428
|
-
},
|
|
429
|
-
{
|
|
430
|
-
type: 'function',
|
|
431
|
-
function: {
|
|
432
|
-
name: 'list_dir',
|
|
433
|
-
description: 'List files and directories at the given path. Returns names, types (file/dir), and sizes.',
|
|
434
|
-
parameters: {
|
|
435
|
-
type: 'object',
|
|
436
|
-
properties: {
|
|
437
|
-
path: {
|
|
438
|
-
type: 'string',
|
|
439
|
-
description: 'The directory path to list (optional, defaults to workspace root).',
|
|
440
|
-
},
|
|
441
|
-
},
|
|
442
|
-
required: [],
|
|
443
|
-
},
|
|
444
|
-
},
|
|
445
|
-
},
|
|
446
|
-
{
|
|
447
|
-
type: 'function',
|
|
448
|
-
function: {
|
|
449
|
-
name: 'delete_file',
|
|
450
|
-
description: 'Delete a file at the given path from the workspace.',
|
|
451
|
-
parameters: {
|
|
452
|
-
type: 'object',
|
|
453
|
-
properties: {
|
|
454
|
-
path: {
|
|
455
|
-
type: 'string',
|
|
456
|
-
description: 'The file path to delete, relative to the workspace root or absolute.',
|
|
457
|
-
},
|
|
458
|
-
},
|
|
459
|
-
required: ['path'],
|
|
460
|
-
},
|
|
461
|
-
},
|
|
462
|
-
},
|
|
463
|
-
{
|
|
464
|
-
type: 'function',
|
|
465
|
-
function: {
|
|
466
|
-
name: 'create_branch',
|
|
467
|
-
description: 'Create and checkout a new Git branch.',
|
|
468
|
-
parameters: {
|
|
469
|
-
type: 'object',
|
|
470
|
-
properties: {
|
|
471
|
-
branchName: { type: 'string', description: 'The name of the branch to create.' },
|
|
472
|
-
},
|
|
473
|
-
required: ['branchName'],
|
|
474
|
-
},
|
|
475
|
-
},
|
|
476
|
-
},
|
|
477
|
-
{
|
|
478
|
-
type: 'function',
|
|
479
|
-
function: {
|
|
480
|
-
name: 'commit_changes',
|
|
481
|
-
description: 'Stage all current changes and commit them.',
|
|
482
|
-
parameters: {
|
|
483
|
-
type: 'object',
|
|
484
|
-
properties: {
|
|
485
|
-
message: { type: 'string', description: 'The commit message.' },
|
|
486
|
-
},
|
|
487
|
-
required: ['message'],
|
|
488
|
-
},
|
|
489
|
-
},
|
|
490
|
-
},
|
|
491
|
-
{
|
|
492
|
-
type: 'function',
|
|
493
|
-
function: {
|
|
494
|
-
name: 'push_branch',
|
|
495
|
-
description: 'Push the current active branch to origin or custom remote.',
|
|
496
|
-
parameters: {
|
|
497
|
-
type: 'object',
|
|
498
|
-
properties: {
|
|
499
|
-
remote: { type: 'string', description: 'The remote repository name (default: origin).' },
|
|
500
|
-
},
|
|
501
|
-
required: [],
|
|
502
|
-
},
|
|
503
|
-
},
|
|
504
|
-
},
|
|
505
|
-
{
|
|
506
|
-
type: 'function',
|
|
507
|
-
function: {
|
|
508
|
-
name: 'create_pull_request',
|
|
509
|
-
description: 'Create a pull request on GitHub for the current branch.',
|
|
510
|
-
parameters: {
|
|
511
|
-
type: 'object',
|
|
512
|
-
properties: {
|
|
513
|
-
baseBranch: { type: 'string', description: 'The base branch to merge into (default: main).' },
|
|
514
|
-
},
|
|
515
|
-
required: [],
|
|
516
|
-
},
|
|
517
|
-
},
|
|
518
|
-
},
|
|
519
|
-
{
|
|
520
|
-
type: 'function',
|
|
521
|
-
function: {
|
|
522
|
-
name: 'lsp_goto_definition',
|
|
523
|
-
description: 'Find definition coordinates for a symbol at a given 0-indexed line and character position using LSP.',
|
|
524
|
-
parameters: {
|
|
525
|
-
type: 'object',
|
|
526
|
-
properties: {
|
|
527
|
-
path: { type: 'string', description: 'File path containing the symbol.' },
|
|
528
|
-
line: { type: 'integer', description: '0-indexed line number.' },
|
|
529
|
-
character: { type: 'integer', description: '0-indexed character offset.' },
|
|
530
|
-
},
|
|
531
|
-
required: ['path', 'line', 'character'],
|
|
532
|
-
},
|
|
533
|
-
},
|
|
534
|
-
},
|
|
535
|
-
{
|
|
536
|
-
type: 'function',
|
|
537
|
-
function: {
|
|
538
|
-
name: 'lsp_find_references',
|
|
539
|
-
description: 'Find all reference locations for a symbol at a given 0-indexed line and character position using LSP.',
|
|
540
|
-
parameters: {
|
|
541
|
-
type: 'object',
|
|
542
|
-
properties: {
|
|
543
|
-
path: { type: 'string', description: 'File path containing the symbol.' },
|
|
544
|
-
line: { type: 'integer', description: '0-indexed line number.' },
|
|
545
|
-
character: { type: 'integer', description: '0-indexed character offset.' },
|
|
546
|
-
},
|
|
547
|
-
required: ['path', 'line', 'character'],
|
|
548
|
-
},
|
|
549
|
-
},
|
|
550
|
-
},
|
|
551
|
-
{
|
|
552
|
-
type: 'function',
|
|
553
|
-
function: {
|
|
554
|
-
name: 'lsp_hover',
|
|
555
|
-
description: 'Retrieve type information and documentation for a symbol at a given 0-indexed line and character position using LSP.',
|
|
556
|
-
parameters: {
|
|
557
|
-
type: 'object',
|
|
558
|
-
properties: {
|
|
559
|
-
path: { type: 'string', description: 'File path containing the symbol.' },
|
|
560
|
-
line: { type: 'integer', description: '0-indexed line number.' },
|
|
561
|
-
character: { type: 'integer', description: '0-indexed character offset.' },
|
|
562
|
-
},
|
|
563
|
-
required: ['path', 'line', 'character'],
|
|
564
|
-
},
|
|
565
|
-
},
|
|
566
|
-
},
|
|
567
|
-
{
|
|
568
|
-
type: 'function',
|
|
569
|
-
function: {
|
|
570
|
-
name: 'web_fetch',
|
|
571
|
-
description: 'Fetch a webpage using an HTTP GET request and return its content converted to Markdown. Use this to read documentation or external references.',
|
|
572
|
-
parameters: {
|
|
573
|
-
type: 'object',
|
|
574
|
-
properties: {
|
|
575
|
-
url: { type: 'string', description: 'The absolute URL to fetch.' },
|
|
576
|
-
},
|
|
577
|
-
required: ['url'],
|
|
578
|
-
},
|
|
579
|
-
},
|
|
580
|
-
},
|
|
581
|
-
{
|
|
582
|
-
type: 'function',
|
|
583
|
-
function: {
|
|
584
|
-
name: 'web_search',
|
|
585
|
-
description: 'Perform a web search for a given query and return a list of search results as Markdown snippets. Use this to find information on the web.',
|
|
586
|
-
parameters: {
|
|
587
|
-
type: 'object',
|
|
588
|
-
properties: {
|
|
589
|
-
query: { type: 'string', description: 'The search query.' },
|
|
590
|
-
},
|
|
591
|
-
required: ['query'],
|
|
592
|
-
},
|
|
593
|
-
},
|
|
594
|
-
},
|
|
595
|
-
{
|
|
596
|
-
type: 'function',
|
|
597
|
-
function: {
|
|
598
|
-
name: 'str_replace',
|
|
599
|
-
description: 'Use this tool by default for any in-place edit to an existing file. Performs a surgical, atomic replacement of oldString with newString. By default, oldString must be unique within the file (expectUnique=true) — non-unique matches are rejected with a clear error so the caller can narrow the snippet. Pass replaceAll=true to substitute every occurrence. Rejected in PLAN mode. Refused for platform-locked paths. Goes through the same atomic staging pipeline as write_file and the LSP pre-save gate before any disk mutation. NEVER substitute this with shell commands like `sed -i`, `python3 -c`, or `cat > file` — those are sandbox-blocked.',
|
|
600
|
-
parameters: {
|
|
601
|
-
type: 'object',
|
|
602
|
-
properties: {
|
|
603
|
-
path: {
|
|
604
|
-
type: 'string',
|
|
605
|
-
description: 'The file path to edit, relative to the workspace root or absolute.',
|
|
606
|
-
},
|
|
607
|
-
oldString: {
|
|
608
|
-
type: 'string',
|
|
609
|
-
description: 'The exact substring to replace. Must appear in the file.',
|
|
610
|
-
},
|
|
611
|
-
newString: {
|
|
612
|
-
type: 'string',
|
|
613
|
-
description: 'The replacement content.',
|
|
614
|
-
},
|
|
615
|
-
replaceAll: {
|
|
616
|
-
type: 'boolean',
|
|
617
|
-
description: 'If true, replace every occurrence. Default false.',
|
|
618
|
-
},
|
|
619
|
-
expectUnique: {
|
|
620
|
-
type: 'boolean',
|
|
621
|
-
description: 'If true (default), the operation aborts when oldString is not unique. Set to false to allow non-unique matches with the first occurrence replaced.',
|
|
622
|
-
},
|
|
623
|
-
},
|
|
624
|
-
required: ['path', 'oldString', 'newString'],
|
|
625
|
-
},
|
|
626
|
-
},
|
|
627
|
-
},
|
|
628
|
-
{
|
|
629
|
-
type: 'function',
|
|
630
|
-
function: {
|
|
631
|
-
name: 'todo_read',
|
|
632
|
-
description: 'Read the current project todo list from <cwd>/.fixo/todo_list.json. Returns a human-readable rendering of all items grouped into Open and Completed. A missing or unreadable file yields an empty list — by design.',
|
|
633
|
-
parameters: {
|
|
634
|
-
type: 'object',
|
|
635
|
-
properties: {},
|
|
636
|
-
required: [],
|
|
637
|
-
},
|
|
638
|
-
},
|
|
639
|
-
},
|
|
640
|
-
{
|
|
641
|
-
type: 'function',
|
|
642
|
-
function: {
|
|
643
|
-
name: 'todo_write',
|
|
644
|
-
description: 'Mutate the project todo list. Operations: add (content+blockedBy optional), set_status (id+status), remove (id), clear_done. Persisted atomically to <cwd>/.fixo/todo_list.json. Rejected in PLAN mode.',
|
|
645
|
-
parameters: {
|
|
646
|
-
type: 'object',
|
|
647
|
-
properties: {
|
|
648
|
-
op: {
|
|
649
|
-
type: 'string',
|
|
650
|
-
enum: ['add', 'set_status', 'remove', 'clear_done'],
|
|
651
|
-
description: 'The mutation to apply.',
|
|
652
|
-
},
|
|
653
|
-
content: {
|
|
654
|
-
type: 'string',
|
|
655
|
-
description: 'Item content (op=add only).',
|
|
656
|
-
},
|
|
657
|
-
id: {
|
|
658
|
-
type: 'string',
|
|
659
|
-
description: 'Item id (op=set_status, op=remove).',
|
|
660
|
-
},
|
|
661
|
-
status: {
|
|
662
|
-
type: 'string',
|
|
663
|
-
enum: ['pending', 'in_progress', 'done', 'cancelled'],
|
|
664
|
-
description: 'New status (op=set_status only).',
|
|
665
|
-
},
|
|
666
|
-
blockedBy: {
|
|
667
|
-
type: 'string',
|
|
668
|
-
description: 'Optional blocker description (op=add only).',
|
|
669
|
-
},
|
|
670
|
-
},
|
|
671
|
-
required: ['op'],
|
|
672
|
-
},
|
|
673
|
-
},
|
|
674
|
-
},
|
|
675
|
-
{
|
|
676
|
-
type: 'function',
|
|
677
|
-
function: {
|
|
678
|
-
name: 'glob_files',
|
|
679
|
-
description: 'High-performance filesystem pattern matcher. Returns paths matching the given glob pattern, relative to the workspace root. Built on Node 22+ native fs.promises.glob. By default, common build/VCS directories (node_modules, .git, dist, .fixo, .fixocli) are excluded. Symlinks are not followed and hidden files are excluded unless explicitly enabled. Capped at maxResults (default 1000, hard cap 5000).',
|
|
680
|
-
parameters: {
|
|
681
|
-
type: 'object',
|
|
682
|
-
properties: {
|
|
683
|
-
pattern: {
|
|
684
|
-
type: 'string',
|
|
685
|
-
description: 'Glob pattern, e.g. "src/**/*.ts" or "**/package.json".',
|
|
686
|
-
},
|
|
687
|
-
cwd: {
|
|
688
|
-
type: 'string',
|
|
689
|
-
description: 'Optional directory to scope the glob. Must resolve inside the workspace. Defaults to the workspace root.',
|
|
690
|
-
},
|
|
691
|
-
ignore: {
|
|
692
|
-
type: 'string',
|
|
693
|
-
description: 'Optional extra glob pattern (or comma-separated patterns) to add to the default skip set.',
|
|
694
|
-
},
|
|
695
|
-
maxResults: {
|
|
696
|
-
type: 'integer',
|
|
697
|
-
description: 'Maximum number of results to return. Default 1000, hard cap 5000.',
|
|
698
|
-
},
|
|
699
|
-
includeHidden: {
|
|
700
|
-
type: 'boolean',
|
|
701
|
-
description: 'If true, do not exclude dotfile entries from the match. Default false.',
|
|
702
|
-
},
|
|
703
|
-
followSymlinks: {
|
|
704
|
-
type: 'boolean',
|
|
705
|
-
description: 'If true, follow symbolic links during traversal. Default false (safer).',
|
|
706
|
-
},
|
|
707
|
-
},
|
|
708
|
-
required: ['pattern'],
|
|
709
|
-
},
|
|
710
|
-
},
|
|
711
|
-
},
|
|
712
|
-
];
|
|
713
201
|
/* ──────────────────────── Per-process Run ID (Pillar 2) ──────────── */
|
|
714
202
|
let cachedRunId = null;
|
|
715
203
|
/**
|
|
@@ -720,8 +208,10 @@ let cachedRunId = null;
|
|
|
720
208
|
export function getOrCreateRunId() {
|
|
721
209
|
if (cachedRunId)
|
|
722
210
|
return cachedRunId;
|
|
723
|
-
|
|
724
|
-
|
|
211
|
+
// Use crypto.randomBytes for collision-free staging namespace IDs.
|
|
212
|
+
// Math.random() has a 1-in-2^30 collision chance; crypto.randomBytes
|
|
213
|
+
// is cryptographically secure and eliminates any collision risk.
|
|
214
|
+
cachedRunId = randomBytes(6).toString('hex') + Date.now().toString(36).slice(-6);
|
|
725
215
|
return cachedRunId;
|
|
726
216
|
}
|
|
727
217
|
/** Test/utility hook — reset the cached run id. */
|
|
@@ -902,6 +392,20 @@ export async function executeTool(name, args, cwd, verbose = false, options = {}
|
|
|
902
392
|
};
|
|
903
393
|
try {
|
|
904
394
|
const policy = options.policy ?? options.session?.policy ?? 'shell-confirm';
|
|
395
|
+
// Auto-init git repo on first mutating tool call (Finding C)
|
|
396
|
+
if (MUTATION_TOOL_NAMES.has(name)) {
|
|
397
|
+
const git = new GitManager(cwd);
|
|
398
|
+
if (!git.isGitRepo()) {
|
|
399
|
+
try {
|
|
400
|
+
spawnSync('git', ['init'], { cwd, encoding: 'utf-8', stdio: 'ignore' });
|
|
401
|
+
spawnSync('git', ['add', '.'], { cwd, encoding: 'utf-8', stdio: 'ignore' });
|
|
402
|
+
spawnSync('git', ['commit', '-m', 'chore: initial checkpoint by fixo'], { cwd, encoding: 'utf-8', stdio: 'ignore' });
|
|
403
|
+
}
|
|
404
|
+
catch (e) {
|
|
405
|
+
// Ignore if git fails (e.g. no user.name, empty directory, or git not installed)
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
905
409
|
// ──── PreToolUse hooks (§3.4) ────
|
|
906
410
|
// Run any user-defined pre-tool hooks. A `deny` decision
|
|
907
411
|
// short-circuits the call; a `modify` decision replaces
|
|
@@ -1083,19 +587,12 @@ export async function executeTool(name, args, cwd, verbose = false, options = {}
|
|
|
1083
587
|
}
|
|
1084
588
|
catch (err) {
|
|
1085
589
|
if (verbose) {
|
|
1086
|
-
console.error('Failed to run AST command safety check,
|
|
1087
|
-
}
|
|
1088
|
-
const trimmed = args.command.trim();
|
|
1089
|
-
const { DANGEROUS_COMMANDS } = await import('../runtime/policy.js');
|
|
1090
|
-
for (const pattern of DANGEROUS_COMMANDS) {
|
|
1091
|
-
if (pattern.test(trimmed)) {
|
|
1092
|
-
safetyResult = {
|
|
1093
|
-
safe: false,
|
|
1094
|
-
reason: `Regex security match: Command contains potentially unsafe pattern/metacharacter: ${pattern.toString()}`
|
|
1095
|
-
};
|
|
1096
|
-
break;
|
|
1097
|
-
}
|
|
590
|
+
console.error('Failed to run AST command safety check, failing closed for security:', err.message);
|
|
1098
591
|
}
|
|
592
|
+
safetyResult = {
|
|
593
|
+
safe: false,
|
|
594
|
+
reason: `Error: AST command safety check failed and regex fallback is disabled for security reasons: ${err.message}`
|
|
595
|
+
};
|
|
1099
596
|
}
|
|
1100
597
|
if (!safetyResult.safe) {
|
|
1101
598
|
const allowed = await askUnsafeCommandPermission(args.command, safetyResult.reason, options.allowWithoutPrompt);
|
|
@@ -1104,7 +601,7 @@ export async function executeTool(name, args, cwd, verbose = false, options = {}
|
|
|
1104
601
|
break;
|
|
1105
602
|
}
|
|
1106
603
|
}
|
|
1107
|
-
event.result = executeRunCommand(args.command, args.cwd || cwd, cwd, options.session);
|
|
604
|
+
event.result = executeRunCommand(args.command, args.cwd || cwd, cwd, options.session, options.safety?.sandboxMode);
|
|
1108
605
|
break;
|
|
1109
606
|
case 'search_code':
|
|
1110
607
|
setSpinner({ kind: 'search', name: 'Search', detail: `"${truncate(args.query, 40)}" in ${args.path ?? '.'}` });
|
|
@@ -1294,6 +791,16 @@ function executeReadFile(filePath, cwd, session, largeFileGateBytes = 15 * 1024,
|
|
|
1294
791
|
if (!fs.existsSync(resolved)) {
|
|
1295
792
|
return `Error: File not found: ${filePath}`;
|
|
1296
793
|
}
|
|
794
|
+
const baseName = path.basename(resolved).toLowerCase();
|
|
795
|
+
const lowerPath = resolved.toLowerCase();
|
|
796
|
+
if (baseName === '.env' ||
|
|
797
|
+
baseName.startsWith('.env.') ||
|
|
798
|
+
baseName === 'id_rsa' ||
|
|
799
|
+
baseName === 'providers.json' ||
|
|
800
|
+
lowerPath.includes('/.ssh/') ||
|
|
801
|
+
lowerPath.endsWith('.pem')) {
|
|
802
|
+
return `Error: Access to sensitive file "${filePath}" is blocked for security reasons.`;
|
|
803
|
+
}
|
|
1297
804
|
const stat = fs.statSync(resolved);
|
|
1298
805
|
if (stat.isDirectory()) {
|
|
1299
806
|
return `Error: "${filePath}" is a directory, not a file. Use list_dir instead.`;
|
|
@@ -1328,7 +835,7 @@ function executeReadFile(filePath, cwd, session, largeFileGateBytes = 15 * 1024,
|
|
|
1328
835
|
*/
|
|
1329
836
|
function countLines(filePath) {
|
|
1330
837
|
let count = 0;
|
|
1331
|
-
let
|
|
838
|
+
let lastCharWasNewline = true;
|
|
1332
839
|
const stream = fs.openSync(filePath, 'r');
|
|
1333
840
|
try {
|
|
1334
841
|
const buf = Buffer.allocUnsafe(64 * 1024);
|
|
@@ -1337,14 +844,14 @@ function countLines(filePath) {
|
|
|
1337
844
|
for (let i = 0; i < bytesRead; i++) {
|
|
1338
845
|
if (buf[i] === 0x0a) {
|
|
1339
846
|
count++;
|
|
1340
|
-
|
|
847
|
+
lastCharWasNewline = true;
|
|
1341
848
|
}
|
|
1342
849
|
else {
|
|
1343
|
-
|
|
850
|
+
lastCharWasNewline = false;
|
|
1344
851
|
}
|
|
1345
852
|
}
|
|
1346
853
|
}
|
|
1347
|
-
if (!
|
|
854
|
+
if (!lastCharWasNewline)
|
|
1348
855
|
count++;
|
|
1349
856
|
}
|
|
1350
857
|
finally {
|
|
@@ -1419,6 +926,16 @@ async function executeExtractImports(filePath, cwd, session) {
|
|
|
1419
926
|
function executeWriteFile(filePath, content, cwd, options = {}) {
|
|
1420
927
|
const guard = new WorkspaceGuard(cwd);
|
|
1421
928
|
const resolved = guard.resolve(filePath, 'file');
|
|
929
|
+
const baseName = path.basename(resolved).toLowerCase();
|
|
930
|
+
const lowerPath = resolved.toLowerCase();
|
|
931
|
+
if (baseName === '.env' ||
|
|
932
|
+
baseName.startsWith('.env.') ||
|
|
933
|
+
baseName === 'id_rsa' ||
|
|
934
|
+
baseName === 'providers.json' ||
|
|
935
|
+
lowerPath.includes('/.ssh/') ||
|
|
936
|
+
lowerPath.endsWith('.pem')) {
|
|
937
|
+
return Promise.resolve(`Error: Access to sensitive file "${filePath}" is blocked for security reasons.`);
|
|
938
|
+
}
|
|
1422
939
|
// Pillar 5 / Protection 1 — refuse to mutate the platform's
|
|
1423
940
|
// own runtime. This is the guard that prevents an autonomous
|
|
1424
941
|
// agent from corrupting `src/agent/tool-executor.ts` and
|
|
@@ -1466,27 +983,53 @@ function executeWriteFile(filePath, content, cwd, options = {}) {
|
|
|
1466
983
|
return `File updated: ${filePath}`;
|
|
1467
984
|
});
|
|
1468
985
|
}
|
|
1469
|
-
function executeRunCommand(command, requestedCwd, workspaceRoot, session) {
|
|
986
|
+
function executeRunCommand(command, requestedCwd, workspaceRoot, session, sandboxMode) {
|
|
1470
987
|
const guard = new WorkspaceGuard(workspaceRoot);
|
|
1471
988
|
const commandCwd = guard.resolve(requestedCwd, 'command cwd');
|
|
1472
989
|
try {
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
990
|
+
let result;
|
|
991
|
+
if (sandboxMode === 'os-sandbox') {
|
|
992
|
+
// Opt-in OS-level sandbox. The regex command-parser layer that
|
|
993
|
+
// ran upstream is left in place — this is defence in depth.
|
|
994
|
+
// If the platform binary is missing we surface a structured
|
|
995
|
+
// error instead of silently downgrading to unsandboxed exec.
|
|
996
|
+
try {
|
|
997
|
+
result = runSandboxed(command, {
|
|
998
|
+
cwd: commandCwd,
|
|
999
|
+
allowedWritePaths: [workspaceRoot],
|
|
1000
|
+
allowNetwork: true,
|
|
1001
|
+
timeout: 60_000,
|
|
1002
|
+
maxBuffer: 1024 * 1024,
|
|
1003
|
+
env: redactedEnv(),
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
catch (sandboxErr) {
|
|
1007
|
+
if (sandboxErr instanceof SandboxUnavailableError) {
|
|
1008
|
+
return `Error: OS sandbox mode is enabled but cannot be applied — ${sandboxErr.message}. Either install the platform binary or set preferences.safety.sandboxMode to 'guard'.`;
|
|
1009
|
+
}
|
|
1010
|
+
throw sandboxErr;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
else {
|
|
1014
|
+
result = spawnSync(command, {
|
|
1015
|
+
shell: true,
|
|
1016
|
+
cwd: commandCwd,
|
|
1017
|
+
encoding: 'utf-8',
|
|
1018
|
+
timeout: 60_000, // 60 second timeout
|
|
1019
|
+
maxBuffer: 1024 * 1024, // 1MB max output
|
|
1020
|
+
env: redactedEnv(),
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1481
1023
|
const output = redactSecrets([result.stdout ?? '', result.stderr ?? ''].filter(Boolean).join('\n'));
|
|
1482
1024
|
const status = result.status ?? 0;
|
|
1483
1025
|
session?.record('command_finished', { command, cwd: guard.relative(commandCwd), status, output: truncate(output, 4000) });
|
|
1484
1026
|
return output || `(command completed with code ${status})`;
|
|
1485
1027
|
}
|
|
1486
1028
|
catch (error) {
|
|
1487
|
-
const
|
|
1488
|
-
const
|
|
1489
|
-
const
|
|
1029
|
+
const err = error;
|
|
1030
|
+
const stdout = err.stdout ?? '';
|
|
1031
|
+
const stderr = err.stderr ?? '';
|
|
1032
|
+
const code = err.status ?? 'unknown';
|
|
1490
1033
|
return redactSecrets(`Command exited with code ${code}\n\nSTDOUT:\n${stdout}\n\nSTDERR:\n${stderr}`.trim());
|
|
1491
1034
|
}
|
|
1492
1035
|
}
|
|
@@ -1503,7 +1046,8 @@ function executeSearchCode(query, searchPath, filePattern, cwd) {
|
|
|
1503
1046
|
}
|
|
1504
1047
|
catch (error) {
|
|
1505
1048
|
if (process.env.DEBUG || process.env.VERBOSE || process.argv.includes('--verbose')) {
|
|
1506
|
-
|
|
1049
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1050
|
+
console.warn(`[Debug Warning] Failed to determine if ripgrep (rg) is installed: ${msg}`);
|
|
1507
1051
|
}
|
|
1508
1052
|
}
|
|
1509
1053
|
let output = '';
|
|
@@ -1560,7 +1104,8 @@ function executeListDir(dirPath, cwd) {
|
|
|
1560
1104
|
}
|
|
1561
1105
|
let entries;
|
|
1562
1106
|
try {
|
|
1563
|
-
|
|
1107
|
+
const inv = getRunInventory(getOrCreateRunId());
|
|
1108
|
+
entries = inv.listDir(resolved);
|
|
1564
1109
|
}
|
|
1565
1110
|
catch (error) {
|
|
1566
1111
|
return `Error: Cannot read directory: ${error instanceof Error ? error.message : String(error)}`;
|
|
@@ -1575,6 +1120,7 @@ function executeListDir(dirPath, cwd) {
|
|
|
1575
1120
|
return a.name.localeCompare(b.name);
|
|
1576
1121
|
});
|
|
1577
1122
|
const lines = [];
|
|
1123
|
+
const inv = getRunInventory(getOrCreateRunId());
|
|
1578
1124
|
for (const entry of filtered) {
|
|
1579
1125
|
if (entry.isDirectory()) {
|
|
1580
1126
|
lines.push(`📁 ${entry.name}/`);
|
|
@@ -1582,7 +1128,7 @@ function executeListDir(dirPath, cwd) {
|
|
|
1582
1128
|
else {
|
|
1583
1129
|
let size = '';
|
|
1584
1130
|
try {
|
|
1585
|
-
const s =
|
|
1131
|
+
const s = inv.fileStats(path.join(resolved, entry.name));
|
|
1586
1132
|
size = formatSize(s.size);
|
|
1587
1133
|
}
|
|
1588
1134
|
catch {
|
|
@@ -1898,7 +1444,7 @@ export async function executeTodoWrite(args, cwd, options = {}) {
|
|
|
1898
1444
|
* dispatch is hot-path-friendly and test-friendly (tests inject
|
|
1899
1445
|
* a custom registry via {@link setBackgroundJobRegistry}).
|
|
1900
1446
|
*/
|
|
1901
|
-
|
|
1447
|
+
const globalBackgroundRegistry = null;
|
|
1902
1448
|
const backgroundRegistries = new Map();
|
|
1903
1449
|
export function getBackgroundJobRegistry(cwd) {
|
|
1904
1450
|
let reg = backgroundRegistries.get(cwd);
|