@praneeth_54/agentdoctor 2.0.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +69 -1
- package/README.md +65 -14
- package/dist/agent/approvals.d.ts +24 -0
- package/dist/agent/approvals.js +64 -0
- package/dist/agent/chat/index.d.ts +7 -0
- package/dist/agent/chat/index.js +5 -0
- package/dist/agent/chat/memory.d.ts +44 -0
- package/dist/agent/chat/memory.js +103 -0
- package/dist/agent/chat/project-summary.d.ts +16 -0
- package/dist/agent/chat/project-summary.js +70 -0
- package/dist/agent/chat/prompts.d.ts +6 -0
- package/dist/agent/chat/prompts.js +39 -0
- package/dist/agent/chat/response.d.ts +19 -0
- package/dist/agent/chat/response.js +109 -0
- package/dist/agent/chat/service.d.ts +42 -0
- package/dist/agent/chat/service.js +252 -0
- package/dist/agent/chat/types.d.ts +48 -0
- package/dist/agent/chat/types.js +1 -0
- package/dist/agent/context/retrieve.d.ts +21 -0
- package/dist/agent/context/retrieve.js +117 -0
- package/dist/agent/context/truth.d.ts +6 -0
- package/dist/agent/context/truth.js +19 -0
- package/dist/agent/context/types.d.ts +23 -0
- package/dist/agent/context/types.js +4 -0
- package/dist/agent/index.d.ts +26 -0
- package/dist/agent/index.js +14 -0
- package/dist/agent/loop.d.ts +51 -0
- package/dist/agent/loop.js +222 -0
- package/dist/agent/modes.d.ts +18 -0
- package/dist/agent/modes.js +102 -0
- package/dist/agent/plan.d.ts +30 -0
- package/dist/agent/plan.js +121 -0
- package/dist/agent/runtime.d.ts +67 -0
- package/dist/agent/runtime.js +180 -0
- package/dist/agent/state.d.ts +30 -0
- package/dist/agent/state.js +95 -0
- package/dist/agent/student.d.ts +56 -0
- package/dist/agent/student.js +230 -0
- package/dist/agent/tools/execute.d.ts +18 -0
- package/dist/agent/tools/execute.js +355 -0
- package/dist/agent/tools/index.d.ts +6 -0
- package/dist/agent/tools/index.js +5 -0
- package/dist/agent/tools/registry.d.ts +7 -0
- package/dist/agent/tools/registry.js +212 -0
- package/dist/agent/tools/run.d.ts +24 -0
- package/dist/agent/tools/run.js +44 -0
- package/dist/agent/tools/types.d.ts +32 -0
- package/dist/agent/tools/types.js +10 -0
- package/dist/agent/tools/write.d.ts +24 -0
- package/dist/agent/tools/write.js +121 -0
- package/dist/agent/verify.d.ts +29 -0
- package/dist/agent/verify.js +210 -0
- package/dist/ai/config.d.ts +22 -0
- package/dist/ai/config.js +68 -0
- package/dist/ai/index.d.ts +17 -0
- package/dist/ai/index.js +52 -0
- package/dist/ai/providers/mock.d.ts +20 -0
- package/dist/ai/providers/mock.js +84 -0
- package/dist/ai/providers/none.d.ts +6 -0
- package/dist/ai/providers/none.js +23 -0
- package/dist/ai/providers/openai-compatible.d.ts +21 -0
- package/dist/ai/providers/openai-compatible.js +151 -0
- package/dist/ai/redact.d.ts +8 -0
- package/dist/ai/redact.js +21 -0
- package/dist/ai/types.d.ts +71 -0
- package/dist/ai/types.js +6 -0
- package/dist/cli/commands/agent.d.ts +27 -0
- package/dist/cli/commands/agent.js +153 -0
- package/dist/cli/commands/chat.d.ts +14 -0
- package/dist/cli/commands/chat.js +155 -0
- package/dist/cli/commands/learn.d.ts +12 -0
- package/dist/cli/commands/learn.js +107 -0
- package/dist/cli/program.js +92 -0
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/dashboard/server.d.ts +6 -0
- package/dist/dashboard/server.js +89 -1
- package/dist/enforcement/runner.d.ts +5 -0
- package/dist/enforcement/runner.js +51 -4
- package/dist/index.d.ts +13 -0
- package/dist/index.js +8 -0
- package/dist/languages/go.js +7 -2
- package/dist/languages/php.d.ts +1 -0
- package/dist/languages/php.js +11 -2
- package/dist/languages/python.d.ts +6 -2
- package/dist/languages/python.js +39 -9
- package/dist/mcp/agent/registry.d.ts +13 -0
- package/dist/mcp/agent/registry.js +234 -0
- package/dist/mcp/agentdoctor/server.js +8 -1
- package/dist/security/paths.js +43 -15
- package/package.json +1 -1
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { handleArchitectureCheckTool, handleCallGraphLookup, handleCodebaseSearch, handleDependencyLookup, handleRefactorImpactTool, handleSymbolLookup, handleTestImpactTool, } from "../../mcp/intelligence/handlers.js";
|
|
4
|
+
import { scan } from "../../core/scanner/scan.js";
|
|
5
|
+
import { analyzeChanges } from "../../core/changes/analyze.js";
|
|
6
|
+
import { discoverFiles } from "../../discovery/files.js";
|
|
7
|
+
import { PathEscapeError, resolveSafeRepoPath } from "../../security/paths.js";
|
|
8
|
+
import { resolveRepoRoot } from "../../utils/path.js";
|
|
9
|
+
import { redactSecrets } from "../../platform/security/redact.js";
|
|
10
|
+
import { summarizeProjectForChat } from "../chat/project-summary.js";
|
|
11
|
+
import { getToolSpec, riskForTool } from "./registry.js";
|
|
12
|
+
import { createFileSafe, deleteFileSafe, editFileSafe } from "./write.js";
|
|
13
|
+
import { inferTestArgv, runAgentCommand } from "./run.js";
|
|
14
|
+
import { evaluateApproval } from "../approvals.js";
|
|
15
|
+
import { modeBlocksToolCategory } from "../modes.js";
|
|
16
|
+
import { assertWorkspacePathAccess } from "../../workspace/index.js";
|
|
17
|
+
function asRecord(args) {
|
|
18
|
+
return args;
|
|
19
|
+
}
|
|
20
|
+
function str(args, key) {
|
|
21
|
+
const v = args[key];
|
|
22
|
+
return typeof v === "string" && v.trim() ? v.trim() : null;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Execute an agent tool.
|
|
26
|
+
* Write/execute tools require explicit allowWrite/allowExecute (after human approval).
|
|
27
|
+
*/
|
|
28
|
+
export async function executeAgentTool(rootInput, call, options) {
|
|
29
|
+
const started = Date.now();
|
|
30
|
+
const root = resolveRepoRoot(rootInput);
|
|
31
|
+
const risk = riskForTool(call.name);
|
|
32
|
+
const spec = getToolSpec(call.name);
|
|
33
|
+
const fail = (code, message) => ({
|
|
34
|
+
callId: call.id,
|
|
35
|
+
name: call.name,
|
|
36
|
+
ok: false,
|
|
37
|
+
data: null,
|
|
38
|
+
error: { code, message },
|
|
39
|
+
risk,
|
|
40
|
+
durationMs: Date.now() - started,
|
|
41
|
+
});
|
|
42
|
+
const ok = (data) => ({
|
|
43
|
+
callId: call.id,
|
|
44
|
+
name: call.name,
|
|
45
|
+
ok: true,
|
|
46
|
+
data: redactDeep(data),
|
|
47
|
+
risk,
|
|
48
|
+
durationMs: Date.now() - started,
|
|
49
|
+
});
|
|
50
|
+
if (!spec)
|
|
51
|
+
return fail("unknown_tool", `Unknown tool: ${call.name}`);
|
|
52
|
+
if (modeBlocksToolCategory(options?.mode, spec.category)) {
|
|
53
|
+
return fail("mode_forbidden", `Mode ${options?.mode ?? "LEARN"} does not allow ${spec.category} tools (allowWrites=false). The model cannot override this.`);
|
|
54
|
+
}
|
|
55
|
+
if (spec.category === "write" || spec.category === "execute") {
|
|
56
|
+
const gate = evaluateApproval({ action: `tool:${call.name}`, risk: risk, toolName: call.name }, { approvedByHuman: options?.approvedByHuman === true });
|
|
57
|
+
if (gate.needsHumanApproval) {
|
|
58
|
+
return fail("approval_required", gate.reason);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (spec.category === "write" && !options?.allowWrite) {
|
|
62
|
+
return fail("not_enabled", "Write tools require allowWrite after human approval.");
|
|
63
|
+
}
|
|
64
|
+
if (spec.category === "execute" && !options?.allowExecute) {
|
|
65
|
+
return fail("not_enabled", "Execute tools require allowExecute after human approval.");
|
|
66
|
+
}
|
|
67
|
+
const ensureWorkspace = (absPath) => {
|
|
68
|
+
// Repo-root isolation is already enforced by resolveSafeRepoPath.
|
|
69
|
+
// Workspace gate applies only when a multi-root WorkspaceModel is provided
|
|
70
|
+
// (avoids false denies from /var vs /private/var when workspace is null).
|
|
71
|
+
if (options?.workspace == null)
|
|
72
|
+
return null;
|
|
73
|
+
const access = assertWorkspacePathAccess({
|
|
74
|
+
workspace: options.workspace,
|
|
75
|
+
operationRoot: root,
|
|
76
|
+
targetPath: absPath,
|
|
77
|
+
});
|
|
78
|
+
if (!access.allowed) {
|
|
79
|
+
return fail("workspace_denied", access.reason);
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
};
|
|
83
|
+
try {
|
|
84
|
+
switch (call.name) {
|
|
85
|
+
case "read_file": {
|
|
86
|
+
const rel = str(call.arguments, "path");
|
|
87
|
+
if (!rel)
|
|
88
|
+
return fail("invalid_argument", "path required");
|
|
89
|
+
let abs;
|
|
90
|
+
try {
|
|
91
|
+
abs = resolveSafeRepoPath(root, rel);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (error instanceof PathEscapeError) {
|
|
95
|
+
return fail("path_escape", "path escapes repository root");
|
|
96
|
+
}
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
const denied = ensureWorkspace(abs);
|
|
100
|
+
if (denied)
|
|
101
|
+
return denied;
|
|
102
|
+
const maxBytes = typeof call.arguments.maxBytes === "number" ? call.arguments.maxBytes : 200_000;
|
|
103
|
+
const buf = await fs.readFile(abs);
|
|
104
|
+
if (buf.byteLength > maxBytes) {
|
|
105
|
+
return fail("too_large", `File exceeds ${maxBytes} bytes`);
|
|
106
|
+
}
|
|
107
|
+
const text = buf.toString("utf8");
|
|
108
|
+
return ok({
|
|
109
|
+
path: rel.split(path.sep).join("/"),
|
|
110
|
+
bytes: buf.byteLength,
|
|
111
|
+
content: redactSecrets(text).text,
|
|
112
|
+
channel: "PROJECT_DATA",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
case "list_files": {
|
|
116
|
+
const limit = typeof call.arguments.limit === "number" ? call.arguments.limit : 200;
|
|
117
|
+
const discovery = await discoverFiles({ root });
|
|
118
|
+
const files = discovery.files.slice(0, limit).map((f) => f.relativePath);
|
|
119
|
+
return ok({
|
|
120
|
+
count: discovery.files.length,
|
|
121
|
+
files,
|
|
122
|
+
truncated: discovery.files.length > limit,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
case "search_code":
|
|
126
|
+
return ok(await handleCodebaseSearch(root, asRecord(call.arguments)));
|
|
127
|
+
case "find_symbol":
|
|
128
|
+
return ok(await handleSymbolLookup(root, asRecord(call.arguments)));
|
|
129
|
+
case "find_references": {
|
|
130
|
+
const symbol = str(call.arguments, "symbol");
|
|
131
|
+
if (!symbol)
|
|
132
|
+
return fail("invalid_argument", "symbol required");
|
|
133
|
+
return ok(await handleRefactorImpactTool(root, { symbol }));
|
|
134
|
+
}
|
|
135
|
+
case "find_callers": {
|
|
136
|
+
const target = str(call.arguments, "target");
|
|
137
|
+
if (!target)
|
|
138
|
+
return fail("invalid_argument", "target required");
|
|
139
|
+
const raw = (await handleCallGraphLookup(root, { symbol: target }));
|
|
140
|
+
const edges = (raw.calls ?? []).filter((e) => e.to.includes(target));
|
|
141
|
+
return ok({ ok: raw.ok, direction: "callers", target, edges });
|
|
142
|
+
}
|
|
143
|
+
case "find_callees": {
|
|
144
|
+
const target = str(call.arguments, "target");
|
|
145
|
+
if (!target)
|
|
146
|
+
return fail("invalid_argument", "target required");
|
|
147
|
+
const raw = (await handleCallGraphLookup(root, { symbol: target }));
|
|
148
|
+
const edges = (raw.calls ?? []).filter((e) => e.from.includes(target));
|
|
149
|
+
return ok({ ok: raw.ok, direction: "callees", target, edges });
|
|
150
|
+
}
|
|
151
|
+
case "inspect_project":
|
|
152
|
+
return ok(await summarizeProjectForChat(root));
|
|
153
|
+
case "inspect_architecture":
|
|
154
|
+
return ok(await handleArchitectureCheckTool(root));
|
|
155
|
+
case "inspect_dependencies": {
|
|
156
|
+
const target = str(call.arguments, "target");
|
|
157
|
+
if (!target)
|
|
158
|
+
return fail("invalid_argument", "target required");
|
|
159
|
+
return ok(await handleDependencyLookup(root, { target }));
|
|
160
|
+
}
|
|
161
|
+
case "inspect_tests":
|
|
162
|
+
return ok(await handleTestImpactTool(root));
|
|
163
|
+
case "inspect_findings": {
|
|
164
|
+
const result = await scan({ cwd: root });
|
|
165
|
+
return ok({
|
|
166
|
+
findings: result.findings.slice(0, 50),
|
|
167
|
+
summary: result.summary,
|
|
168
|
+
agents: result.agents.map((a) => ({
|
|
169
|
+
id: a.id,
|
|
170
|
+
detected: a.detected,
|
|
171
|
+
configured: a.configured,
|
|
172
|
+
status: a.status,
|
|
173
|
+
})),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
case "inspect_git_status": {
|
|
177
|
+
const report = await analyzeChanges({ root });
|
|
178
|
+
return ok({
|
|
179
|
+
gitAvailable: report.gitAvailable,
|
|
180
|
+
files: report.files.slice(0, 100),
|
|
181
|
+
summary: report.summary,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
case "inspect_git_diff": {
|
|
185
|
+
const since = str(call.arguments, "since") ?? undefined;
|
|
186
|
+
const report = await analyzeChanges({
|
|
187
|
+
root,
|
|
188
|
+
...(since ? { since } : {}),
|
|
189
|
+
impact: true,
|
|
190
|
+
});
|
|
191
|
+
return ok({
|
|
192
|
+
gitAvailable: report.gitAvailable,
|
|
193
|
+
files: report.files.slice(0, 100),
|
|
194
|
+
summary: report.summary,
|
|
195
|
+
impact: report.impact ?? null,
|
|
196
|
+
note: "Full unified patch text is not dumped; use change analyze for evidence.",
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
case "create_file": {
|
|
200
|
+
const rel = str(call.arguments, "path");
|
|
201
|
+
const content = typeof call.arguments.content === "string" ? call.arguments.content : null;
|
|
202
|
+
if (!rel || content === null)
|
|
203
|
+
return fail("invalid_argument", "path and content required");
|
|
204
|
+
let absCreate;
|
|
205
|
+
try {
|
|
206
|
+
absCreate = resolveSafeRepoPath(root, rel);
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
if (error instanceof PathEscapeError) {
|
|
210
|
+
return fail("path_escape", "path escapes repository root");
|
|
211
|
+
}
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
const denyCreate = ensureWorkspace(absCreate);
|
|
215
|
+
if (denyCreate)
|
|
216
|
+
return denyCreate;
|
|
217
|
+
return ok(await createFileSafe(root, rel, content));
|
|
218
|
+
}
|
|
219
|
+
case "edit_file": {
|
|
220
|
+
const rel = str(call.arguments, "path");
|
|
221
|
+
if (!rel)
|
|
222
|
+
return fail("invalid_argument", "path required");
|
|
223
|
+
let absEdit;
|
|
224
|
+
try {
|
|
225
|
+
absEdit = resolveSafeRepoPath(root, rel);
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
if (error instanceof PathEscapeError) {
|
|
229
|
+
return fail("path_escape", "path escapes repository root");
|
|
230
|
+
}
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
const denyEdit = ensureWorkspace(absEdit);
|
|
234
|
+
if (denyEdit)
|
|
235
|
+
return denyEdit;
|
|
236
|
+
const editOpts = {};
|
|
237
|
+
if (typeof call.arguments.content === "string")
|
|
238
|
+
editOpts.content = call.arguments.content;
|
|
239
|
+
if (typeof call.arguments.oldContent === "string") {
|
|
240
|
+
editOpts.oldContent = call.arguments.oldContent;
|
|
241
|
+
}
|
|
242
|
+
if (typeof call.arguments.startLine === "number") {
|
|
243
|
+
editOpts.startLine = call.arguments.startLine;
|
|
244
|
+
}
|
|
245
|
+
if (typeof call.arguments.endLine === "number")
|
|
246
|
+
editOpts.endLine = call.arguments.endLine;
|
|
247
|
+
if (typeof call.arguments.replacement === "string") {
|
|
248
|
+
editOpts.replacement = call.arguments.replacement;
|
|
249
|
+
}
|
|
250
|
+
return ok(await editFileSafe(root, rel, editOpts));
|
|
251
|
+
}
|
|
252
|
+
case "delete_file": {
|
|
253
|
+
const rel = str(call.arguments, "path");
|
|
254
|
+
if (!rel)
|
|
255
|
+
return fail("invalid_argument", "path required");
|
|
256
|
+
let absDel;
|
|
257
|
+
try {
|
|
258
|
+
absDel = resolveSafeRepoPath(root, rel);
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
if (error instanceof PathEscapeError) {
|
|
262
|
+
return fail("path_escape", "path escapes repository root");
|
|
263
|
+
}
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
const denyDel = ensureWorkspace(absDel);
|
|
267
|
+
if (denyDel)
|
|
268
|
+
return denyDel;
|
|
269
|
+
return ok(await deleteFileSafe(root, rel));
|
|
270
|
+
}
|
|
271
|
+
case "run_command": {
|
|
272
|
+
const command = str(call.arguments, "command") ?? undefined;
|
|
273
|
+
const argv = Array.isArray(call.arguments.argv)
|
|
274
|
+
? call.arguments.argv.filter((x) => typeof x === "string")
|
|
275
|
+
: undefined;
|
|
276
|
+
if (!command && (!argv || argv.length === 0)) {
|
|
277
|
+
return fail("invalid_argument", "command or argv required");
|
|
278
|
+
}
|
|
279
|
+
const result = await runAgentCommand({
|
|
280
|
+
root,
|
|
281
|
+
...(command ? { command } : {}),
|
|
282
|
+
...(argv ? { argv } : {}),
|
|
283
|
+
execute: true,
|
|
284
|
+
});
|
|
285
|
+
if (result.blocked) {
|
|
286
|
+
return fail("blocked", result.reason);
|
|
287
|
+
}
|
|
288
|
+
if (!result.ok) {
|
|
289
|
+
return fail(result.decision === "require-approval" ? "policy_approval" : "command_failed", result.reason || result.notice);
|
|
290
|
+
}
|
|
291
|
+
return ok(result);
|
|
292
|
+
}
|
|
293
|
+
case "run_tests": {
|
|
294
|
+
const command = str(call.arguments, "command");
|
|
295
|
+
let argv;
|
|
296
|
+
if (command) {
|
|
297
|
+
// fall through to runAgentCommand with command string
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
try {
|
|
301
|
+
const pkgRaw = await fs.readFile(path.join(root, "package.json"), "utf8");
|
|
302
|
+
argv = inferTestArgv(JSON.parse(pkgRaw)) ?? undefined;
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
argv = undefined;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (!command && !argv) {
|
|
309
|
+
return fail("no_test_runner", "Could not infer test command; pass command explicitly (e.g. npm test).");
|
|
310
|
+
}
|
|
311
|
+
const result = await runAgentCommand({
|
|
312
|
+
root,
|
|
313
|
+
...(command ? { command } : {}),
|
|
314
|
+
...(argv ? { argv } : {}),
|
|
315
|
+
execute: true,
|
|
316
|
+
timeoutMs: 120_000,
|
|
317
|
+
});
|
|
318
|
+
if (result.blocked) {
|
|
319
|
+
return fail("blocked", result.reason);
|
|
320
|
+
}
|
|
321
|
+
if (!result.ok) {
|
|
322
|
+
return fail(result.decision === "require-approval" ? "policy_approval" : "command_failed", result.reason || result.notice);
|
|
323
|
+
}
|
|
324
|
+
return ok(result);
|
|
325
|
+
}
|
|
326
|
+
default: {
|
|
327
|
+
const _exhaustive = call.name;
|
|
328
|
+
return fail("unknown_tool", `Unhandled tool: ${_exhaustive}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
if (error instanceof PathEscapeError) {
|
|
334
|
+
return fail("path_escape", "path escapes repository root");
|
|
335
|
+
}
|
|
336
|
+
return fail("tool_error", error instanceof Error ? error.message : String(error));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
function redactDeep(value) {
|
|
340
|
+
if (typeof value === "string")
|
|
341
|
+
return redactSecrets(value).text;
|
|
342
|
+
if (Array.isArray(value))
|
|
343
|
+
return value.map(redactDeep);
|
|
344
|
+
if (value && typeof value === "object") {
|
|
345
|
+
const out = {};
|
|
346
|
+
for (const [k, v] of Object.entries(value)) {
|
|
347
|
+
out[k] = redactDeep(v);
|
|
348
|
+
}
|
|
349
|
+
return out;
|
|
350
|
+
}
|
|
351
|
+
return value;
|
|
352
|
+
}
|
|
353
|
+
export function isReadTool(name) {
|
|
354
|
+
return getToolSpec(name)?.category === "read";
|
|
355
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type { AgentToolName, AgentToolSpec, AgentToolCall, AgentToolResult, AgentRiskLevel, } from "./types.js";
|
|
2
|
+
export { newToolCall } from "./types.js";
|
|
3
|
+
export { listAgentToolSpecs, getToolSpec, riskForTool } from "./registry.js";
|
|
4
|
+
export { executeAgentTool, isReadTool } from "./execute.js";
|
|
5
|
+
export { createFileSafe, editFileSafe, deleteFileSafe, buildUnifiedDiff } from "./write.js";
|
|
6
|
+
export { runAgentCommand, inferTestArgv } from "./run.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { newToolCall } from "./types.js";
|
|
2
|
+
export { listAgentToolSpecs, getToolSpec, riskForTool } from "./registry.js";
|
|
3
|
+
export { executeAgentTool, isReadTool } from "./execute.js";
|
|
4
|
+
export { createFileSafe, editFileSafe, deleteFileSafe, buildUnifiedDiff } from "./write.js";
|
|
5
|
+
export { runAgentCommand, inferTestArgv } from "./run.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AgentRiskLevel, AgentToolName, AgentToolSpec } from "./types.js";
|
|
2
|
+
export declare function listAgentToolSpecs(options?: {
|
|
3
|
+
includeWrite?: boolean;
|
|
4
|
+
includeExecute?: boolean;
|
|
5
|
+
}): AgentToolSpec[];
|
|
6
|
+
export declare function getToolSpec(name: AgentToolName): AgentToolSpec | undefined;
|
|
7
|
+
export declare function riskForTool(name: AgentToolName): AgentRiskLevel;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
const READ_SPECS = [
|
|
2
|
+
{
|
|
3
|
+
name: "read_file",
|
|
4
|
+
description: "READ: Read a UTF-8 text file inside the workspace (path-safe).",
|
|
5
|
+
risk: "LOW",
|
|
6
|
+
category: "read",
|
|
7
|
+
parameters: {
|
|
8
|
+
type: "object",
|
|
9
|
+
properties: { path: { type: "string" }, maxBytes: { type: "number" } },
|
|
10
|
+
required: ["path"],
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: "list_files",
|
|
15
|
+
description: "READ: List repository files (bounded discovery).",
|
|
16
|
+
risk: "LOW",
|
|
17
|
+
category: "read",
|
|
18
|
+
parameters: {
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: { limit: { type: "number" } },
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: "search_code",
|
|
25
|
+
description: "READ: Search graph nodes by label/path substring.",
|
|
26
|
+
risk: "LOW",
|
|
27
|
+
category: "read",
|
|
28
|
+
parameters: {
|
|
29
|
+
type: "object",
|
|
30
|
+
properties: { query: { type: "string" } },
|
|
31
|
+
required: ["query"],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "find_symbol",
|
|
36
|
+
description: "READ: Lookup symbols by name.",
|
|
37
|
+
risk: "LOW",
|
|
38
|
+
category: "read",
|
|
39
|
+
parameters: {
|
|
40
|
+
type: "object",
|
|
41
|
+
properties: { name: { type: "string" } },
|
|
42
|
+
required: ["name"],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: "find_references",
|
|
47
|
+
description: "READ: Refactor/rename impact for a symbol (best-effort references).",
|
|
48
|
+
risk: "LOW",
|
|
49
|
+
category: "read",
|
|
50
|
+
parameters: {
|
|
51
|
+
type: "object",
|
|
52
|
+
properties: { symbol: { type: "string" } },
|
|
53
|
+
required: ["symbol"],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: "find_callers",
|
|
58
|
+
description: "READ: Call-graph callers for a symbol/path.",
|
|
59
|
+
risk: "LOW",
|
|
60
|
+
category: "read",
|
|
61
|
+
parameters: {
|
|
62
|
+
type: "object",
|
|
63
|
+
properties: { target: { type: "string" } },
|
|
64
|
+
required: ["target"],
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "find_callees",
|
|
69
|
+
description: "READ: Call-graph callees for a symbol/path.",
|
|
70
|
+
risk: "LOW",
|
|
71
|
+
category: "read",
|
|
72
|
+
parameters: {
|
|
73
|
+
type: "object",
|
|
74
|
+
properties: { target: { type: "string" } },
|
|
75
|
+
required: ["target"],
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "inspect_project",
|
|
80
|
+
description: "READ: Project fingerprint summary.",
|
|
81
|
+
risk: "LOW",
|
|
82
|
+
category: "read",
|
|
83
|
+
parameters: { type: "object", properties: {} },
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: "inspect_architecture",
|
|
87
|
+
description: "READ: Architecture contract check + C4 overview.",
|
|
88
|
+
risk: "LOW",
|
|
89
|
+
category: "read",
|
|
90
|
+
parameters: { type: "object", properties: {} },
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: "inspect_dependencies",
|
|
94
|
+
description: "READ: Dependency edges for a target path/symbol.",
|
|
95
|
+
risk: "LOW",
|
|
96
|
+
category: "read",
|
|
97
|
+
parameters: {
|
|
98
|
+
type: "object",
|
|
99
|
+
properties: { target: { type: "string" } },
|
|
100
|
+
required: ["target"],
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: "inspect_tests",
|
|
105
|
+
description: "READ: Test-impact analysis for current changes (heuristic).",
|
|
106
|
+
risk: "LOW",
|
|
107
|
+
category: "read",
|
|
108
|
+
parameters: { type: "object", properties: {} },
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: "inspect_findings",
|
|
112
|
+
description: "READ: AgentDoctor safety scan findings.",
|
|
113
|
+
risk: "LOW",
|
|
114
|
+
category: "read",
|
|
115
|
+
parameters: { type: "object", properties: {} },
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: "inspect_git_status",
|
|
119
|
+
description: "READ: Git change status (staged/unstaged/untracked).",
|
|
120
|
+
risk: "LOW",
|
|
121
|
+
category: "read",
|
|
122
|
+
parameters: { type: "object", properties: {} },
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: "inspect_git_diff",
|
|
126
|
+
description: "READ: Change set summary (not full patch dump).",
|
|
127
|
+
risk: "LOW",
|
|
128
|
+
category: "read",
|
|
129
|
+
parameters: { type: "object", properties: { since: { type: "string" } } },
|
|
130
|
+
},
|
|
131
|
+
];
|
|
132
|
+
const WRITE_SPECS = [
|
|
133
|
+
{
|
|
134
|
+
name: "create_file",
|
|
135
|
+
description: "WRITE: Create a new file inside the workspace (requires approval).",
|
|
136
|
+
risk: "MEDIUM",
|
|
137
|
+
category: "write",
|
|
138
|
+
parameters: {
|
|
139
|
+
type: "object",
|
|
140
|
+
properties: { path: { type: "string" }, content: { type: "string" } },
|
|
141
|
+
required: ["path", "content"],
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: "edit_file",
|
|
146
|
+
description: "WRITE: Edit a file (exact replace, line range, or full content) with diff.",
|
|
147
|
+
risk: "MEDIUM",
|
|
148
|
+
category: "write",
|
|
149
|
+
parameters: {
|
|
150
|
+
type: "object",
|
|
151
|
+
properties: {
|
|
152
|
+
path: { type: "string" },
|
|
153
|
+
content: { type: "string" },
|
|
154
|
+
oldContent: { type: "string" },
|
|
155
|
+
startLine: { type: "number" },
|
|
156
|
+
endLine: { type: "number" },
|
|
157
|
+
replacement: { type: "string" },
|
|
158
|
+
},
|
|
159
|
+
required: ["path"],
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: "delete_file",
|
|
164
|
+
description: "WRITE: Delete a file inside the workspace (HIGH risk).",
|
|
165
|
+
risk: "HIGH",
|
|
166
|
+
category: "write",
|
|
167
|
+
parameters: {
|
|
168
|
+
type: "object",
|
|
169
|
+
properties: { path: { type: "string" } },
|
|
170
|
+
required: ["path"],
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
];
|
|
174
|
+
const EXEC_SPECS = [
|
|
175
|
+
{
|
|
176
|
+
name: "run_command",
|
|
177
|
+
description: "EXECUTE: Run a controlled argv command (policy + shell=false).",
|
|
178
|
+
risk: "MEDIUM",
|
|
179
|
+
category: "execute",
|
|
180
|
+
parameters: {
|
|
181
|
+
type: "object",
|
|
182
|
+
properties: {
|
|
183
|
+
command: { type: "string" },
|
|
184
|
+
argv: { type: "array", items: { type: "string" } },
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: "run_tests",
|
|
190
|
+
description: "EXECUTE: Run project tests via controlled runner.",
|
|
191
|
+
risk: "MEDIUM",
|
|
192
|
+
category: "execute",
|
|
193
|
+
parameters: {
|
|
194
|
+
type: "object",
|
|
195
|
+
properties: { command: { type: "string" } },
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
];
|
|
199
|
+
export function listAgentToolSpecs(options) {
|
|
200
|
+
const out = [...READ_SPECS];
|
|
201
|
+
if (options?.includeWrite)
|
|
202
|
+
out.push(...WRITE_SPECS);
|
|
203
|
+
if (options?.includeExecute)
|
|
204
|
+
out.push(...EXEC_SPECS);
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
export function getToolSpec(name) {
|
|
208
|
+
return listAgentToolSpecs({ includeWrite: true, includeExecute: true }).find((t) => t.name === name);
|
|
209
|
+
}
|
|
210
|
+
export function riskForTool(name) {
|
|
211
|
+
return getToolSpec(name)?.risk ?? "CRITICAL";
|
|
212
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface AgentCommandResult {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
blocked: boolean;
|
|
4
|
+
command: string;
|
|
5
|
+
notice: string;
|
|
6
|
+
stdout?: string;
|
|
7
|
+
stderr?: string;
|
|
8
|
+
exitCode?: number | null;
|
|
9
|
+
durationMs?: number;
|
|
10
|
+
decision: string;
|
|
11
|
+
reason: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Execute a project command only via the existing controlled runner (shell=false by default).
|
|
15
|
+
*/
|
|
16
|
+
export declare function runAgentCommand(options: {
|
|
17
|
+
root: string;
|
|
18
|
+
command?: string;
|
|
19
|
+
argv?: string[];
|
|
20
|
+
execute?: boolean;
|
|
21
|
+
timeoutMs?: number;
|
|
22
|
+
}): Promise<AgentCommandResult>;
|
|
23
|
+
/** Detect common project test runners from package manifests / markers (best-effort). */
|
|
24
|
+
export declare function inferTestArgv(packageJson: unknown): string[] | null;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { runControlledCommand } from "../../enforcement/runner.js";
|
|
2
|
+
import { resolveRepoRoot } from "../../utils/path.js";
|
|
3
|
+
/**
|
|
4
|
+
* Execute a project command only via the existing controlled runner (shell=false by default).
|
|
5
|
+
*/
|
|
6
|
+
export async function runAgentCommand(options) {
|
|
7
|
+
const root = resolveRepoRoot(options.root);
|
|
8
|
+
const result = await runControlledCommand({
|
|
9
|
+
root,
|
|
10
|
+
...(options.command !== undefined ? { command: options.command } : {}),
|
|
11
|
+
...(options.argv !== undefined ? { argv: options.argv } : {}),
|
|
12
|
+
executeIfAllowed: options.execute !== false,
|
|
13
|
+
useShell: false,
|
|
14
|
+
...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
|
|
15
|
+
});
|
|
16
|
+
const decision = result.decision.decision;
|
|
17
|
+
const blocked = decision === "block" || decision === "deny-network";
|
|
18
|
+
const exitOk = result.exitCode === 0;
|
|
19
|
+
const executed = result.decision.executionStatus === "executed";
|
|
20
|
+
const allowed = decision === "allow";
|
|
21
|
+
return {
|
|
22
|
+
ok: allowed && executed && exitOk,
|
|
23
|
+
blocked,
|
|
24
|
+
command: options.command ?? (options.argv ?? []).join(" "),
|
|
25
|
+
notice: result.notice,
|
|
26
|
+
...(result.stdout !== undefined ? { stdout: result.stdout } : {}),
|
|
27
|
+
...(result.stderr !== undefined ? { stderr: result.stderr } : {}),
|
|
28
|
+
...(result.exitCode !== undefined ? { exitCode: result.exitCode } : {}),
|
|
29
|
+
...(result.durationMs !== undefined ? { durationMs: result.durationMs } : {}),
|
|
30
|
+
decision,
|
|
31
|
+
reason: result.decision.reason,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** Detect common project test runners from package manifests / markers (best-effort). */
|
|
35
|
+
export function inferTestArgv(packageJson) {
|
|
36
|
+
if (!packageJson || typeof packageJson !== "object")
|
|
37
|
+
return null;
|
|
38
|
+
const scripts = packageJson.scripts;
|
|
39
|
+
if (scripts?.test) {
|
|
40
|
+
// Prefer npm test when package.json present — argv form for shell=false
|
|
41
|
+
return ["npm", "test"];
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|