vigthoria-cli 1.13.20 → 1.13.23
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/completions/_vigthoria +49 -0
- package/completions/vigthoria.bash +6 -0
- package/completions/vigthoria.fish +41 -0
- package/dist/commands/auth.d.ts +1 -0
- package/dist/commands/auth.js +63 -128
- package/dist/commands/background.d.ts +0 -2
- package/dist/commands/background.js +26 -34
- package/dist/commands/bridge.js +45 -41
- package/dist/commands/cancel.d.ts +1 -0
- package/dist/commands/cancel.js +22 -16
- package/dist/commands/chat.d.ts +3 -42
- package/dist/commands/chat.js +378 -1123
- package/dist/commands/coding-registration.d.ts +4 -0
- package/dist/commands/coding-registration.js +24 -0
- package/dist/commands/config.js +23 -29
- package/dist/commands/conversation-registration.d.ts +4 -0
- package/dist/commands/conversation-registration.js +122 -0
- package/dist/commands/deploy.d.ts +8 -2
- package/dist/commands/deploy.js +92 -90
- package/dist/commands/device.d.ts +1 -2
- package/dist/commands/device.js +37 -21
- package/dist/commands/doctor-registration.d.ts +4 -0
- package/dist/commands/doctor-registration.js +207 -0
- package/dist/commands/edit.js +22 -33
- package/dist/commands/explain.js +7 -10
- package/dist/commands/fork.d.ts +1 -2
- package/dist/commands/fork.js +72 -22
- package/dist/commands/game.d.ts +1 -1
- package/dist/commands/game.js +17 -6
- package/dist/commands/generate.js +7 -9
- package/dist/commands/history.js +11 -7
- package/dist/commands/hub.d.ts +2 -2
- package/dist/commands/hub.js +34 -35
- package/dist/commands/legion.d.ts +0 -1
- package/dist/commands/legion.js +62 -79
- package/dist/commands/music.d.ts +1 -2
- package/dist/commands/music.js +62 -27
- package/dist/commands/platform-registration.d.ts +4 -0
- package/dist/commands/platform-registration.js +45 -0
- package/dist/commands/preview.d.ts +0 -1
- package/dist/commands/preview.js +82 -35
- package/dist/commands/product-run-registration.d.ts +4 -0
- package/dist/commands/product-run-registration.js +490 -0
- package/dist/commands/replay.d.ts +1 -2
- package/dist/commands/replay.js +15 -11
- package/dist/commands/repo.d.ts +14 -7
- package/dist/commands/repo.js +244 -198
- package/dist/commands/review.js +9 -12
- package/dist/commands/security.d.ts +1 -2
- package/dist/commands/security.js +12 -6
- package/dist/commands/update-registration.d.ts +3 -0
- package/dist/commands/update-registration.js +368 -0
- package/dist/commands/v4-registration.d.ts +4 -0
- package/dist/commands/v4-registration.js +24 -0
- package/dist/commands/v4.d.ts +1 -1
- package/dist/commands/v4.js +46 -8
- package/dist/commands/wallet.d.ts +1 -3
- package/dist/commands/wallet.js +18 -45
- package/dist/commands/workflow.d.ts +2 -0
- package/dist/commands/workflow.js +52 -39
- package/dist/index.js +124 -1589
- package/dist/utils/agent-stream-state.d.ts +27 -0
- package/dist/utils/agent-stream-state.js +93 -0
- package/dist/utils/agentRoute.d.ts +2 -1
- package/dist/utils/agentRoute.js +22 -9
- package/dist/utils/agentRunOutcome.d.ts +16 -0
- package/dist/utils/agentRunOutcome.js +37 -2
- package/dist/utils/api-client-factory.d.ts +8 -0
- package/dist/utils/api-client-factory.js +8 -0
- package/dist/utils/api.d.ts +44 -266
- package/dist/utils/api.js +647 -3180
- package/dist/utils/auth-session.d.ts +41 -0
- package/dist/utils/auth-session.js +224 -0
- package/dist/utils/brain-hub-client.d.ts +0 -4
- package/dist/utils/brain-hub-client.js +6 -3
- package/dist/utils/bridge-client.js +2 -0
- package/dist/utils/capability-health-service.d.ts +48 -0
- package/dist/utils/capability-health-service.js +64 -0
- package/dist/utils/chat-prompt-policy.d.ts +18 -0
- package/dist/utils/chat-prompt-policy.js +61 -0
- package/dist/utils/chat-tool-call-parser.d.ts +11 -0
- package/dist/utils/chat-tool-call-parser.js +71 -0
- package/dist/utils/cli-state.d.ts +22 -4
- package/dist/utils/cli-state.js +105 -47
- package/dist/utils/code-operations-service.d.ts +104 -0
- package/dist/utils/code-operations-service.js +837 -0
- package/dist/utils/codebase-indexer.d.ts +1 -0
- package/dist/utils/codebase-indexer.js +28 -8
- package/dist/utils/command-contract.d.ts +61 -0
- package/dist/utils/command-contract.js +149 -0
- package/dist/utils/command-policy.d.ts +10 -0
- package/dist/utils/command-policy.js +54 -0
- package/dist/utils/config.d.ts +13 -5
- package/dist/utils/config.js +220 -94
- package/dist/utils/context-ranker.js +11 -2
- package/dist/utils/context-state.d.ts +39 -0
- package/dist/utils/context-state.js +52 -0
- package/dist/utils/deckEvents.d.ts +0 -5
- package/dist/utils/deckEvents.js +3 -2
- package/dist/utils/desktop-bridge-client.js +2 -0
- package/dist/utils/direct-output-policy.d.ts +3 -0
- package/dist/utils/direct-output-policy.js +76 -0
- package/dist/utils/durable-json.d.ts +57 -0
- package/dist/utils/durable-json.js +319 -0
- package/dist/utils/fastAgentRouter.d.ts +5 -6
- package/dist/utils/fastAgentRouter.js +17 -25
- package/dist/utils/files.js +21 -13
- package/dist/utils/frontend-preview-service.d.ts +76 -0
- package/dist/utils/frontend-preview-service.js +482 -0
- package/dist/utils/goaEvents.d.ts +0 -4
- package/dist/utils/goaEvents.js +5 -2
- package/dist/utils/logger.d.ts +2 -1
- package/dist/utils/logger.js +27 -20
- package/dist/utils/mcp-context-client.d.ts +24 -0
- package/dist/utils/mcp-context-client.js +58 -0
- package/dist/utils/model-governance.d.ts +18 -0
- package/dist/utils/model-governance.js +75 -0
- package/dist/utils/model-transport-service.d.ts +44 -0
- package/dist/utils/model-transport-service.js +111 -0
- package/dist/utils/mutation-journal.d.ts +42 -0
- package/dist/utils/mutation-journal.js +387 -0
- package/dist/utils/network-policy.d.ts +24 -0
- package/dist/utils/network-policy.js +202 -0
- package/dist/utils/operator-client.d.ts +45 -0
- package/dist/utils/operator-client.js +159 -0
- package/dist/utils/post-write-validator.js +7 -9
- package/dist/utils/preview-screenshot-adapter.d.ts +15 -0
- package/dist/utils/preview-screenshot-adapter.js +51 -0
- package/dist/utils/process-policy.d.ts +23 -0
- package/dist/utils/process-policy.js +284 -0
- package/dist/utils/project-memory.d.ts +13 -3
- package/dist/utils/project-memory.js +70 -17
- package/dist/utils/prompt-workspace-resolver.d.ts +14 -0
- package/dist/utils/prompt-workspace-resolver.js +64 -0
- package/dist/utils/release-install.d.ts +11 -0
- package/dist/utils/release-install.js +155 -0
- package/dist/utils/release-policy.d.ts +64 -0
- package/dist/utils/release-policy.js +293 -0
- package/dist/utils/requestIntent.js +22 -3
- package/dist/utils/runtime-capability.d.ts +8 -0
- package/dist/utils/runtime-capability.js +16 -0
- package/dist/utils/runtime-temp.d.ts +76 -0
- package/dist/utils/runtime-temp.js +492 -0
- package/dist/utils/secret-policy.d.ts +20 -0
- package/dist/utils/secret-policy.js +160 -0
- package/dist/utils/session.d.ts +17 -3
- package/dist/utils/session.js +354 -110
- package/dist/utils/tool-approval-service.d.ts +17 -0
- package/dist/utils/tool-approval-service.js +78 -0
- package/dist/utils/tool-capability-providers.d.ts +57 -0
- package/dist/utils/tool-capability-providers.js +57 -0
- package/dist/utils/tools.d.ts +2 -23
- package/dist/utils/tools.js +226 -512
- package/dist/utils/update-policy.d.ts +18 -0
- package/dist/utils/update-policy.js +137 -0
- package/dist/utils/v3-agent-client.d.ts +40 -0
- package/dist/utils/v3-agent-client.js +165 -0
- package/dist/utils/v3-workspace-service.d.ts +28 -0
- package/dist/utils/v3-workspace-service.js +279 -0
- package/dist/utils/vigflow-client.d.ts +95 -0
- package/dist/utils/vigflow-client.js +105 -0
- package/dist/utils/workspace-boundary.d.ts +45 -0
- package/dist/utils/workspace-boundary.js +377 -0
- package/dist/utils/workspace-brain-service.d.ts +2 -0
- package/dist/utils/workspace-brain-service.js +11 -1
- package/dist/utils/workspace-cache.js +37 -15
- package/dist/utils/workspace-stream.js +35 -10
- package/install.ps1 +115 -142
- package/install.sh +112 -81
- package/package.json +43 -9
- package/release-policy.json +43 -0
- package/scripts/release/LOCAL_MACHINE_USER_VERIFICATION.md +1 -1
- package/scripts/release/generate-completions.mjs +24 -0
- package/scripts/release/install-release.mjs +10 -0
- package/scripts/release/publish-cli-release.mjs +116 -0
- package/scripts/release/publish-cli-release.sh +2 -70
- package/scripts/release/resolve-release-manifest.mjs +26 -0
- package/scripts/release/test-balanced-model-live.sh +45 -0
- package/scripts/release/validate-live-service-gates.sh +93 -0
- package/scripts/release/validate-no-go-gates.sh +145 -174
- package/scripts/release/verify-runtime-consistency.mjs +4 -1
- package/dist/commands/index.d.ts +0 -12
- package/dist/commands/index.js +0 -194
- package/dist/commands/update.d.ts +0 -9
- package/dist/commands/update.js +0 -301
- package/dist/utils/templateInstantPath.d.ts +0 -18
- package/dist/utils/templateInstantPath.js +0 -228
package/dist/utils/tools.js
CHANGED
|
@@ -15,18 +15,23 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import * as fs from 'fs';
|
|
17
17
|
import * as path from 'path';
|
|
18
|
-
import {
|
|
18
|
+
import { execFileSync, spawn } from 'child_process';
|
|
19
19
|
import { createRequire } from 'node:module';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
20
21
|
import chalk from 'chalk';
|
|
21
22
|
// ESM shim — re-create CommonJS-style `require()` so the inline
|
|
22
23
|
// `require('os')` / `require('minimatch')` / `require('glob')` calls
|
|
23
24
|
// inside lazy code paths continue to work unchanged in ESM mode.
|
|
24
25
|
const require = createRequire(import.meta.url);
|
|
25
26
|
import { CH } from './logger.js';
|
|
26
|
-
import {
|
|
27
|
+
import { assertContainedAbsolutePath } from './workspace-boundary.js';
|
|
28
|
+
import { assertProcessNetworkAllowed, buildApprovalScope, hardenGitArguments, parseGitArguments, parseSupportedProcess, safeGitEnvironment, } from './process-policy.js';
|
|
29
|
+
import { containsHighConfidenceSecret, isSensitivePath, safeChildProcessEnv } from './secret-policy.js';
|
|
30
|
+
import { isOfflineNetworkMode } from './network-policy.js';
|
|
31
|
+
import { createRuntimeTempDirectory, removeRuntimeTempDirectory } from './runtime-temp.js';
|
|
32
|
+
import { ToolApprovalService } from './tool-approval-service.js';
|
|
33
|
+
import { DelegationToolProvider, FilesystemToolProvider, NetworkBrowserToolProvider, ProcessToolProvider, RepositoryToolProvider, SearchToolProvider, SshToolProvider, ToolCapabilityRegistry, } from './tool-capability-providers.js';
|
|
27
34
|
const STREAM_RESPONSE_MAX_YIELD_CHARS = 32 * 1024;
|
|
28
|
-
const POWERSHELL_SAFE_PATH_PATTERN = /^[A-Za-z0-9_:\\/.\-\s]+$/;
|
|
29
|
-
const POWERSHELL_SAFE_INCLUDE_PATTERN = /^[A-Za-z0-9_*?.\-]+$/;
|
|
30
35
|
const SSH_SAFE_HOST_PATTERN = /^[A-Za-z0-9.-]+$/;
|
|
31
36
|
function getSshAllowedHosts() {
|
|
32
37
|
const configured = String(process.env.VIGTHORIA_SSH_ALLOWED_HOSTS || '')
|
|
@@ -58,6 +63,9 @@ function resolveWindowsInstallerPath() {
|
|
|
58
63
|
export async function installUpdateWindows() {
|
|
59
64
|
const installerPath = resolveWindowsInstallerPath();
|
|
60
65
|
try {
|
|
66
|
+
if (isOfflineNetworkMode()) {
|
|
67
|
+
return { success: false, platform: 'windows', error: 'Offline mode prohibits launching the update installer' };
|
|
68
|
+
}
|
|
61
69
|
if (!fs.existsSync(installerPath)) {
|
|
62
70
|
return { success: false, platform: 'windows', error: 'ENOENT' };
|
|
63
71
|
}
|
|
@@ -68,9 +76,18 @@ export async function installUpdateWindows() {
|
|
|
68
76
|
detached: true,
|
|
69
77
|
stdio: 'ignore',
|
|
70
78
|
windowsHide: false,
|
|
79
|
+
env: safeChildProcessEnv(),
|
|
80
|
+
});
|
|
81
|
+
const timer = setTimeout(() => {
|
|
82
|
+
child.kill();
|
|
83
|
+
reject(new Error('Windows update installer did not start within 10 seconds'));
|
|
84
|
+
}, 10_000);
|
|
85
|
+
child.once('error', (error) => {
|
|
86
|
+
clearTimeout(timer);
|
|
87
|
+
reject(error);
|
|
71
88
|
});
|
|
72
|
-
child.once('error', reject);
|
|
73
89
|
child.once('spawn', () => {
|
|
90
|
+
clearTimeout(timer);
|
|
74
91
|
child.unref();
|
|
75
92
|
resolve();
|
|
76
93
|
});
|
|
@@ -89,7 +106,6 @@ export async function* robustifyStreamResponse(res) {
|
|
|
89
106
|
const MAX_YIELD_CHARS = STREAM_RESPONSE_MAX_YIELD_CHARS;
|
|
90
107
|
const MAX_BUFFER_CHARS = 256 * 1024;
|
|
91
108
|
const body = res?.body ?? res;
|
|
92
|
-
const decoder = new TextDecoder('utf-8');
|
|
93
109
|
let reader = null;
|
|
94
110
|
let lineBuffer = '';
|
|
95
111
|
const emitContent = async function* (type, content) {
|
|
@@ -330,6 +346,8 @@ export class AgenticTools {
|
|
|
330
346
|
cwd;
|
|
331
347
|
permissionCallback;
|
|
332
348
|
autoApprove;
|
|
349
|
+
approvals;
|
|
350
|
+
capabilityRegistry;
|
|
333
351
|
undoStack = [];
|
|
334
352
|
maxUndoStack = 50;
|
|
335
353
|
retryConfig = {
|
|
@@ -353,17 +371,6 @@ export class AgenticTools {
|
|
|
353
371
|
}
|
|
354
372
|
return `External tool call failed in ${toolName} during ${operation}: ${String(error)}`;
|
|
355
373
|
}
|
|
356
|
-
externalToolFailure(toolName, operation, error, suggestion) {
|
|
357
|
-
const message = this.formatExternalToolError(toolName, operation, error);
|
|
358
|
-
this.logger.debug(message);
|
|
359
|
-
this.logger.warn(message);
|
|
360
|
-
return {
|
|
361
|
-
success: false,
|
|
362
|
-
error: message,
|
|
363
|
-
suggestion: suggestion || 'Review the command, arguments, permissions, and environment, then retry.',
|
|
364
|
-
canRetry: true,
|
|
365
|
-
};
|
|
366
|
-
}
|
|
367
374
|
cleanupAfterToolError(toolName, operation, cleanup) {
|
|
368
375
|
try {
|
|
369
376
|
cleanup();
|
|
@@ -374,29 +381,6 @@ export class AgenticTools {
|
|
|
374
381
|
this.logger.warn(message);
|
|
375
382
|
}
|
|
376
383
|
}
|
|
377
|
-
runExternalCommand(toolName, operation, command, options, cleanup) {
|
|
378
|
-
if (!this.isNonEmptyString(toolName)) {
|
|
379
|
-
throw new Error('Invalid tool execution: toolName must be a non-empty string.');
|
|
380
|
-
}
|
|
381
|
-
if (!this.isNonEmptyString(operation)) {
|
|
382
|
-
throw new Error(`Invalid external command for ${toolName}: operation must be a non-empty string.`);
|
|
383
|
-
}
|
|
384
|
-
if (!this.isNonEmptyString(command)) {
|
|
385
|
-
throw new Error(`Invalid external command for ${toolName} during ${operation}: command must be a non-empty string.`);
|
|
386
|
-
}
|
|
387
|
-
try {
|
|
388
|
-
return execSync(command, options);
|
|
389
|
-
}
|
|
390
|
-
catch (error) {
|
|
391
|
-
if (cleanup) {
|
|
392
|
-
this.cleanupAfterToolError(toolName, operation, cleanup);
|
|
393
|
-
}
|
|
394
|
-
const message = this.formatExternalToolError(toolName, operation, error);
|
|
395
|
-
const wrapped = new Error(message);
|
|
396
|
-
wrapped.cause = error;
|
|
397
|
-
throw wrapped;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
384
|
isNonEmptyString(value) {
|
|
401
385
|
return typeof value === 'string' && value.trim().length > 0;
|
|
402
386
|
}
|
|
@@ -422,19 +406,6 @@ export class AgenticTools {
|
|
|
422
406
|
}
|
|
423
407
|
}
|
|
424
408
|
}
|
|
425
|
-
requireNonEmptyString(value, fieldName, toolName) {
|
|
426
|
-
if (!this.isNonEmptyString(value)) {
|
|
427
|
-
throw new Error(`Invalid arguments for ${toolName}: ${fieldName} is required and must be a non-empty string.`);
|
|
428
|
-
}
|
|
429
|
-
return value;
|
|
430
|
-
}
|
|
431
|
-
requireArgsObject(args, toolName) {
|
|
432
|
-
this.assertStringRecord(args, `Invalid arguments for ${toolName}: args`);
|
|
433
|
-
}
|
|
434
|
-
// Session-based tool approvals - remembers which tools user approved for this turn
|
|
435
|
-
sessionApprovedTools = new Set();
|
|
436
|
-
// Persistent permissions - tool allowlists per project
|
|
437
|
-
static permissionsFile = path.join(process.env.HOME || process.env.USERPROFILE || '~', '.vigthoria', 'permissions.json');
|
|
438
409
|
constructor(logger, cwd, permissionCallback, autoApprove = false) {
|
|
439
410
|
if (!logger) {
|
|
440
411
|
throw new Error('AgenticTools initialization failed: logger is required.');
|
|
@@ -449,9 +420,23 @@ export class AgenticTools {
|
|
|
449
420
|
throw new Error('AgenticTools initialization failed: autoApprove must be a boolean.');
|
|
450
421
|
}
|
|
451
422
|
this.logger = logger;
|
|
452
|
-
this.cwd = cwd;
|
|
423
|
+
this.cwd = fs.realpathSync(path.resolve(cwd));
|
|
453
424
|
this.permissionCallback = permissionCallback;
|
|
454
425
|
this.autoApprove = autoApprove;
|
|
426
|
+
this.approvals = new ToolApprovalService(this.cwd, logger);
|
|
427
|
+
this.capabilityRegistry = new ToolCapabilityRegistry([
|
|
428
|
+
new FilesystemToolProvider({
|
|
429
|
+
read_file: (args) => this.readFile(args), write_file: (args) => this.writeFile(args),
|
|
430
|
+
edit_file: (args) => this.editFile(args), list_dir: (args) => this.listDir(args),
|
|
431
|
+
glob: (args) => this.glob(args), multi_edit: (args) => this.multiEdit(args),
|
|
432
|
+
}),
|
|
433
|
+
new ProcessToolProvider({ bash: (args) => this.bash(args) }),
|
|
434
|
+
new SearchToolProvider({ grep: (args) => this.grep(args), codebase_search: (args) => this.codebaseSearch(args) }),
|
|
435
|
+
new RepositoryToolProvider({ git: (args) => this.git(args), repo: (args) => this.repo(args) }),
|
|
436
|
+
new NetworkBrowserToolProvider({ fetch_url: (args) => this.fetchUrl(args), browser: (args) => this.browserTool(args) }),
|
|
437
|
+
new SshToolProvider({ ssh_exec: (args) => this.sshExec(args) }),
|
|
438
|
+
new DelegationToolProvider({ task: (args) => this.task(args) }),
|
|
439
|
+
]);
|
|
455
440
|
}
|
|
456
441
|
getErrorMessage(error) {
|
|
457
442
|
if (error instanceof Error) {
|
|
@@ -467,85 +452,17 @@ export class AgenticTools {
|
|
|
467
452
|
return String(error);
|
|
468
453
|
}
|
|
469
454
|
}
|
|
470
|
-
assertToolCall(call) {
|
|
471
|
-
if (!call || typeof call !== 'object') {
|
|
472
|
-
throw new Error('Invalid tool call: call must be an object.');
|
|
473
|
-
}
|
|
474
|
-
if (typeof call.tool !== 'string' || call.tool.trim().length === 0) {
|
|
475
|
-
throw new Error('Invalid tool call: tool name must be a non-empty string.');
|
|
476
|
-
}
|
|
477
|
-
if (!call.args || typeof call.args !== 'object' || Array.isArray(call.args)) {
|
|
478
|
-
throw new Error(`Invalid tool call for ${call.tool}: args must be an object.`);
|
|
479
|
-
}
|
|
480
|
-
for (const [key, value] of Object.entries(call.args)) {
|
|
481
|
-
if (typeof key !== 'string' || key.trim().length === 0) {
|
|
482
|
-
throw new Error(`Invalid tool call for ${call.tool}: argument names must be non-empty strings.`);
|
|
483
|
-
}
|
|
484
|
-
if (typeof value !== 'string') {
|
|
485
|
-
throw new Error(`Invalid argument "${key}" for ${call.tool}: expected string, received ${typeof value}.`);
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
/**
|
|
490
|
-
* Load persistent permissions for the current project
|
|
491
|
-
*/
|
|
492
|
-
loadPersistentPermissions() {
|
|
493
|
-
try {
|
|
494
|
-
if (fs.existsSync(AgenticTools.permissionsFile)) {
|
|
495
|
-
return JSON.parse(fs.readFileSync(AgenticTools.permissionsFile, 'utf-8'));
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
catch (error) {
|
|
499
|
-
const message = this.formatExternalToolError('permissions', `read ${AgenticTools.permissionsFile}`, error);
|
|
500
|
-
this.logger.debug(message);
|
|
501
|
-
this.logger.warn(message);
|
|
502
|
-
}
|
|
503
|
-
return {};
|
|
504
|
-
}
|
|
505
|
-
/**
|
|
506
|
-
* Save a persistent permission for a tool in the current project
|
|
507
|
-
*/
|
|
508
|
-
savePersistentPermission(toolName) {
|
|
509
|
-
const permissions = this.loadPersistentPermissions();
|
|
510
|
-
const projectKey = this.cwd;
|
|
511
|
-
if (!permissions[projectKey]) {
|
|
512
|
-
permissions[projectKey] = { tools: [], updatedAt: new Date().toISOString() };
|
|
513
|
-
}
|
|
514
|
-
if (!permissions[projectKey].tools.includes(toolName)) {
|
|
515
|
-
permissions[projectKey].tools.push(toolName);
|
|
516
|
-
permissions[projectKey].updatedAt = new Date().toISOString();
|
|
517
|
-
}
|
|
518
|
-
try {
|
|
519
|
-
const dir = path.dirname(AgenticTools.permissionsFile);
|
|
520
|
-
if (!fs.existsSync(dir))
|
|
521
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
522
|
-
fs.writeFileSync(AgenticTools.permissionsFile, JSON.stringify(permissions, null, 2), 'utf-8');
|
|
523
|
-
}
|
|
524
|
-
catch (error) {
|
|
525
|
-
const message = this.formatExternalToolError('permissions', `write ${AgenticTools.permissionsFile}`, error);
|
|
526
|
-
this.logger.debug(message);
|
|
527
|
-
this.logger.warn(message);
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
/**
|
|
531
|
-
* Check if a tool has persistent permission for the current project
|
|
532
|
-
*/
|
|
533
|
-
hasPersistentPermission(toolName) {
|
|
534
|
-
const permissions = this.loadPersistentPermissions();
|
|
535
|
-
const projectKey = this.cwd;
|
|
536
|
-
return permissions[projectKey]?.tools?.includes(toolName) || false;
|
|
537
|
-
}
|
|
538
455
|
/**
|
|
539
456
|
* Clear session-approved tools (call this at the start of each new AI turn)
|
|
540
457
|
*/
|
|
541
458
|
clearSessionApprovals() {
|
|
542
|
-
this.
|
|
459
|
+
this.approvals.clearSession();
|
|
543
460
|
}
|
|
544
461
|
/**
|
|
545
462
|
* Get currently approved tools for this session
|
|
546
463
|
*/
|
|
547
464
|
getSessionApprovedTools() {
|
|
548
|
-
return
|
|
465
|
+
return this.approvals.sessionKeys();
|
|
549
466
|
}
|
|
550
467
|
/**
|
|
551
468
|
* Get the undo stack for inspection
|
|
@@ -647,7 +564,7 @@ export class AgenticTools {
|
|
|
647
564
|
},
|
|
648
565
|
{
|
|
649
566
|
name: 'bash',
|
|
650
|
-
description: 'Run a shell
|
|
567
|
+
description: 'Run one supported process using a parsed argument vector (no shell syntax)',
|
|
651
568
|
parameters: [
|
|
652
569
|
{ name: 'command', description: 'Shell command to execute', required: true },
|
|
653
570
|
{ name: 'cwd', description: 'Working directory', required: false },
|
|
@@ -835,11 +752,17 @@ export class AgenticTools {
|
|
|
835
752
|
}
|
|
836
753
|
// Check permission for dangerous/modifying actions
|
|
837
754
|
if (tool.requiresPermission && !this.autoApprove) {
|
|
838
|
-
|
|
839
|
-
|
|
755
|
+
let approvalScope;
|
|
756
|
+
try {
|
|
757
|
+
approvalScope = buildApprovalScope(normalizedCall.tool, normalizedCall.args, this.cwd);
|
|
758
|
+
}
|
|
759
|
+
catch (error) {
|
|
760
|
+
return this.createErrorResult(ToolErrorType.INVALID_ARGS, this.getErrorMessage(error));
|
|
761
|
+
}
|
|
762
|
+
if (this.approvals.hasPersistent(approvalScope)) {
|
|
840
763
|
this.logger.info(`${call.tool}: Auto-approved (persistent)`);
|
|
841
764
|
}
|
|
842
|
-
else if (this.
|
|
765
|
+
else if (this.approvals.hasSession(approvalScope)) {
|
|
843
766
|
// Already approved - skip permission prompt
|
|
844
767
|
this.logger.info(`${call.tool}: Auto-approved (batch)`);
|
|
845
768
|
}
|
|
@@ -856,17 +779,16 @@ export class AgenticTools {
|
|
|
856
779
|
// 'persist' (typed 'p') = approve permanently for this project
|
|
857
780
|
// 'y' or 'yes' = approve this single request only
|
|
858
781
|
if (approved === 'batch') {
|
|
859
|
-
this.
|
|
782
|
+
this.approvals.recordSession(approvalScope);
|
|
860
783
|
}
|
|
861
784
|
else if (approved === 'persist') {
|
|
862
|
-
this.
|
|
863
|
-
this.
|
|
864
|
-
this.logger.info(`${normalizedCall.tool}: Saved as persistent permission`);
|
|
785
|
+
this.approvals.recordPersistent(approvalScope);
|
|
786
|
+
this.logger.info(`${normalizedCall.tool}: Saved exact operation as persistent permission`);
|
|
865
787
|
}
|
|
866
788
|
}
|
|
867
789
|
}
|
|
868
790
|
// Execute with retry logic for applicable operations
|
|
869
|
-
return this.executeWithRetry(normalizedCall
|
|
791
|
+
return this.executeWithRetry(normalizedCall);
|
|
870
792
|
}
|
|
871
793
|
normalizeToolCall(call) {
|
|
872
794
|
const tool = String(call.tool || '').trim();
|
|
@@ -912,7 +834,7 @@ export class AgenticTools {
|
|
|
912
834
|
/**
|
|
913
835
|
* Execute tool with automatic retry for transient failures
|
|
914
836
|
*/
|
|
915
|
-
async executeWithRetry(call
|
|
837
|
+
async executeWithRetry(call) {
|
|
916
838
|
let lastError = null;
|
|
917
839
|
for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) {
|
|
918
840
|
try {
|
|
@@ -980,40 +902,7 @@ export class AgenticTools {
|
|
|
980
902
|
message,
|
|
981
903
|
};
|
|
982
904
|
}
|
|
983
|
-
|
|
984
|
-
case 'read_file':
|
|
985
|
-
return this.readFile(call.args);
|
|
986
|
-
case 'write_file':
|
|
987
|
-
return this.writeFile(call.args);
|
|
988
|
-
case 'edit_file':
|
|
989
|
-
return this.editFile(call.args);
|
|
990
|
-
case 'bash':
|
|
991
|
-
return this.bash(call.args);
|
|
992
|
-
case 'grep':
|
|
993
|
-
return this.grep(call.args);
|
|
994
|
-
case 'list_dir':
|
|
995
|
-
return this.listDir(call.args);
|
|
996
|
-
case 'glob':
|
|
997
|
-
return this.glob(call.args);
|
|
998
|
-
case 'git':
|
|
999
|
-
return this.git(call.args);
|
|
1000
|
-
case 'repo':
|
|
1001
|
-
return this.repo(call.args);
|
|
1002
|
-
case 'fetch_url':
|
|
1003
|
-
return this.fetchUrl(call.args);
|
|
1004
|
-
case 'browser':
|
|
1005
|
-
return this.browserTool(call.args);
|
|
1006
|
-
case 'ssh_exec':
|
|
1007
|
-
return this.sshExec(call.args);
|
|
1008
|
-
case 'task':
|
|
1009
|
-
return this.task(call.args);
|
|
1010
|
-
case 'multi_edit':
|
|
1011
|
-
return this.multiEdit(call.args);
|
|
1012
|
-
case 'codebase_search':
|
|
1013
|
-
return this.codebaseSearch(call.args);
|
|
1014
|
-
default:
|
|
1015
|
-
return this.createErrorResult(ToolErrorType.INVALID_ARGS, `Tool not implemented: ${call.tool}`);
|
|
1016
|
-
}
|
|
905
|
+
return await this.capabilityRegistry.execute(call);
|
|
1017
906
|
}
|
|
1018
907
|
catch (error) {
|
|
1019
908
|
const context = `Tool '${call.tool}' failed while executing with args ${this.safeStringifyArgs(call.args)}`;
|
|
@@ -1216,7 +1105,7 @@ export class AgenticTools {
|
|
|
1216
1105
|
msg += chalk.red(' The AI is requesting to execute commands on your system.\n');
|
|
1217
1106
|
}
|
|
1218
1107
|
// Add batch approval hint
|
|
1219
|
-
msg += `\n${chalk.gray('Respond:')} ${chalk.white('[y]es')} ${chalk.gray('/')} ${chalk.white('[n]o')} ${chalk.gray('/')} ${chalk.cyan('[a]pprove
|
|
1108
|
+
msg += `\n${chalk.gray('Respond:')} ${chalk.white('[y]es')} ${chalk.gray('/')} ${chalk.white('[n]o')} ${chalk.gray('/')} ${chalk.cyan('[a]pprove this exact operation')} ${chalk.gray('for this turn')}\n`;
|
|
1220
1109
|
return msg;
|
|
1221
1110
|
}
|
|
1222
1111
|
sleep(ms) {
|
|
@@ -1227,6 +1116,9 @@ export class AgenticTools {
|
|
|
1227
1116
|
* Read file with enhanced error handling and suggestions
|
|
1228
1117
|
*/
|
|
1229
1118
|
readFile(args) {
|
|
1119
|
+
if (isSensitivePath(args.path)) {
|
|
1120
|
+
return this.createErrorResult(ToolErrorType.PERMISSION_DENIED, `Sensitive file is excluded from agent context: ${path.basename(args.path)}`);
|
|
1121
|
+
}
|
|
1230
1122
|
const filePath = this.resolvePath(args.path);
|
|
1231
1123
|
if (!fs.existsSync(filePath)) {
|
|
1232
1124
|
// Try to find similar files
|
|
@@ -1322,7 +1214,7 @@ export class AgenticTools {
|
|
|
1322
1214
|
};
|
|
1323
1215
|
this.undoStack.push(undoOp);
|
|
1324
1216
|
// Limit undo stack size
|
|
1325
|
-
if (this.undoStack.length >
|
|
1217
|
+
if (this.undoStack.length > this.maxUndoStack) {
|
|
1326
1218
|
this.undoStack.shift();
|
|
1327
1219
|
}
|
|
1328
1220
|
const isNew = originalContent === null;
|
|
@@ -1395,7 +1287,7 @@ export class AgenticTools {
|
|
|
1395
1287
|
};
|
|
1396
1288
|
this.undoStack.push(undoOp);
|
|
1397
1289
|
// Limit undo stack size
|
|
1398
|
-
if (this.undoStack.length >
|
|
1290
|
+
if (this.undoStack.length > this.maxUndoStack) {
|
|
1399
1291
|
this.undoStack.shift();
|
|
1400
1292
|
}
|
|
1401
1293
|
// Show syntax-highlighted diff
|
|
@@ -1450,7 +1342,14 @@ export class AgenticTools {
|
|
|
1450
1342
|
return errors;
|
|
1451
1343
|
}
|
|
1452
1344
|
validateJavaScriptSyntax(source) {
|
|
1453
|
-
|
|
1345
|
+
let tempDirectory;
|
|
1346
|
+
try {
|
|
1347
|
+
tempDirectory = createRuntimeTempDirectory('syntax-');
|
|
1348
|
+
}
|
|
1349
|
+
catch (error) {
|
|
1350
|
+
return this.formatExternalToolError('syntax_check', 'allocate bounded temporary storage', error);
|
|
1351
|
+
}
|
|
1352
|
+
const tempFile = path.join(tempDirectory, 'candidate.js');
|
|
1454
1353
|
try {
|
|
1455
1354
|
try {
|
|
1456
1355
|
fs.writeFileSync(tempFile, source, 'utf-8');
|
|
@@ -1459,7 +1358,7 @@ export class AgenticTools {
|
|
|
1459
1358
|
return this.formatExternalToolError('syntax_check', `write temporary file ${tempFile}`, writeError);
|
|
1460
1359
|
}
|
|
1461
1360
|
try {
|
|
1462
|
-
|
|
1361
|
+
execFileSync(process.execPath, ['--check', tempFile], { stdio: 'pipe', timeout: 10_000, windowsHide: true, env: safeChildProcessEnv() });
|
|
1463
1362
|
return null;
|
|
1464
1363
|
}
|
|
1465
1364
|
catch (error) {
|
|
@@ -1469,9 +1368,7 @@ export class AgenticTools {
|
|
|
1469
1368
|
}
|
|
1470
1369
|
finally {
|
|
1471
1370
|
this.cleanupAfterToolError('syntax_check', `remove temporary file ${tempFile}`, () => {
|
|
1472
|
-
|
|
1473
|
-
fs.unlinkSync(tempFile);
|
|
1474
|
-
}
|
|
1371
|
+
removeRuntimeTempDirectory(tempDirectory);
|
|
1475
1372
|
});
|
|
1476
1373
|
}
|
|
1477
1374
|
}
|
|
@@ -1615,30 +1512,21 @@ export class AgenticTools {
|
|
|
1615
1512
|
}
|
|
1616
1513
|
try {
|
|
1617
1514
|
const startTime = Date.now();
|
|
1618
|
-
|
|
1515
|
+
const processRequest = parseSupportedProcess(args.command);
|
|
1619
1516
|
const execOptions = {
|
|
1620
1517
|
cwd,
|
|
1621
1518
|
timeout,
|
|
1622
|
-
encoding: '
|
|
1519
|
+
encoding: 'utf8',
|
|
1623
1520
|
maxBuffer: 10 * 1024 * 1024, // 10MB
|
|
1624
1521
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1625
1522
|
// Ensure consistent environment
|
|
1626
|
-
env: {
|
|
1627
|
-
...process.env,
|
|
1628
|
-
// Prevent locale issues
|
|
1629
|
-
LC_ALL: 'C',
|
|
1630
|
-
LANG: 'C',
|
|
1631
|
-
},
|
|
1523
|
+
env: safeChildProcessEnv(process.env, { LC_ALL: 'C', LANG: 'C' }),
|
|
1632
1524
|
};
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
// Use /bin/sh on Unix-like systems (more portable than bash)
|
|
1639
|
-
execOptions.shell = '/bin/sh';
|
|
1640
|
-
}
|
|
1641
|
-
const output = this.runExternalCommand('bash', args.command, args.command, execOptions);
|
|
1525
|
+
const output = execFileSync(processRequest.executable, processRequest.args, {
|
|
1526
|
+
...execOptions,
|
|
1527
|
+
encoding: 'utf8',
|
|
1528
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1529
|
+
});
|
|
1642
1530
|
const duration = Date.now() - startTime;
|
|
1643
1531
|
return {
|
|
1644
1532
|
success: true,
|
|
@@ -1651,8 +1539,23 @@ export class AgenticTools {
|
|
|
1651
1539
|
}
|
|
1652
1540
|
catch (error) {
|
|
1653
1541
|
// Command failed but may have output
|
|
1654
|
-
|
|
1655
|
-
|
|
1542
|
+
// runExternalCommand deliberately wraps child_process failures so every
|
|
1543
|
+
// external tool gets a consistent message. Preserve the underlying
|
|
1544
|
+
// ExecException here; otherwise stdout/stderr/exit status disappear and
|
|
1545
|
+
// remote executors receive only the unhelpful "Command failed" wrapper.
|
|
1546
|
+
const processError = error?.cause && typeof error.cause === 'object'
|
|
1547
|
+
? error.cause
|
|
1548
|
+
: error;
|
|
1549
|
+
const decodeProcessText = (value) => {
|
|
1550
|
+
if (Buffer.isBuffer(value))
|
|
1551
|
+
return value.toString('utf8');
|
|
1552
|
+
return typeof value === 'string' ? value : '';
|
|
1553
|
+
};
|
|
1554
|
+
const output = decodeProcessText(processError?.stdout);
|
|
1555
|
+
const stderr = decodeProcessText(processError?.stderr);
|
|
1556
|
+
const exitStatus = Number.isInteger(processError?.status)
|
|
1557
|
+
? ` (exit ${processError.status})`
|
|
1558
|
+
: '';
|
|
1656
1559
|
// Provide helpful error messages for common cases
|
|
1657
1560
|
if (error.killed) {
|
|
1658
1561
|
return this.createErrorResult(ToolErrorType.TIMEOUT, `Command timed out after ${timeout / 1000}s`, 'Try increasing the timeout or breaking the command into smaller parts.');
|
|
@@ -1667,9 +1570,9 @@ export class AgenticTools {
|
|
|
1667
1570
|
return {
|
|
1668
1571
|
success: false,
|
|
1669
1572
|
output,
|
|
1670
|
-
error: stderr || error.message
|
|
1573
|
+
error: stderr || `${error.message}${exitStatus}`,
|
|
1671
1574
|
suggestion: 'Check command syntax and try again.',
|
|
1672
|
-
canRetry: !
|
|
1575
|
+
canRetry: !processError?.killed,
|
|
1673
1576
|
};
|
|
1674
1577
|
}
|
|
1675
1578
|
}
|
|
@@ -1685,7 +1588,7 @@ export class AgenticTools {
|
|
|
1685
1588
|
if (platform === 'win32') {
|
|
1686
1589
|
return this.grepWindows(args, searchPath);
|
|
1687
1590
|
}
|
|
1688
|
-
return this.grepUnix(args, searchPath
|
|
1591
|
+
return this.grepUnix(args, searchPath);
|
|
1689
1592
|
}
|
|
1690
1593
|
/**
|
|
1691
1594
|
* Windows grep: rg > Select-String > Node-native
|
|
@@ -1693,96 +1596,51 @@ export class AgenticTools {
|
|
|
1693
1596
|
grepWindows(args, searchPath) {
|
|
1694
1597
|
// 1. Try ripgrep (rg) first — best cross-platform search tool
|
|
1695
1598
|
try {
|
|
1696
|
-
|
|
1599
|
+
execFileSync('rg', ['--version'], { encoding: 'utf-8', timeout: 5000, stdio: 'pipe', windowsHide: true, env: safeChildProcessEnv() });
|
|
1697
1600
|
return this.grepWithRg(args, searchPath);
|
|
1698
1601
|
}
|
|
1699
1602
|
catch (error) {
|
|
1700
1603
|
this.logger.debug(this.formatExternalToolError('grep', 'checking ripgrep availability on Windows', error));
|
|
1701
1604
|
}
|
|
1702
|
-
//
|
|
1703
|
-
|
|
1704
|
-
return this.grepWithSelectString(args, searchPath);
|
|
1705
|
-
}
|
|
1706
|
-
catch (error) {
|
|
1707
|
-
this.logger.debug(this.formatExternalToolError('grep', 'running PowerShell Select-String fallback', error));
|
|
1708
|
-
}
|
|
1709
|
-
// 3. Fall back to Node-native recursive file scanner
|
|
1605
|
+
// Fall back to the Node-native scanner. Avoid constructing a PowerShell
|
|
1606
|
+
// program from model-controlled patterns.
|
|
1710
1607
|
return this.grepNodeNative(args, searchPath);
|
|
1711
1608
|
}
|
|
1712
1609
|
/**
|
|
1713
1610
|
* Unix grep: rg > system grep (BSD/GNU)
|
|
1714
1611
|
*/
|
|
1715
|
-
grepUnix(args, searchPath
|
|
1612
|
+
grepUnix(args, searchPath) {
|
|
1716
1613
|
// Try ripgrep first if available
|
|
1717
1614
|
try {
|
|
1718
|
-
|
|
1615
|
+
execFileSync('rg', ['--version'], { encoding: 'utf-8', timeout: 5000, stdio: 'pipe', windowsHide: true, env: safeChildProcessEnv() });
|
|
1719
1616
|
return this.grepWithRg(args, searchPath);
|
|
1720
1617
|
}
|
|
1721
1618
|
catch (error) {
|
|
1722
1619
|
this.logger.debug(this.formatExternalToolError('grep', 'checking ripgrep availability on Unix', error));
|
|
1723
1620
|
// rg not available, fall through to system grep
|
|
1724
1621
|
}
|
|
1725
|
-
|
|
1726
|
-
let cmd;
|
|
1727
|
-
if (isMac) {
|
|
1728
|
-
cmd = args.include
|
|
1729
|
-
? `grep -rn --include="${args.include}" "${args.pattern}" "${searchPath}"`
|
|
1730
|
-
: `grep -rn "${args.pattern}" "${searchPath}"`;
|
|
1731
|
-
}
|
|
1732
|
-
else {
|
|
1733
|
-
cmd = args.include
|
|
1734
|
-
? `grep -rn --color=never --include="${args.include}" "${args.pattern}" "${searchPath}"`
|
|
1735
|
-
: `grep -rn --color=never "${args.pattern}" "${searchPath}"`;
|
|
1736
|
-
}
|
|
1737
|
-
try {
|
|
1738
|
-
const output = this.runExternalCommand('grep', 'system grep search', cmd, {
|
|
1739
|
-
cwd: this.cwd,
|
|
1740
|
-
encoding: 'utf-8',
|
|
1741
|
-
maxBuffer: 5 * 1024 * 1024,
|
|
1742
|
-
timeout: 30000,
|
|
1743
|
-
env: { ...process.env, GREP_OPTIONS: '' },
|
|
1744
|
-
});
|
|
1745
|
-
return {
|
|
1746
|
-
success: true,
|
|
1747
|
-
output: output.trim(),
|
|
1748
|
-
metadata: { searchStatus: 'search_matches_found' },
|
|
1749
|
-
};
|
|
1750
|
-
}
|
|
1751
|
-
catch (error) {
|
|
1752
|
-
// Distinguish real "no matches" (exit 1 + no stderr) from command failure
|
|
1753
|
-
const stderr = (error.stderr || '').toString();
|
|
1754
|
-
if (error.status === 1 && !stderr.trim()) {
|
|
1755
|
-
return {
|
|
1756
|
-
success: true,
|
|
1757
|
-
output: 'No matches found',
|
|
1758
|
-
metadata: { searchStatus: 'search_no_matches' },
|
|
1759
|
-
};
|
|
1760
|
-
}
|
|
1761
|
-
// Real failure: command not found, permission denied, etc.
|
|
1762
|
-
return {
|
|
1763
|
-
success: false,
|
|
1764
|
-
error: stderr || error.message,
|
|
1765
|
-
suggestion: 'The grep command failed. Install ripgrep (rg) for best cross-platform search.',
|
|
1766
|
-
canRetry: true,
|
|
1767
|
-
metadata: { searchStatus: 'search_failed' },
|
|
1768
|
-
};
|
|
1769
|
-
}
|
|
1622
|
+
return this.grepNodeNative(args, searchPath);
|
|
1770
1623
|
}
|
|
1771
1624
|
/**
|
|
1772
1625
|
* Search using ripgrep (rg) — works on all platforms
|
|
1773
1626
|
*/
|
|
1774
1627
|
grepWithRg(args, searchPath) {
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1628
|
+
const rgArgs = [
|
|
1629
|
+
'-n', '--no-heading',
|
|
1630
|
+
'-g', '!.git/**', '-g', '!.vigthoria/**', '-g', '!.env', '-g', '!.env.*',
|
|
1631
|
+
'-g', '!**/credentials*', '-g', '!**/secrets*', '-g', '!**/*.pem', '-g', '!**/*.key',
|
|
1632
|
+
];
|
|
1633
|
+
if (args.include)
|
|
1634
|
+
rgArgs.push('-g', args.include);
|
|
1635
|
+
rgArgs.push('--', args.pattern, searchPath);
|
|
1780
1636
|
try {
|
|
1781
|
-
const output =
|
|
1637
|
+
const output = execFileSync('rg', rgArgs, {
|
|
1782
1638
|
cwd: this.cwd,
|
|
1783
|
-
encoding: '
|
|
1639
|
+
encoding: 'utf8',
|
|
1784
1640
|
maxBuffer: 5 * 1024 * 1024,
|
|
1785
1641
|
timeout: 30000,
|
|
1642
|
+
windowsHide: true,
|
|
1643
|
+
env: safeChildProcessEnv(),
|
|
1786
1644
|
});
|
|
1787
1645
|
return {
|
|
1788
1646
|
success: true,
|
|
@@ -1801,56 +1659,6 @@ export class AgenticTools {
|
|
|
1801
1659
|
throw error; // Let caller try next backend
|
|
1802
1660
|
}
|
|
1803
1661
|
}
|
|
1804
|
-
/**
|
|
1805
|
-
* Search using PowerShell Select-String (Windows)
|
|
1806
|
-
*/
|
|
1807
|
-
grepWithSelectString(args, searchPath) {
|
|
1808
|
-
// Normalize path for PowerShell
|
|
1809
|
-
const psPath = searchPath.replace(/\//g, '\\');
|
|
1810
|
-
if (!POWERSHELL_SAFE_PATH_PATTERN.test(psPath)) {
|
|
1811
|
-
return this.createErrorResult(ToolErrorType.INVALID_ARGS, 'Unsafe search path for PowerShell backend', 'Use a normal workspace path containing only letters, numbers, separators, dot, dash, and spaces.');
|
|
1812
|
-
}
|
|
1813
|
-
const includeArg = args.include ? String(args.include) : '*';
|
|
1814
|
-
if (!POWERSHELL_SAFE_INCLUDE_PATTERN.test(includeArg)) {
|
|
1815
|
-
return this.createErrorResult(ToolErrorType.INVALID_ARGS, 'Unsafe include filter for PowerShell backend', 'Use simple wildcard filters like *.ts or *.py.');
|
|
1816
|
-
}
|
|
1817
|
-
const includeFilter = `-Include "${includeArg}"`;
|
|
1818
|
-
const escapedPattern = args.pattern.replace(/'/g, "''");
|
|
1819
|
-
const cmd = `powershell -NoProfile -Command "Get-ChildItem -Path '${psPath}' -Recurse -File ${includeFilter} | Select-String -Pattern '${escapedPattern}' | ForEach-Object { $_.Path + ':' + $_.LineNumber + ':' + $_.Line }"`;
|
|
1820
|
-
try {
|
|
1821
|
-
const output = execSync(cmd, {
|
|
1822
|
-
cwd: this.cwd,
|
|
1823
|
-
encoding: 'utf-8',
|
|
1824
|
-
maxBuffer: 5 * 1024 * 1024,
|
|
1825
|
-
timeout: 60000,
|
|
1826
|
-
});
|
|
1827
|
-
const trimmed = output.trim();
|
|
1828
|
-
if (!trimmed) {
|
|
1829
|
-
return {
|
|
1830
|
-
success: true,
|
|
1831
|
-
output: 'No matches found',
|
|
1832
|
-
metadata: { searchStatus: 'search_no_matches', backend: 'select-string' },
|
|
1833
|
-
};
|
|
1834
|
-
}
|
|
1835
|
-
return {
|
|
1836
|
-
success: true,
|
|
1837
|
-
output: trimmed,
|
|
1838
|
-
metadata: { searchStatus: 'search_matches_found', backend: 'select-string' },
|
|
1839
|
-
};
|
|
1840
|
-
}
|
|
1841
|
-
catch (error) {
|
|
1842
|
-
const stderr = (error.stderr || '').toString();
|
|
1843
|
-
// Select-String returns exit code 1 for no matches when used in pipeline
|
|
1844
|
-
if (error.status === 1 && !stderr.trim()) {
|
|
1845
|
-
return {
|
|
1846
|
-
success: true,
|
|
1847
|
-
output: 'No matches found',
|
|
1848
|
-
metadata: { searchStatus: 'search_no_matches', backend: 'select-string' },
|
|
1849
|
-
};
|
|
1850
|
-
}
|
|
1851
|
-
throw error; // Let caller try next backend
|
|
1852
|
-
}
|
|
1853
|
-
}
|
|
1854
1662
|
/**
|
|
1855
1663
|
* Pure Node.js recursive file search — reliable on all platforms, no external deps
|
|
1856
1664
|
*/
|
|
@@ -1897,6 +1705,9 @@ export class AgenticTools {
|
|
|
1897
1705
|
}
|
|
1898
1706
|
if (!entry.isFile())
|
|
1899
1707
|
continue;
|
|
1708
|
+
const relativePath = path.relative(this.cwd, fullPath).replace(/\\/g, '/');
|
|
1709
|
+
if (isSensitivePath(relativePath))
|
|
1710
|
+
continue;
|
|
1900
1711
|
// Check include pattern
|
|
1901
1712
|
if (includeGlob) {
|
|
1902
1713
|
const matches = minimatch
|
|
@@ -1915,7 +1726,6 @@ export class AgenticTools {
|
|
|
1915
1726
|
const lines = content.split('\n');
|
|
1916
1727
|
for (let i = 0; i < lines.length; i++) {
|
|
1917
1728
|
if (regex.test(lines[i])) {
|
|
1918
|
-
const relativePath = path.relative(this.cwd, fullPath).replace(/\\/g, '/');
|
|
1919
1729
|
results.push(`${relativePath}:${i + 1}:${lines[i]}`);
|
|
1920
1730
|
if (results.length >= maxResults)
|
|
1921
1731
|
break;
|
|
@@ -1957,7 +1767,7 @@ export class AgenticTools {
|
|
|
1957
1767
|
return { success: false, error: `Directory not found: ${args.path}` };
|
|
1958
1768
|
}
|
|
1959
1769
|
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
1960
|
-
const output = entries.map(e => {
|
|
1770
|
+
const output = entries.filter((entry) => !isSensitivePath(path.join(args.path || '.', entry.name))).map(e => {
|
|
1961
1771
|
const suffix = e.isDirectory() ? '/' : '';
|
|
1962
1772
|
return e.name + suffix;
|
|
1963
1773
|
}).join('\n');
|
|
@@ -1966,8 +1776,17 @@ export class AgenticTools {
|
|
|
1966
1776
|
glob(args) {
|
|
1967
1777
|
const { globSync } = require('glob');
|
|
1968
1778
|
try {
|
|
1969
|
-
const files = globSync(args.pattern, { cwd: this.cwd });
|
|
1970
|
-
|
|
1779
|
+
const files = globSync(args.pattern, { cwd: this.cwd, follow: false, nodir: true });
|
|
1780
|
+
const contained = files.filter((file) => {
|
|
1781
|
+
try {
|
|
1782
|
+
this.resolvePath(file);
|
|
1783
|
+
return !isSensitivePath(file);
|
|
1784
|
+
}
|
|
1785
|
+
catch {
|
|
1786
|
+
return false;
|
|
1787
|
+
}
|
|
1788
|
+
});
|
|
1789
|
+
return { success: true, output: contained.join('\n') };
|
|
1971
1790
|
}
|
|
1972
1791
|
catch (error) {
|
|
1973
1792
|
return { success: false, error: error.message };
|
|
@@ -1975,10 +1794,13 @@ export class AgenticTools {
|
|
|
1975
1794
|
}
|
|
1976
1795
|
git(args) {
|
|
1977
1796
|
try {
|
|
1978
|
-
const
|
|
1797
|
+
const gitArgs = parseGitArguments(args.args);
|
|
1798
|
+
const output = execFileSync('git', hardenGitArguments(gitArgs), {
|
|
1979
1799
|
cwd: this.cwd,
|
|
1980
1800
|
encoding: 'utf-8',
|
|
1981
1801
|
timeout: 60000,
|
|
1802
|
+
windowsHide: true,
|
|
1803
|
+
env: safeChildProcessEnv(safeGitEnvironment()),
|
|
1982
1804
|
});
|
|
1983
1805
|
return { success: true, output: output.trim() };
|
|
1984
1806
|
}
|
|
@@ -1997,158 +1819,39 @@ export class AgenticTools {
|
|
|
1997
1819
|
const targetPath = args.path || this.cwd;
|
|
1998
1820
|
const description = args.description || '';
|
|
1999
1821
|
try {
|
|
2000
|
-
//
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
}
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
return { success: false, error: 'No auth token found. Run vigthoria login first.' };
|
|
2019
|
-
}
|
|
2020
|
-
// Build files array
|
|
2021
|
-
let filesArray = [];
|
|
2022
|
-
if (args.files) {
|
|
2023
|
-
// AI passed files directly
|
|
2024
|
-
let filesInput;
|
|
2025
|
-
if (typeof args.files === 'string') {
|
|
2026
|
-
try {
|
|
2027
|
-
filesInput = JSON.parse(args.files);
|
|
2028
|
-
}
|
|
2029
|
-
catch {
|
|
2030
|
-
filesInput = args.files;
|
|
2031
|
-
}
|
|
2032
|
-
}
|
|
2033
|
-
else {
|
|
2034
|
-
filesInput = args.files;
|
|
2035
|
-
}
|
|
2036
|
-
if (Array.isArray(filesInput)) {
|
|
2037
|
-
// Already in [{path, content}] format
|
|
2038
|
-
filesArray = filesInput.map((f) => ({ path: f.path || f.name || 'file', content: String(f.content || '') }));
|
|
2039
|
-
}
|
|
2040
|
-
else if (typeof filesInput === 'object' && filesInput !== null) {
|
|
2041
|
-
// {filename: content} format - convert
|
|
2042
|
-
for (const [filePath, content] of Object.entries(filesInput)) {
|
|
2043
|
-
filesArray.push({ path: filePath, content: String(content) });
|
|
2044
|
-
}
|
|
2045
|
-
}
|
|
2046
|
-
}
|
|
2047
|
-
// If no files from AI, read from disk
|
|
2048
|
-
if (filesArray.length === 0 && fs.existsSync(targetPath)) {
|
|
2049
|
-
const readDir = (dir, base) => {
|
|
2050
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
2051
|
-
for (const entry of entries) {
|
|
2052
|
-
const fullPath = path.join(dir, entry.name);
|
|
2053
|
-
const relPath = path.relative(base, fullPath);
|
|
2054
|
-
// Skip hidden dirs, node_modules, common non-essential dirs
|
|
2055
|
-
if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === '__pycache__')
|
|
2056
|
-
continue;
|
|
2057
|
-
if (entry.isDirectory()) {
|
|
2058
|
-
readDir(fullPath, base);
|
|
2059
|
-
}
|
|
2060
|
-
else if (entry.isFile()) {
|
|
2061
|
-
try {
|
|
2062
|
-
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
2063
|
-
filesArray.push({ path: relPath, content });
|
|
2064
|
-
}
|
|
2065
|
-
catch { /* skip binary/unreadable files */ }
|
|
2066
|
-
}
|
|
2067
|
-
}
|
|
2068
|
-
};
|
|
2069
|
-
const stat = fs.statSync(targetPath);
|
|
2070
|
-
if (stat.isDirectory()) {
|
|
2071
|
-
readDir(targetPath, targetPath);
|
|
2072
|
-
}
|
|
2073
|
-
else {
|
|
2074
|
-
filesArray.push({ path: path.basename(targetPath), content: fs.readFileSync(targetPath, 'utf-8') });
|
|
1822
|
+
// Every repository action invokes the canonical CLI route through an
|
|
1823
|
+
// argument vector. This prevents the agent tool from developing a second
|
|
1824
|
+
// credential, containment, mutation-retry, and payload policy.
|
|
1825
|
+
// Project,
|
|
1826
|
+
// user, permission, and destination values never enter a shell string.
|
|
1827
|
+
let cliArgs;
|
|
1828
|
+
const validRepoIdentifier = (value) => /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value);
|
|
1829
|
+
if (project && !validRepoIdentifier(project)) {
|
|
1830
|
+
return { success: false, error: 'Project name contains unsupported characters' };
|
|
1831
|
+
}
|
|
1832
|
+
switch (action) {
|
|
1833
|
+
case 'push':
|
|
1834
|
+
if (!project) {
|
|
1835
|
+
return {
|
|
1836
|
+
success: false,
|
|
1837
|
+
error: 'Project name is required for push',
|
|
1838
|
+
suggestion: 'Provide a project name, e.g., repo action=push project=my-project',
|
|
1839
|
+
};
|
|
2075
1840
|
}
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
projectName: project,
|
|
2083
|
-
description: description || `${project} - pushed via Vigthoria AI`,
|
|
2084
|
-
visibility,
|
|
2085
|
-
files: filesArray,
|
|
2086
|
-
commitMessage: `Push from Vigthoria AI: ${filesArray.length} file(s)`
|
|
2087
|
-
});
|
|
2088
|
-
const headers = {
|
|
2089
|
-
'Authorization': `Bearer ${authToken}`,
|
|
2090
|
-
'Content-Type': 'application/json'
|
|
2091
|
-
};
|
|
2092
|
-
// Try proxy first; direct Community server is only reachable when running on the
|
|
2093
|
-
// Vigthoria backend itself (isServerRuntime), never expose this fallback to remote users.
|
|
2094
|
-
const pushUrls = [`${apiBase}/api/community-repo/push`];
|
|
2095
|
-
if (isServerRuntime()) {
|
|
2096
|
-
const directHost = ['local', 'host'].join('');
|
|
2097
|
-
pushUrls.push(`http://${directHost}:9000/api/repo/push`);
|
|
2098
|
-
}
|
|
2099
|
-
let result;
|
|
2100
|
-
let lastError = '';
|
|
2101
|
-
for (const pushUrl of pushUrls) {
|
|
2102
|
-
try {
|
|
2103
|
-
const response = await fetch(pushUrl, { method: 'POST', headers, body });
|
|
2104
|
-
result = await response.json();
|
|
2105
|
-
if (response.ok && result.success)
|
|
2106
|
-
break;
|
|
2107
|
-
if (result.code === 'REPOSITORY_SECURITY_REVIEW') {
|
|
2108
|
-
return {
|
|
2109
|
-
success: false,
|
|
2110
|
-
error: result.error || 'Repository security review required',
|
|
2111
|
-
metadata: {
|
|
2112
|
-
code: result.code,
|
|
2113
|
-
review: {
|
|
2114
|
-
reviewId: result.reviewId,
|
|
2115
|
-
status: result.status,
|
|
2116
|
-
riskScore: result.riskScore,
|
|
2117
|
-
findings: result.findings || [],
|
|
2118
|
-
actionItems: result.actionItems || [],
|
|
2119
|
-
retryCommand: result.retryCommand,
|
|
2120
|
-
},
|
|
2121
|
-
},
|
|
2122
|
-
suggestion: `Inspect with "vigthoria repo review ${result.reviewId}" and retry corrected content with "${result.retryCommand || `vigthoria repo push --retry ${result.reviewId}`}".`,
|
|
2123
|
-
};
|
|
2124
|
-
}
|
|
2125
|
-
lastError = result.error || result.message || `Status ${response.status}`;
|
|
2126
|
-
result = null;
|
|
1841
|
+
if (args.files) {
|
|
1842
|
+
return {
|
|
1843
|
+
success: false,
|
|
1844
|
+
error: 'Inline repository payloads are prohibited. Write approved files inside the workspace before pushing.',
|
|
1845
|
+
suggestion: 'Use write_file for each approved workspace file, then call repo action=push with path.',
|
|
1846
|
+
};
|
|
2127
1847
|
}
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
result = null;
|
|
1848
|
+
if (!['private', 'restricted', 'public'].includes(visibility)) {
|
|
1849
|
+
return { success: false, error: 'Repository visibility is invalid' };
|
|
2131
1850
|
}
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
error: lastError || 'Push failed on all endpoints',
|
|
2137
|
-
suggestion: 'Check your authentication and try again. Run vigthoria login to refresh your token.'
|
|
2138
|
-
};
|
|
2139
|
-
}
|
|
2140
|
-
return {
|
|
2141
|
-
success: true,
|
|
2142
|
-
output: `Successfully pushed "${project}" to Vigthoria Community!\n` +
|
|
2143
|
-
`Action: ${result.action || 'created'}\n` +
|
|
2144
|
-
`Files: ${result.filesWritten || filesArray.length}\n` +
|
|
2145
|
-
`URL: ${result.url || `https://community.vigthoria.io/showcase/${result.projectId}`}`,
|
|
2146
|
-
metadata: { action: 'push', project, filesWritten: result.filesWritten || filesArray.length, url: result.url }
|
|
2147
|
-
};
|
|
2148
|
-
}
|
|
2149
|
-
// All other actions use CLI commands
|
|
2150
|
-
let command;
|
|
2151
|
-
switch (action) {
|
|
1851
|
+
cliArgs = ['repo', 'push', this.resolvePath(targetPath), '--name', project, '--visibility', visibility, '--yes'];
|
|
1852
|
+
if (description)
|
|
1853
|
+
cliArgs.push('--description', description);
|
|
1854
|
+
break;
|
|
2152
1855
|
case 'pull':
|
|
2153
1856
|
if (!project) {
|
|
2154
1857
|
return {
|
|
@@ -2157,10 +1860,10 @@ export class AgenticTools {
|
|
|
2157
1860
|
suggestion: 'Provide a project name, e.g., repo action=pull project=my-project'
|
|
2158
1861
|
};
|
|
2159
1862
|
}
|
|
2160
|
-
|
|
1863
|
+
cliArgs = ['repo', 'pull', project];
|
|
2161
1864
|
break;
|
|
2162
1865
|
case 'list':
|
|
2163
|
-
|
|
1866
|
+
cliArgs = ['repo', 'list'];
|
|
2164
1867
|
break;
|
|
2165
1868
|
case 'status':
|
|
2166
1869
|
if (!project) {
|
|
@@ -2170,7 +1873,7 @@ export class AgenticTools {
|
|
|
2170
1873
|
suggestion: 'Provide a project name, e.g., repo action=status project=my-project'
|
|
2171
1874
|
};
|
|
2172
1875
|
}
|
|
2173
|
-
|
|
1876
|
+
cliArgs = ['repo', 'status', project];
|
|
2174
1877
|
break;
|
|
2175
1878
|
case 'share':
|
|
2176
1879
|
if (!project || !args.username) {
|
|
@@ -2181,7 +1884,10 @@ export class AgenticTools {
|
|
|
2181
1884
|
};
|
|
2182
1885
|
}
|
|
2183
1886
|
const permission = args.permission || 'read';
|
|
2184
|
-
|
|
1887
|
+
if (!validRepoIdentifier(args.username) || !['read', 'write', 'admin'].includes(permission)) {
|
|
1888
|
+
return { success: false, error: 'Repository username or permission is invalid' };
|
|
1889
|
+
}
|
|
1890
|
+
cliArgs = ['repo', 'share', project, args.username, '--permission', permission];
|
|
2185
1891
|
break;
|
|
2186
1892
|
case 'delete':
|
|
2187
1893
|
if (!project) {
|
|
@@ -2191,7 +1897,7 @@ export class AgenticTools {
|
|
|
2191
1897
|
suggestion: 'Provide a project name, e.g., repo action=delete project=my-project'
|
|
2192
1898
|
};
|
|
2193
1899
|
}
|
|
2194
|
-
|
|
1900
|
+
cliArgs = ['repo', 'delete', project, '--force'];
|
|
2195
1901
|
break;
|
|
2196
1902
|
case 'clone':
|
|
2197
1903
|
if (!project) {
|
|
@@ -2201,7 +1907,7 @@ export class AgenticTools {
|
|
|
2201
1907
|
suggestion: 'Provide a project name, e.g., repo action=clone project=my-project path=/path/to/target'
|
|
2202
1908
|
};
|
|
2203
1909
|
}
|
|
2204
|
-
|
|
1910
|
+
cliArgs = ['repo', 'clone', project, this.resolvePath(targetPath)];
|
|
2205
1911
|
break;
|
|
2206
1912
|
default:
|
|
2207
1913
|
return {
|
|
@@ -2210,11 +1916,12 @@ export class AgenticTools {
|
|
|
2210
1916
|
suggestion: 'Available actions: push, pull, list, status, share, delete, clone'
|
|
2211
1917
|
};
|
|
2212
1918
|
}
|
|
2213
|
-
const
|
|
1919
|
+
const cliEntry = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'index.js');
|
|
1920
|
+
const output = execFileSync(process.execPath, [cliEntry, ...cliArgs], {
|
|
2214
1921
|
cwd: this.cwd,
|
|
2215
1922
|
encoding: 'utf-8',
|
|
2216
1923
|
timeout: 120000,
|
|
2217
|
-
env:
|
|
1924
|
+
env: safeChildProcessEnv(process.env, { FORCE_COLOR: '0' })
|
|
2218
1925
|
});
|
|
2219
1926
|
return {
|
|
2220
1927
|
success: true,
|
|
@@ -2508,25 +2215,26 @@ export class AgenticTools {
|
|
|
2508
2215
|
}
|
|
2509
2216
|
}
|
|
2510
2217
|
try {
|
|
2511
|
-
const
|
|
2512
|
-
const
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2218
|
+
const processRequest = parseSupportedProcess(command);
|
|
2219
|
+
const remoteExecutable = path.basename(processRequest.executable);
|
|
2220
|
+
const sshArgs = [
|
|
2221
|
+
'-o', 'BatchMode=yes',
|
|
2222
|
+
'-o', 'StrictHostKeyChecking=accept-new',
|
|
2223
|
+
'-o', 'ConnectTimeout=10',
|
|
2224
|
+
normalizedHost,
|
|
2225
|
+
'--',
|
|
2226
|
+
remoteExecutable,
|
|
2227
|
+
...processRequest.args,
|
|
2228
|
+
];
|
|
2229
|
+
assertProcessNetworkAllowed('ssh', sshArgs);
|
|
2230
|
+
const execOptions = {
|
|
2231
|
+
encoding: 'utf8',
|
|
2516
2232
|
timeout,
|
|
2517
2233
|
maxBuffer: 10 * 1024 * 1024,
|
|
2234
|
+
windowsHide: true,
|
|
2235
|
+
env: safeChildProcessEnv(),
|
|
2518
2236
|
};
|
|
2519
|
-
|
|
2520
|
-
// On Windows, use the ssh command from OpenSSH
|
|
2521
|
-
sshCommand = `ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 ${host} "${command.replace(/"/g, '\\"')}"`;
|
|
2522
|
-
execOptions.shell = true;
|
|
2523
|
-
}
|
|
2524
|
-
else {
|
|
2525
|
-
// On Unix-like systems
|
|
2526
|
-
sshCommand = `ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 ${host} '${command.replace(/'/g, "'\\''")}'`;
|
|
2527
|
-
execOptions.shell = '/bin/sh';
|
|
2528
|
-
}
|
|
2529
|
-
const output = execSync(sshCommand, execOptions);
|
|
2237
|
+
const output = execFileSync('ssh', sshArgs, execOptions);
|
|
2530
2238
|
return {
|
|
2531
2239
|
success: true,
|
|
2532
2240
|
output: output.trim(),
|
|
@@ -2610,23 +2318,25 @@ export class AgenticTools {
|
|
|
2610
2318
|
else {
|
|
2611
2319
|
resolvedPath = path.normalize(path.join(this.cwd, safeInput));
|
|
2612
2320
|
}
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
// Return the sanitized relative path within workspace
|
|
2618
|
-
const basename = path.basename(stripWorkspacePrefix(normalizedInput) || normalizedInput);
|
|
2619
|
-
return path.join(this.cwd, basename);
|
|
2620
|
-
}
|
|
2621
|
-
return resolvedPath;
|
|
2321
|
+
return assertContainedAbsolutePath(workspaceRoot, resolvedPath, {
|
|
2322
|
+
allowMissing: true,
|
|
2323
|
+
allowRoot: true,
|
|
2324
|
+
});
|
|
2622
2325
|
}
|
|
2623
2326
|
/**
|
|
2624
2327
|
* Check if a path is within the allowed workspace
|
|
2625
2328
|
*/
|
|
2626
2329
|
isPathWithinWorkspace(p) {
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2330
|
+
try {
|
|
2331
|
+
assertContainedAbsolutePath(this.cwd, path.isAbsolute(p) ? p : path.join(this.cwd, p), {
|
|
2332
|
+
allowMissing: true,
|
|
2333
|
+
allowRoot: true,
|
|
2334
|
+
});
|
|
2335
|
+
return true;
|
|
2336
|
+
}
|
|
2337
|
+
catch {
|
|
2338
|
+
return false;
|
|
2339
|
+
}
|
|
2630
2340
|
}
|
|
2631
2341
|
// ═══════════════════════════════════════════════════════════════
|
|
2632
2342
|
// TASK (Sub-Agent) Tool
|
|
@@ -2660,10 +2370,10 @@ export class AgenticTools {
|
|
|
2660
2370
|
try {
|
|
2661
2371
|
for (let turn = 0; turn < maxSubTurns; turn++) {
|
|
2662
2372
|
// Import api dynamically to avoid circular dependency
|
|
2663
|
-
const {
|
|
2373
|
+
const { createAPIClient } = await import('./api-client-factory.js');
|
|
2664
2374
|
const { Config } = await import('./config.js');
|
|
2665
2375
|
const config = new Config();
|
|
2666
|
-
const api =
|
|
2376
|
+
const api = createAPIClient(config, this.logger);
|
|
2667
2377
|
const response = await api.chat(messages, 'code');
|
|
2668
2378
|
const assistantMessage = response.message || '';
|
|
2669
2379
|
messages.push({ role: 'assistant', content: assistantMessage });
|
|
@@ -2822,6 +2532,9 @@ export class AgenticTools {
|
|
|
2822
2532
|
if (ignorePatterns.includes(entry.name) || entry.name.startsWith('.'))
|
|
2823
2533
|
continue;
|
|
2824
2534
|
const fullPath = path.join(currentDir, entry.name);
|
|
2535
|
+
const relativePath = path.relative(this.cwd, fullPath).replace(/\\/g, '/');
|
|
2536
|
+
if (isSensitivePath(relativePath))
|
|
2537
|
+
continue;
|
|
2825
2538
|
if (entry.isDirectory()) {
|
|
2826
2539
|
walk(fullPath, depth + 1);
|
|
2827
2540
|
}
|
|
@@ -2830,13 +2543,15 @@ export class AgenticTools {
|
|
|
2830
2543
|
const basename = entry.name;
|
|
2831
2544
|
// Simple glob matching for extension patterns like *.ts, *.js
|
|
2832
2545
|
const globPattern = pattern.replace(/\*\*\//g, '(.*/)?').replace(/\*/g, '[^/]*').replace(/\?/g, '.');
|
|
2833
|
-
if (new RegExp(globPattern, 'i').test(basename)
|
|
2834
|
-
|
|
2835
|
-
}
|
|
2546
|
+
if (!new RegExp(globPattern, 'i').test(basename) && !fullPath.includes(pattern.replace(/\*/g, '')))
|
|
2547
|
+
continue;
|
|
2836
2548
|
}
|
|
2837
|
-
|
|
2838
|
-
|
|
2549
|
+
try {
|
|
2550
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
2551
|
+
if (!containsHighConfidenceSecret(content))
|
|
2552
|
+
files.push(fullPath);
|
|
2839
2553
|
}
|
|
2554
|
+
catch { /* unreadable files are excluded from agent context */ }
|
|
2840
2555
|
}
|
|
2841
2556
|
}
|
|
2842
2557
|
};
|
|
@@ -2913,12 +2628,12 @@ export class AgenticTools {
|
|
|
2913
2628
|
if (includePattern)
|
|
2914
2629
|
rgArgs.push('-g', includePattern);
|
|
2915
2630
|
rgArgs.push('--', query, this.cwd);
|
|
2916
|
-
const
|
|
2917
|
-
const quote = (s) => isWin ? `"${s}"` : `'${s}'`;
|
|
2918
|
-
const rgOutput = execSync(`rg ${rgArgs.map(a => quote(a)).join(' ')}`, {
|
|
2631
|
+
const rgOutput = execFileSync('rg', rgArgs, {
|
|
2919
2632
|
encoding: 'utf-8',
|
|
2920
2633
|
timeout: 15000,
|
|
2921
2634
|
maxBuffer: 5 * 1024 * 1024,
|
|
2635
|
+
windowsHide: true,
|
|
2636
|
+
env: safeChildProcessEnv(),
|
|
2922
2637
|
}).trim();
|
|
2923
2638
|
for (const line of rgOutput.split('\n').slice(0, maxResults - results.length)) {
|
|
2924
2639
|
if (!line.trim())
|
|
@@ -3032,7 +2747,6 @@ export class AgenticTools {
|
|
|
3032
2747
|
.replace(/\t/g, '\\t'); // Escape tabs
|
|
3033
2748
|
// Escape literal newlines inside strings (but not \n which is already escaped)
|
|
3034
2749
|
// We need to be careful - only escape newlines that are inside quoted strings
|
|
3035
|
-
const parts = [];
|
|
3036
2750
|
let inString = false;
|
|
3037
2751
|
let currentPart = '';
|
|
3038
2752
|
for (let i = 0; i < fixed.length; i++) {
|