keystone-cli 1.0.3 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +276 -32
- package/package.json +8 -4
- package/src/cli.ts +350 -416
- package/src/commands/doc.ts +31 -0
- package/src/commands/event.ts +29 -0
- package/src/commands/graph.ts +37 -0
- package/src/commands/index.ts +14 -0
- package/src/commands/init.ts +185 -0
- package/src/commands/run.ts +124 -0
- package/src/commands/schema.ts +40 -0
- package/src/commands/utils.ts +78 -0
- package/src/commands/validate.ts +111 -0
- package/src/db/workflow-db.test.ts +314 -0
- package/src/db/workflow-db.ts +810 -210
- package/src/expression/evaluator-audit.test.ts +4 -2
- package/src/expression/evaluator.test.ts +14 -1
- package/src/expression/evaluator.ts +166 -19
- package/src/parser/config-schema.ts +18 -0
- package/src/parser/schema.ts +153 -22
- package/src/parser/test-schema.ts +6 -6
- package/src/parser/workflow-parser.test.ts +24 -0
- package/src/parser/workflow-parser.ts +65 -3
- package/src/runner/auto-heal.test.ts +5 -6
- package/src/runner/blueprint-executor.test.ts +2 -2
- package/src/runner/debug-repl.test.ts +5 -8
- package/src/runner/debug-repl.ts +59 -16
- package/src/runner/durable-timers.test.ts +11 -2
- package/src/runner/engine-executor.test.ts +1 -1
- package/src/runner/events.ts +57 -0
- package/src/runner/executors/artifact-executor.ts +166 -0
- package/src/runner/{blueprint-executor.ts → executors/blueprint-executor.ts} +15 -7
- package/src/runner/{engine-executor.ts → executors/engine-executor.ts} +55 -7
- package/src/runner/executors/file-executor.test.ts +48 -0
- package/src/runner/executors/file-executor.ts +324 -0
- package/src/runner/{foreach-executor.ts → executors/foreach-executor.ts} +168 -80
- package/src/runner/executors/human-executor.ts +144 -0
- package/src/runner/executors/join-executor.ts +75 -0
- package/src/runner/executors/llm-executor.ts +1266 -0
- package/src/runner/executors/memory-executor.ts +71 -0
- package/src/runner/executors/plan-executor.ts +104 -0
- package/src/runner/executors/request-executor.ts +265 -0
- package/src/runner/executors/script-executor.ts +43 -0
- package/src/runner/executors/shell-executor.ts +403 -0
- package/src/runner/executors/subworkflow-executor.ts +114 -0
- package/src/runner/executors/types.ts +69 -0
- package/src/runner/executors/wait-executor.ts +59 -0
- package/src/runner/join-scheduling.test.ts +197 -0
- package/src/runner/llm-adapter-runtime.test.ts +209 -0
- package/src/runner/llm-adapter.test.ts +419 -24
- package/src/runner/llm-adapter.ts +130 -26
- package/src/runner/llm-clarification.test.ts +2 -1
- package/src/runner/llm-executor.test.ts +532 -17
- package/src/runner/mcp-client-audit.test.ts +1 -2
- package/src/runner/mcp-client.ts +136 -46
- package/src/runner/mcp-manager.test.ts +4 -0
- package/src/runner/mcp-server.test.ts +58 -0
- package/src/runner/mcp-server.ts +26 -0
- package/src/runner/memoization.test.ts +190 -0
- package/src/runner/optimization-runner.ts +4 -9
- package/src/runner/quality-gate.test.ts +69 -0
- package/src/runner/reflexion.test.ts +6 -17
- package/src/runner/resource-pool.ts +102 -14
- package/src/runner/services/context-builder.ts +144 -0
- package/src/runner/services/secret-manager.ts +105 -0
- package/src/runner/services/workflow-validator.ts +131 -0
- package/src/runner/shell-executor.test.ts +28 -4
- package/src/runner/standard-tools-ast.test.ts +196 -0
- package/src/runner/standard-tools-execution.test.ts +27 -0
- package/src/runner/standard-tools-integration.test.ts +6 -10
- package/src/runner/standard-tools.ts +339 -102
- package/src/runner/step-executor.test.ts +216 -4
- package/src/runner/step-executor.ts +69 -941
- package/src/runner/stream-utils.ts +7 -3
- package/src/runner/test-harness.ts +20 -1
- package/src/runner/timeout.test.ts +10 -0
- package/src/runner/timeout.ts +11 -2
- package/src/runner/tool-integration.test.ts +1 -1
- package/src/runner/wait-step.test.ts +102 -0
- package/src/runner/workflow-runner.test.ts +208 -15
- package/src/runner/workflow-runner.ts +890 -818
- package/src/runner/workflow-scheduler.ts +75 -0
- package/src/runner/workflow-state.ts +269 -0
- package/src/runner/workflow-subflows.test.ts +13 -12
- package/src/scripts/generate-schemas.ts +16 -0
- package/src/templates/agents/explore.md +1 -0
- package/src/templates/agents/general.md +1 -0
- package/src/templates/agents/handoff-router.md +14 -0
- package/src/templates/agents/handoff-specialist.md +15 -0
- package/src/templates/agents/keystone-architect.md +13 -44
- package/src/templates/agents/my-agent.md +1 -0
- package/src/templates/agents/software-engineer.md +1 -0
- package/src/templates/agents/summarizer.md +1 -0
- package/src/templates/agents/test-agent.md +1 -0
- package/src/templates/agents/tester.md +1 -0
- package/src/templates/{basic-inputs.yaml → basics/basic-inputs.yaml} +2 -0
- package/src/templates/{basic-shell.yaml → basics/basic-shell.yaml} +4 -1
- package/src/templates/{full-feature-demo.yaml → basics/full-feature-demo.yaml} +2 -0
- package/src/templates/{stop-watch.yaml → basics/stop-watch.yaml} +1 -0
- package/src/templates/{child-rollback.yaml → control-flow/child-rollback.yaml} +1 -0
- package/src/templates/{cleanup-finally.yaml → control-flow/cleanup-finally.yaml} +1 -0
- package/src/templates/{fan-out-fan-in.yaml → control-flow/fan-out-fan-in.yaml} +3 -0
- package/src/templates/control-flow/idempotency-example.yaml +30 -0
- package/src/templates/{loop-parallel.yaml → control-flow/loop-parallel.yaml} +3 -0
- package/src/templates/{parent-rollback.yaml → control-flow/parent-rollback.yaml} +1 -0
- package/src/templates/{retry-policy.yaml → control-flow/retry-policy.yaml} +3 -0
- package/src/templates/features/artifact-example.yaml +40 -0
- package/src/templates/{engine-example.yaml → features/engine-example.yaml} +1 -0
- package/src/templates/{human-interaction.yaml → features/human-interaction.yaml} +1 -0
- package/src/templates/{llm-agent.yaml → features/llm-agent.yaml} +1 -0
- package/src/templates/{memory-service.yaml → features/memory-service.yaml} +2 -0
- package/src/templates/{robust-automation.yaml → features/robust-automation.yaml} +3 -0
- package/src/templates/features/script-example.yaml +28 -0
- package/src/templates/patterns/agent-handoff.yaml +53 -0
- package/src/templates/{approval-process.yaml → patterns/approval-process.yaml} +1 -0
- package/src/templates/{batch-processor.yaml → patterns/batch-processor.yaml} +2 -0
- package/src/templates/{composition-child.yaml → patterns/composition-child.yaml} +2 -1
- package/src/templates/patterns/composition-parent.yaml +18 -0
- package/src/templates/{data-pipeline.yaml → patterns/data-pipeline.yaml} +2 -0
- package/src/templates/{decompose-implement.yaml → scaffolding/decompose-implement.yaml} +1 -0
- package/src/templates/{decompose-problem.yaml → scaffolding/decompose-problem.yaml} +1 -0
- package/src/templates/{decompose-research.yaml → scaffolding/decompose-research.yaml} +1 -0
- package/src/templates/{decompose-review.yaml → scaffolding/decompose-review.yaml} +1 -0
- package/src/templates/{dev.yaml → scaffolding/dev.yaml} +1 -0
- package/src/templates/scaffolding/review-loop.yaml +97 -0
- package/src/templates/{scaffold-feature.yaml → scaffolding/scaffold-feature.yaml} +2 -0
- package/src/templates/{scaffold-generate.yaml → scaffolding/scaffold-generate.yaml} +1 -0
- package/src/templates/{scaffold-plan.yaml → scaffolding/scaffold-plan.yaml} +1 -0
- package/src/templates/testing/invalid.yaml +6 -0
- package/src/ui/dashboard.tsx +191 -33
- package/src/utils/auth-manager.test.ts +337 -0
- package/src/utils/auth-manager.ts +157 -61
- package/src/utils/blueprint-utils.ts +4 -6
- package/src/utils/config-loader.test.ts +2 -0
- package/src/utils/config-loader.ts +12 -3
- package/src/utils/constants.ts +76 -0
- package/src/utils/container.ts +63 -0
- package/src/utils/context-injector.test.ts +200 -0
- package/src/utils/context-injector.ts +244 -0
- package/src/utils/doc-generator.ts +85 -0
- package/src/utils/env-filter.ts +45 -0
- package/src/utils/json-parser.test.ts +12 -0
- package/src/utils/json-parser.ts +30 -5
- package/src/utils/logger.ts +12 -1
- package/src/utils/mermaid.ts +4 -0
- package/src/utils/paths.ts +52 -1
- package/src/utils/process-sandbox-worker.test.ts +46 -0
- package/src/utils/process-sandbox.ts +227 -14
- package/src/utils/redactor.test.ts +11 -6
- package/src/utils/redactor.ts +25 -9
- package/src/utils/sandbox.ts +3 -0
- package/src/runner/llm-executor.ts +0 -638
- package/src/runner/shell-executor.ts +0 -366
- package/src/templates/composition-parent.yaml +0 -14
- package/src/templates/invalid.yaml +0 -5
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
+
import { Lang, parse } from '@ast-grep/napi';
|
|
3
4
|
import type { AgentTool } from '../parser/schema';
|
|
4
5
|
import { LIMITS, TIMEOUTS } from '../utils/constants';
|
|
5
|
-
import { detectShellInjectionRisk } from './shell-executor';
|
|
6
|
+
import { detectShellInjectionRisk } from './executors/shell-executor';
|
|
6
7
|
|
|
7
8
|
export const STANDARD_TOOLS: AgentTool[] = [
|
|
8
9
|
{
|
|
@@ -76,6 +77,25 @@ export const STANDARD_TOOLS: AgentTool[] = [
|
|
|
76
77
|
content: '${{ args.content }}',
|
|
77
78
|
},
|
|
78
79
|
},
|
|
80
|
+
{
|
|
81
|
+
name: 'append_file',
|
|
82
|
+
description: 'Append content to the end of a file (creates if not exists)',
|
|
83
|
+
parameters: {
|
|
84
|
+
type: 'object',
|
|
85
|
+
properties: {
|
|
86
|
+
path: { type: 'string', description: 'Path to the file to append to' },
|
|
87
|
+
content: { type: 'string', description: 'Content to append to the file' },
|
|
88
|
+
},
|
|
89
|
+
required: ['path', 'content'],
|
|
90
|
+
},
|
|
91
|
+
execution: {
|
|
92
|
+
id: 'std_append_file',
|
|
93
|
+
type: 'file',
|
|
94
|
+
op: 'append',
|
|
95
|
+
path: '${{ args.path }}',
|
|
96
|
+
content: '${{ args.content }}',
|
|
97
|
+
},
|
|
98
|
+
},
|
|
79
99
|
{
|
|
80
100
|
name: 'list_files',
|
|
81
101
|
description: 'List files in a directory',
|
|
@@ -123,22 +143,6 @@ export const STANDARD_TOOLS: AgentTool[] = [
|
|
|
123
143
|
required: ['pattern'],
|
|
124
144
|
},
|
|
125
145
|
execution: {
|
|
126
|
-
id: 'std_search_files',
|
|
127
|
-
type: 'script',
|
|
128
|
-
run: `
|
|
129
|
-
(function() {
|
|
130
|
-
const fs = require('node:fs');
|
|
131
|
-
const path = require('node:path');
|
|
132
|
-
const { globSync } = require('glob');
|
|
133
|
-
const dir = args.dir || '.';
|
|
134
|
-
const pattern = args.pattern;
|
|
135
|
-
try {
|
|
136
|
-
return globSync(pattern, { cwd: dir, nodir: true });
|
|
137
|
-
} catch (e) {
|
|
138
|
-
throw new Error('Search failed: ' + e.message);
|
|
139
|
-
}
|
|
140
|
-
})();
|
|
141
|
-
`,
|
|
142
146
|
allowInsecure: true,
|
|
143
147
|
},
|
|
144
148
|
},
|
|
@@ -162,106 +166,147 @@ export const STANDARD_TOOLS: AgentTool[] = [
|
|
|
162
166
|
id: 'std_search_content',
|
|
163
167
|
type: 'script',
|
|
164
168
|
run: `
|
|
165
|
-
(function() {
|
|
169
|
+
(async function() {
|
|
166
170
|
const fs = require('node:fs');
|
|
167
171
|
const path = require('node:path');
|
|
168
172
|
const { globSync } = require('glob');
|
|
173
|
+
// Try to request worker_threads. If not available, we might fall back or fail safely.
|
|
174
|
+
// Standard Node environment carries it.
|
|
175
|
+
const { Worker } = require('node:worker_threads');
|
|
176
|
+
|
|
169
177
|
const dir = args.dir || '.';
|
|
170
178
|
const pattern = args.pattern || '**/*';
|
|
171
179
|
const query = args.query;
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
} catch {
|
|
176
|
-
return null;
|
|
177
|
-
}
|
|
178
|
-
})();
|
|
179
|
-
const maxQueryLength = ${LIMITS.MAX_SEARCH_QUERY_LENGTH};
|
|
180
|
-
const maxPatternLength = ${LIMITS.MAX_REGEX_PATTERN_LENGTH};
|
|
181
|
-
const maxResults = ${LIMITS.MAX_SEARCH_RESULTS};
|
|
182
|
-
const maxFileBytes = ${LIMITS.MAX_SEARCH_FILE_BYTES};
|
|
183
|
-
const maxLineLength = ${LIMITS.MAX_SEARCH_LINE_LENGTH};
|
|
184
|
-
const regexTimeoutMs = ${TIMEOUTS.REGEX_TIMEOUT_MS};
|
|
180
|
+
|
|
181
|
+
const LIMITS = ${JSON.stringify(LIMITS)};
|
|
182
|
+
const TIMEOUTS = ${JSON.stringify(TIMEOUTS)};
|
|
185
183
|
|
|
186
|
-
if (query.length >
|
|
187
|
-
throw new Error('Search query exceeds maximum length of ' +
|
|
184
|
+
if (query.length > LIMITS.MAX_SEARCH_QUERY_LENGTH) {
|
|
185
|
+
throw new Error('Search query exceeds maximum length of ' + LIMITS.MAX_SEARCH_QUERY_LENGTH + ' characters');
|
|
188
186
|
}
|
|
189
187
|
|
|
190
188
|
const isRegex = query.startsWith('/') && query.endsWith('/') && query.length > 1;
|
|
191
|
-
let regex;
|
|
192
|
-
let normalizedQuery = '';
|
|
193
|
-
let regexDeadline;
|
|
194
|
-
|
|
195
|
-
// ReDoS protection: detect dangerous regex patterns
|
|
196
|
-
if (isRegex) {
|
|
197
|
-
const pattern = query.slice(1, -1);
|
|
198
|
-
if (pattern.length > maxPatternLength) {
|
|
199
|
-
throw new Error('Regex pattern exceeds maximum length of ' + maxPatternLength + ' characters');
|
|
200
|
-
}
|
|
201
|
-
// Detect common ReDoS patterns: nested quantifiers, overlapping alternations
|
|
202
|
-
const dangerousPatterns = [
|
|
203
|
-
/\\([^)]*(?:[+*]|\\{\\d+(?:,\\d*)?\\})[^)]*\\)(?:[+*]|\\{\\d+(?:,\\d*)?\\})/, // (x+)+, (x{1,3})*
|
|
204
|
-
/\\([^)]*\\|[^)]*\\)(?:[+*]|\\{\\d+(?:,\\d*)?\\})/, // (a|aa)+
|
|
205
|
-
/(?:[+*]|\\{\\d+(?:,\\d*)?\\}){2,}/, // consecutive quantifiers
|
|
206
|
-
];
|
|
207
|
-
if (dangerousPatterns.some(p => p.test(pattern))) {
|
|
208
|
-
throw new Error('Regex pattern contains potentially dangerous constructs (possible ReDoS). Simplify the pattern.');
|
|
209
|
-
}
|
|
210
189
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
190
|
+
// If NOT regex, use simple main-thread search (safe)
|
|
191
|
+
if (!isRegex) {
|
|
192
|
+
const normalizedQuery = query.toLowerCase();
|
|
193
|
+
const files = globSync(pattern, { cwd: dir, nodir: true });
|
|
194
|
+
const results = [];
|
|
195
|
+
for (const file of files) {
|
|
196
|
+
const fullPath = path.join(dir, file);
|
|
197
|
+
try {
|
|
198
|
+
const stat = fs.statSync(fullPath);
|
|
199
|
+
if (stat.size > LIMITS.MAX_SEARCH_FILE_BYTES) continue;
|
|
200
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
201
|
+
const lines = content.split('\\n');
|
|
202
|
+
for (let i = 0; i < lines.length; i++) {
|
|
203
|
+
const line = lines[i];
|
|
204
|
+
if (line.length > LIMITS.MAX_SEARCH_LINE_LENGTH) continue;
|
|
205
|
+
if (line.toLowerCase().includes(normalizedQuery)) {
|
|
206
|
+
results.push({
|
|
207
|
+
file,
|
|
208
|
+
line: i + 1,
|
|
209
|
+
content: line.trim()
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
if (results.length >= LIMITS.MAX_SEARCH_RESULTS) break;
|
|
213
|
+
}
|
|
214
|
+
} catch {}
|
|
215
|
+
if (results.length >= LIMITS.MAX_SEARCH_RESULTS) break;
|
|
216
|
+
}
|
|
217
|
+
return results;
|
|
219
218
|
}
|
|
220
|
-
|
|
219
|
+
|
|
220
|
+
// For REGEX, use a Worker to handle ReDoS/Timeouts
|
|
221
|
+
// We pass the files list to the worker so it does the heavy lifting
|
|
221
222
|
const files = globSync(pattern, { cwd: dir, nodir: true });
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
let stat;
|
|
229
|
-
try {
|
|
230
|
-
stat = fs.statSync(fullPath);
|
|
231
|
-
} catch {
|
|
232
|
-
continue;
|
|
233
|
-
}
|
|
234
|
-
if (stat.size > maxFileBytes) {
|
|
235
|
-
continue;
|
|
236
|
-
}
|
|
237
|
-
let content;
|
|
223
|
+
|
|
224
|
+
const workerScript = \`
|
|
225
|
+
const { parentPort, workerData } = require('node:worker_threads');
|
|
226
|
+
const fs = require('node:fs');
|
|
227
|
+
const path = require('node:path');
|
|
228
|
+
|
|
238
229
|
try {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
continue;
|
|
230
|
+
const { files, dir, query, LIMITS } = workerData;
|
|
231
|
+
let Re2;
|
|
232
|
+
try { Re2 = require('re2'); } catch {}
|
|
233
|
+
|
|
234
|
+
const patternStr = query.slice(1, -1);
|
|
235
|
+
let regex;
|
|
236
|
+
try {
|
|
237
|
+
regex = Re2 ? new Re2(patternStr) : new RegExp(patternStr);
|
|
238
|
+
} catch (e) {
|
|
239
|
+
parentPort.postMessage({ error: 'Invalid regex: ' + e.message });
|
|
240
|
+
process.exit(0);
|
|
251
241
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
242
|
+
|
|
243
|
+
const results = [];
|
|
244
|
+
|
|
245
|
+
for (const file of files) {
|
|
246
|
+
const fullPath = path.join(dir, file);
|
|
247
|
+
try {
|
|
248
|
+
const stat = fs.statSync(fullPath);
|
|
249
|
+
if (stat.size > LIMITS.MAX_SEARCH_FILE_BYTES) continue;
|
|
250
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
251
|
+
const lines = content.split('\\n');
|
|
252
|
+
for (let i = 0; i < lines.length; i++) {
|
|
253
|
+
const line = lines[i];
|
|
254
|
+
if (line.length > LIMITS.MAX_SEARCH_LINE_LENGTH) continue;
|
|
255
|
+
|
|
256
|
+
// This matching is interruptible by thread termination
|
|
257
|
+
if (regex.test(line)) {
|
|
258
|
+
results.push({
|
|
259
|
+
file,
|
|
260
|
+
line: i + 1,
|
|
261
|
+
content: line.trim()
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
if (results.length >= LIMITS.MAX_SEARCH_RESULTS) break;
|
|
265
|
+
}
|
|
266
|
+
} catch {}
|
|
267
|
+
if (results.length >= LIMITS.MAX_SEARCH_RESULTS) break;
|
|
259
268
|
}
|
|
260
|
-
|
|
269
|
+
|
|
270
|
+
parentPort.postMessage({ results });
|
|
271
|
+
} catch (err) {
|
|
272
|
+
parentPort.postMessage({ error: err.message });
|
|
261
273
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
return
|
|
274
|
+
\`;
|
|
275
|
+
|
|
276
|
+
return new Promise((resolve, reject) => {
|
|
277
|
+
const worker = new Worker(workerScript, {
|
|
278
|
+
eval: true,
|
|
279
|
+
workerData: { files, dir, query, LIMITS }
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// Hard timeout
|
|
283
|
+
const timeout = setTimeout(() => {
|
|
284
|
+
worker.terminate();
|
|
285
|
+
reject(new Error('Regex search timed out (possible ReDoS or large working set).'));
|
|
286
|
+
}, TIMEOUTS.REGEX_TIMEOUT_MS);
|
|
287
|
+
|
|
288
|
+
worker.on('message', (msg) => {
|
|
289
|
+
clearTimeout(timeout);
|
|
290
|
+
if (msg.error) {
|
|
291
|
+
reject(new Error(msg.error));
|
|
292
|
+
} else {
|
|
293
|
+
resolve(msg.results);
|
|
294
|
+
}
|
|
295
|
+
worker.terminate();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
worker.on('error', (err) => {
|
|
299
|
+
clearTimeout(timeout);
|
|
300
|
+
reject(err);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
worker.on('exit', (code) => {
|
|
304
|
+
if (code !== 0) {
|
|
305
|
+
clearTimeout(timeout);
|
|
306
|
+
reject(new Error(\`Worker stopped with exit code \${code}\`));
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
});
|
|
265
310
|
})();
|
|
266
311
|
`,
|
|
267
312
|
allowInsecure: true,
|
|
@@ -285,6 +330,189 @@ export const STANDARD_TOOLS: AgentTool[] = [
|
|
|
285
330
|
dir: '${{ args.dir }}',
|
|
286
331
|
},
|
|
287
332
|
},
|
|
333
|
+
{
|
|
334
|
+
name: 'ast_grep_search',
|
|
335
|
+
description:
|
|
336
|
+
'Search for structural code patterns using AST pattern matching. More precise than regex for code refactoring.',
|
|
337
|
+
parameters: {
|
|
338
|
+
type: 'object',
|
|
339
|
+
properties: {
|
|
340
|
+
pattern: {
|
|
341
|
+
type: 'string',
|
|
342
|
+
description: 'AST-grep pattern to search for, e.g. "console.log($A)"',
|
|
343
|
+
},
|
|
344
|
+
language: {
|
|
345
|
+
type: 'string',
|
|
346
|
+
description: 'Programming language (javascript, typescript, python, rust, go, etc.)',
|
|
347
|
+
default: 'typescript',
|
|
348
|
+
},
|
|
349
|
+
paths: {
|
|
350
|
+
type: 'array',
|
|
351
|
+
items: { type: 'string' },
|
|
352
|
+
description: 'File paths to search in',
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
required: ['pattern', 'paths'],
|
|
356
|
+
},
|
|
357
|
+
execution: {
|
|
358
|
+
id: 'std_ast_grep_search',
|
|
359
|
+
type: 'script',
|
|
360
|
+
run: `
|
|
361
|
+
(function() {
|
|
362
|
+
const fs = require('node:fs');
|
|
363
|
+
const path = require('node:path');
|
|
364
|
+
const { Lang, parse } = require('@ast-grep/napi');
|
|
365
|
+
|
|
366
|
+
const pattern = args.pattern;
|
|
367
|
+
const language = args.language || 'typescript';
|
|
368
|
+
const paths = args.paths || [];
|
|
369
|
+
|
|
370
|
+
const langMap = {
|
|
371
|
+
javascript: Lang.JavaScript,
|
|
372
|
+
typescript: Lang.TypeScript,
|
|
373
|
+
tsx: Lang.Tsx,
|
|
374
|
+
python: Lang.Python,
|
|
375
|
+
rust: Lang.Rust,
|
|
376
|
+
go: Lang.Go,
|
|
377
|
+
c: Lang.C,
|
|
378
|
+
cpp: Lang.Cpp,
|
|
379
|
+
java: Lang.Java,
|
|
380
|
+
kotlin: Lang.Kotlin,
|
|
381
|
+
swift: Lang.Swift,
|
|
382
|
+
html: Lang.Html,
|
|
383
|
+
css: Lang.Css,
|
|
384
|
+
json: Lang.Json,
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
const lang = langMap[language.toLowerCase()];
|
|
388
|
+
if (!lang) {
|
|
389
|
+
throw new Error('Unsupported language: ' + language);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const results = [];
|
|
393
|
+
for (const filePath of paths) {
|
|
394
|
+
if (!fs.existsSync(filePath)) continue;
|
|
395
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
396
|
+
const tree = parse(lang, content);
|
|
397
|
+
const root = tree.root();
|
|
398
|
+
const matches = root.findAll(pattern);
|
|
399
|
+
|
|
400
|
+
for (const match of matches) {
|
|
401
|
+
const range = match.range();
|
|
402
|
+
results.push({
|
|
403
|
+
file: filePath,
|
|
404
|
+
line: range.start.line + 1,
|
|
405
|
+
column: range.start.column + 1,
|
|
406
|
+
content: match.text(),
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return results;
|
|
411
|
+
})();
|
|
412
|
+
`,
|
|
413
|
+
allowInsecure: true,
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
name: 'ast_grep_replace',
|
|
418
|
+
description:
|
|
419
|
+
'Replace structural code patterns using AST-aware rewriting. Safer than regex for code refactoring.',
|
|
420
|
+
parameters: {
|
|
421
|
+
type: 'object',
|
|
422
|
+
properties: {
|
|
423
|
+
pattern: {
|
|
424
|
+
type: 'string',
|
|
425
|
+
description: 'AST-grep pattern to match, e.g. "console.log($A)"',
|
|
426
|
+
},
|
|
427
|
+
rewrite: { type: 'string', description: 'Replacement pattern, e.g. "logger.info($A)"' },
|
|
428
|
+
language: {
|
|
429
|
+
type: 'string',
|
|
430
|
+
description: 'Programming language (javascript, typescript, python, rust, go, etc.)',
|
|
431
|
+
default: 'typescript',
|
|
432
|
+
},
|
|
433
|
+
paths: {
|
|
434
|
+
type: 'array',
|
|
435
|
+
items: { type: 'string' },
|
|
436
|
+
description: 'File paths to apply replacements to',
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
required: ['pattern', 'rewrite', 'paths'],
|
|
440
|
+
},
|
|
441
|
+
execution: {
|
|
442
|
+
id: 'std_ast_grep_replace',
|
|
443
|
+
type: 'script',
|
|
444
|
+
run: `
|
|
445
|
+
(function() {
|
|
446
|
+
const fs = require('node:fs');
|
|
447
|
+
const path = require('node:path');
|
|
448
|
+
const { Lang, parse } = require('@ast-grep/napi');
|
|
449
|
+
|
|
450
|
+
const pattern = args.pattern;
|
|
451
|
+
const rewrite = args.rewrite;
|
|
452
|
+
const language = args.language || 'typescript';
|
|
453
|
+
const paths = args.paths || [];
|
|
454
|
+
|
|
455
|
+
const langMap = {
|
|
456
|
+
javascript: Lang.JavaScript,
|
|
457
|
+
typescript: Lang.TypeScript,
|
|
458
|
+
tsx: Lang.Tsx,
|
|
459
|
+
python: Lang.Python,
|
|
460
|
+
rust: Lang.Rust,
|
|
461
|
+
go: Lang.Go,
|
|
462
|
+
c: Lang.C,
|
|
463
|
+
cpp: Lang.Cpp,
|
|
464
|
+
java: Lang.Java,
|
|
465
|
+
kotlin: Lang.Kotlin,
|
|
466
|
+
swift: Lang.Swift,
|
|
467
|
+
html: Lang.Html,
|
|
468
|
+
css: Lang.Css,
|
|
469
|
+
json: Lang.Json,
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
const lang = langMap[language.toLowerCase()];
|
|
473
|
+
if (!lang) {
|
|
474
|
+
throw new Error('Unsupported language: ' + language);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const results = [];
|
|
478
|
+
for (const filePath of paths) {
|
|
479
|
+
if (!fs.existsSync(filePath)) continue;
|
|
480
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
481
|
+
const tree = parse(lang, content);
|
|
482
|
+
const root = tree.root();
|
|
483
|
+
const edit = root.replace(pattern, rewrite);
|
|
484
|
+
|
|
485
|
+
if (edit !== content) {
|
|
486
|
+
fs.writeFileSync(filePath, edit);
|
|
487
|
+
results.push({
|
|
488
|
+
file: filePath,
|
|
489
|
+
modified: true,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return results;
|
|
494
|
+
})();
|
|
495
|
+
`,
|
|
496
|
+
allowInsecure: true,
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
name: 'fetch',
|
|
501
|
+
description: 'Fetch content from a URL (GET request)',
|
|
502
|
+
parameters: {
|
|
503
|
+
type: 'object',
|
|
504
|
+
properties: {
|
|
505
|
+
url: { type: 'string', description: 'URL to fetch' },
|
|
506
|
+
},
|
|
507
|
+
required: ['url'],
|
|
508
|
+
},
|
|
509
|
+
execution: {
|
|
510
|
+
id: 'std_fetch',
|
|
511
|
+
type: 'request',
|
|
512
|
+
url: '${{ args.url }}',
|
|
513
|
+
method: 'GET',
|
|
514
|
+
},
|
|
515
|
+
},
|
|
288
516
|
];
|
|
289
517
|
|
|
290
518
|
/**
|
|
@@ -292,8 +520,7 @@ export const STANDARD_TOOLS: AgentTool[] = [
|
|
|
292
520
|
*/
|
|
293
521
|
export function validateStandardToolSecurity(
|
|
294
522
|
toolName: string,
|
|
295
|
-
|
|
296
|
-
args: any,
|
|
523
|
+
args: unknown,
|
|
297
524
|
options: { allowOutsideCwd?: boolean; allowInsecure?: boolean }
|
|
298
525
|
): void {
|
|
299
526
|
const cwd = process.cwd();
|
|
@@ -330,13 +557,23 @@ export function validateStandardToolSecurity(
|
|
|
330
557
|
'read_file',
|
|
331
558
|
'read_file_lines',
|
|
332
559
|
'write_file',
|
|
560
|
+
'append_file',
|
|
333
561
|
'list_files',
|
|
334
562
|
'search_files',
|
|
335
563
|
'search_content',
|
|
564
|
+
'ast_grep_search',
|
|
565
|
+
'ast_grep_replace',
|
|
336
566
|
].includes(toolName)
|
|
337
567
|
) {
|
|
338
568
|
const rawPath = args.path || args.dir || '.';
|
|
339
569
|
assertWithinCwd(rawPath);
|
|
570
|
+
|
|
571
|
+
// For AST tools, validate all paths in the array
|
|
572
|
+
if (['ast_grep_search', 'ast_grep_replace'].includes(toolName) && Array.isArray(args.paths)) {
|
|
573
|
+
for (const p of args.paths) {
|
|
574
|
+
assertWithinCwd(p);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
340
577
|
}
|
|
341
578
|
|
|
342
579
|
// 2. Check shell risk for run_command and guard working directory
|