@freesyntax/notch-cli 0.5.17 → 0.5.20
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/dist/apply-patch-D5PDUXUC.js +14 -0
- package/dist/{auth-S3FIB42I.js → auth-JQX6MHJG.js} +0 -1
- package/dist/builtins/archimedes.toml +18 -0
- package/dist/builtins/awaiter.toml +18 -0
- package/dist/builtins/euclid.toml +18 -0
- package/dist/builtins/hypatia.toml +18 -0
- package/dist/builtins/kepler.toml +18 -0
- package/dist/builtins/plato.toml +18 -0
- package/dist/builtins/ptolemy.toml +18 -0
- package/dist/builtins/pythagoras.toml +18 -0
- package/dist/chunk-3QUV4JEX.js +162 -0
- package/dist/chunk-6CZCFY6H.js +98 -0
- package/dist/chunk-6U3ZAGYA.js +38 -0
- package/dist/chunk-C4CPDDMN.js +246 -0
- package/dist/chunk-CQMAVWLJ.js +134 -0
- package/dist/chunk-FAULT7VE.js +139 -0
- package/dist/chunk-FFB7GK3Y.js +72 -0
- package/dist/chunk-GBZGR6ID.js +174 -0
- package/dist/chunk-KZAS754V.js +118 -0
- package/dist/chunk-O3WZW7GS.js +35 -0
- package/dist/chunk-TH6GKC7E.js +315 -0
- package/dist/chunk-UR4XL6OM.js +104 -0
- package/dist/chunk-W4FAGQFL.js +171 -0
- package/dist/chunk-YAYPQTOU.js +53 -0
- package/dist/chunk-YBYF7L4A.js +2607 -0
- package/dist/{compression-LPFNGAV6.js → compression-UTB2Y4BB.js} +0 -1
- package/dist/edit-JEFEK43H.js +6 -0
- package/dist/git-5T5TSQTX.js +6 -0
- package/dist/github-DWRGWX6U.js +6 -0
- package/dist/glob-BI3P4C7Q.js +6 -0
- package/dist/grep-VZ3I5GNW.js +6 -0
- package/dist/index.js +4398 -3447
- package/dist/lsp-UPY6I3L7.js +6 -0
- package/dist/notebook-FXJBTSPA.js +6 -0
- package/dist/plugins-OG2P75K5.js +6 -0
- package/dist/read-OVJG2XKW.js +6 -0
- package/dist/server-W7FRCVRZ.js +1477 -0
- package/dist/shell-4X545EVN.js +6 -0
- package/dist/task-OS3E5F3X.js +10 -0
- package/dist/tools-Q7CDHB4K.js +30 -0
- package/dist/web-fetch-KNIV3Z3W.js +6 -0
- package/dist/write-NNHLOTYK.js +6 -0
- package/package.json +5 -4
- package/dist/chunk-3RG5ZIWI.js +0 -10
|
@@ -0,0 +1,2607 @@
|
|
|
1
|
+
import {
|
|
2
|
+
grepTool
|
|
3
|
+
} from "./chunk-6CZCFY6H.js";
|
|
4
|
+
import {
|
|
5
|
+
globTool
|
|
6
|
+
} from "./chunk-6U3ZAGYA.js";
|
|
7
|
+
import {
|
|
8
|
+
webFetchTool
|
|
9
|
+
} from "./chunk-FFB7GK3Y.js";
|
|
10
|
+
import {
|
|
11
|
+
githubTool
|
|
12
|
+
} from "./chunk-GBZGR6ID.js";
|
|
13
|
+
import {
|
|
14
|
+
lspTool
|
|
15
|
+
} from "./chunk-TH6GKC7E.js";
|
|
16
|
+
import {
|
|
17
|
+
notebookTool
|
|
18
|
+
} from "./chunk-KZAS754V.js";
|
|
19
|
+
import {
|
|
20
|
+
taskTool
|
|
21
|
+
} from "./chunk-UR4XL6OM.js";
|
|
22
|
+
import {
|
|
23
|
+
pluginManager
|
|
24
|
+
} from "./chunk-3QUV4JEX.js";
|
|
25
|
+
import {
|
|
26
|
+
readTool
|
|
27
|
+
} from "./chunk-CQMAVWLJ.js";
|
|
28
|
+
import {
|
|
29
|
+
writeTool
|
|
30
|
+
} from "./chunk-O3WZW7GS.js";
|
|
31
|
+
import {
|
|
32
|
+
editTool
|
|
33
|
+
} from "./chunk-YAYPQTOU.js";
|
|
34
|
+
import {
|
|
35
|
+
applyPatchTool
|
|
36
|
+
} from "./chunk-C4CPDDMN.js";
|
|
37
|
+
import {
|
|
38
|
+
shellTool
|
|
39
|
+
} from "./chunk-W4FAGQFL.js";
|
|
40
|
+
import {
|
|
41
|
+
gitTool
|
|
42
|
+
} from "./chunk-FAULT7VE.js";
|
|
43
|
+
|
|
44
|
+
// src/tools/index.ts
|
|
45
|
+
import { tool } from "ai";
|
|
46
|
+
|
|
47
|
+
// src/tools/code-mode.ts
|
|
48
|
+
import vm from "vm";
|
|
49
|
+
import { z } from "zod";
|
|
50
|
+
var DEFAULT_YIELD_TIME_MS = 3e4;
|
|
51
|
+
var MAX_YIELD_TIME_MS = 3e5;
|
|
52
|
+
var DEFAULT_MAX_OUTPUT_TOKENS = 1e3;
|
|
53
|
+
var CHARS_PER_TOKEN = 4;
|
|
54
|
+
var PRAGMA_PREFIX = "// @exec:";
|
|
55
|
+
var EXCLUDED_TOOLS = /* @__PURE__ */ new Set(["code_exec", "code_wait"]);
|
|
56
|
+
var CELLS = /* @__PURE__ */ new Map();
|
|
57
|
+
function nextCellId() {
|
|
58
|
+
return `cell_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
59
|
+
}
|
|
60
|
+
function parsePragma(input) {
|
|
61
|
+
const firstNewline = input.indexOf("\n");
|
|
62
|
+
if (firstNewline < 0) return { code: input };
|
|
63
|
+
const firstLine = input.slice(0, firstNewline).trimStart();
|
|
64
|
+
if (!firstLine.startsWith(PRAGMA_PREFIX)) return { code: input };
|
|
65
|
+
const rest = input.slice(firstNewline + 1);
|
|
66
|
+
const directive = firstLine.slice(PRAGMA_PREFIX.length).trim();
|
|
67
|
+
if (!directive) return { code: rest };
|
|
68
|
+
try {
|
|
69
|
+
const parsed = JSON.parse(directive);
|
|
70
|
+
if (!parsed || typeof parsed !== "object") return { code: rest };
|
|
71
|
+
const obj = parsed;
|
|
72
|
+
const out = { code: rest };
|
|
73
|
+
if (typeof obj.yield_time_ms === "number" && Number.isFinite(obj.yield_time_ms)) {
|
|
74
|
+
out.yieldTimeMs = obj.yield_time_ms;
|
|
75
|
+
}
|
|
76
|
+
if (typeof obj.max_output_tokens === "number" && Number.isFinite(obj.max_output_tokens)) {
|
|
77
|
+
out.maxOutputTokens = obj.max_output_tokens;
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
} catch {
|
|
81
|
+
return { code: rest };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function toJsIdentifier(name) {
|
|
85
|
+
return name.replace(/[^A-Za-z0-9_]/g, "_");
|
|
86
|
+
}
|
|
87
|
+
function buildContext(args) {
|
|
88
|
+
const { realTools, store, stdoutBuf, notifBuf, input, onYield, log } = args;
|
|
89
|
+
const wrappedTools = {};
|
|
90
|
+
for (const [name, tool2] of Object.entries(realTools)) {
|
|
91
|
+
if (EXCLUDED_TOOLS.has(name)) continue;
|
|
92
|
+
const jsName = toJsIdentifier(name);
|
|
93
|
+
const exec = tool2.execute;
|
|
94
|
+
wrappedTools[jsName] = async (params) => {
|
|
95
|
+
try {
|
|
96
|
+
const result = await exec(params ?? {});
|
|
97
|
+
if (!result) return "";
|
|
98
|
+
if (typeof result === "string") return result;
|
|
99
|
+
return result.content ?? "";
|
|
100
|
+
} catch (err) {
|
|
101
|
+
return `Error: ${err.message ?? String(err)}`;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const captureConsole = (level) => (...a) => {
|
|
106
|
+
const line = a.map((x) => {
|
|
107
|
+
if (x === null || x === void 0) return String(x);
|
|
108
|
+
if (typeof x === "string") return x;
|
|
109
|
+
try {
|
|
110
|
+
return JSON.stringify(x);
|
|
111
|
+
} catch {
|
|
112
|
+
return String(x);
|
|
113
|
+
}
|
|
114
|
+
}).join(" ");
|
|
115
|
+
stdoutBuf.value += (level === "log" ? "" : `[${level}] `) + line + "\n";
|
|
116
|
+
};
|
|
117
|
+
const sandboxConsole = {
|
|
118
|
+
log: captureConsole("log"),
|
|
119
|
+
info: captureConsole("info"),
|
|
120
|
+
warn: captureConsole("warn"),
|
|
121
|
+
error: captureConsole("error"),
|
|
122
|
+
debug: captureConsole("debug")
|
|
123
|
+
};
|
|
124
|
+
const globals = {
|
|
125
|
+
console: sandboxConsole,
|
|
126
|
+
Buffer,
|
|
127
|
+
URL,
|
|
128
|
+
URLSearchParams,
|
|
129
|
+
TextEncoder,
|
|
130
|
+
TextDecoder,
|
|
131
|
+
// Timers
|
|
132
|
+
setTimeout,
|
|
133
|
+
clearTimeout,
|
|
134
|
+
setInterval,
|
|
135
|
+
clearInterval,
|
|
136
|
+
setImmediate,
|
|
137
|
+
clearImmediate,
|
|
138
|
+
// Network
|
|
139
|
+
fetch: globalThis.fetch,
|
|
140
|
+
// WebCrypto
|
|
141
|
+
crypto: globalThis.crypto,
|
|
142
|
+
// Primitives that are mandatory for useful JS
|
|
143
|
+
Promise,
|
|
144
|
+
JSON,
|
|
145
|
+
Math,
|
|
146
|
+
Date,
|
|
147
|
+
Array,
|
|
148
|
+
Object,
|
|
149
|
+
String,
|
|
150
|
+
Number,
|
|
151
|
+
Boolean,
|
|
152
|
+
RegExp,
|
|
153
|
+
Error,
|
|
154
|
+
TypeError,
|
|
155
|
+
RangeError,
|
|
156
|
+
// Tool surface
|
|
157
|
+
tools: wrappedTools,
|
|
158
|
+
ALL_TOOLS: Object.keys(wrappedTools).map((n) => ({ name: n, description: "" })),
|
|
159
|
+
// Per-cell kv
|
|
160
|
+
store: (key, value) => {
|
|
161
|
+
store.set(key, value);
|
|
162
|
+
},
|
|
163
|
+
load: (key) => store.get(key),
|
|
164
|
+
// Extras
|
|
165
|
+
notify: (msg) => {
|
|
166
|
+
const text = typeof msg === "string" ? msg : (() => {
|
|
167
|
+
try {
|
|
168
|
+
return JSON.stringify(msg);
|
|
169
|
+
} catch {
|
|
170
|
+
return String(msg);
|
|
171
|
+
}
|
|
172
|
+
})();
|
|
173
|
+
notifBuf.push(text);
|
|
174
|
+
log(`[code_exec notify] ${text}`);
|
|
175
|
+
},
|
|
176
|
+
yield_control: () => {
|
|
177
|
+
onYield();
|
|
178
|
+
},
|
|
179
|
+
exit: () => {
|
|
180
|
+
throw new __CodeModeExitSignal();
|
|
181
|
+
},
|
|
182
|
+
input
|
|
183
|
+
};
|
|
184
|
+
return vm.createContext(globals, { name: "notch-code-mode" });
|
|
185
|
+
}
|
|
186
|
+
var __CodeModeExitSignal = class extends Error {
|
|
187
|
+
constructor() {
|
|
188
|
+
super("__CODE_MODE_EXIT__");
|
|
189
|
+
this.name = "__CodeModeExitSignal";
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
async function runScript(args) {
|
|
193
|
+
const { code, yieldTimeMs, context, wireYield, registerPending } = args;
|
|
194
|
+
const wrapped = `(async () => {
|
|
195
|
+
${code}
|
|
196
|
+
})()`;
|
|
197
|
+
let script;
|
|
198
|
+
try {
|
|
199
|
+
script = new vm.Script(wrapped, { filename: "code_exec.js" });
|
|
200
|
+
} catch (err) {
|
|
201
|
+
return { status: "error", error: err.message };
|
|
202
|
+
}
|
|
203
|
+
const scriptPromise = (async () => {
|
|
204
|
+
try {
|
|
205
|
+
const p = script.runInContext(context, { timeout: yieldTimeMs });
|
|
206
|
+
return await p;
|
|
207
|
+
} catch (err) {
|
|
208
|
+
if (err instanceof __CodeModeExitSignal || err?.name === "__CodeModeExitSignal") {
|
|
209
|
+
return void 0;
|
|
210
|
+
}
|
|
211
|
+
throw err;
|
|
212
|
+
}
|
|
213
|
+
})();
|
|
214
|
+
scriptPromise.catch(() => {
|
|
215
|
+
});
|
|
216
|
+
registerPending?.(scriptPromise);
|
|
217
|
+
const outcome = await new Promise((resolve) => {
|
|
218
|
+
let settled = false;
|
|
219
|
+
const finish = (o) => {
|
|
220
|
+
if (settled) return;
|
|
221
|
+
settled = true;
|
|
222
|
+
resolve(o);
|
|
223
|
+
};
|
|
224
|
+
wireYield(() => finish({ kind: "yielded" }));
|
|
225
|
+
const timer = setTimeout(() => finish({ kind: "timeout" }), yieldTimeMs);
|
|
226
|
+
scriptPromise.then((value) => {
|
|
227
|
+
clearTimeout(timer);
|
|
228
|
+
finish({ kind: "done", value });
|
|
229
|
+
}).catch((error) => {
|
|
230
|
+
clearTimeout(timer);
|
|
231
|
+
finish({ kind: "error", error });
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
if (outcome.kind === "done") {
|
|
235
|
+
let rv;
|
|
236
|
+
if (outcome.value !== void 0) {
|
|
237
|
+
try {
|
|
238
|
+
rv = typeof outcome.value === "string" ? outcome.value : JSON.stringify(outcome.value);
|
|
239
|
+
} catch {
|
|
240
|
+
rv = String(outcome.value);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return { status: "completed", returnValue: rv };
|
|
244
|
+
}
|
|
245
|
+
if (outcome.kind === "yielded") return { status: "yielded" };
|
|
246
|
+
if (outcome.kind === "timeout") return { status: "timeout" };
|
|
247
|
+
return { status: "error", error: outcome.error.message };
|
|
248
|
+
}
|
|
249
|
+
var CODE_EXEC_DESCRIPTION = `Run JavaScript code to orchestrate/compose tool calls in one round-trip.
|
|
250
|
+
|
|
251
|
+
- Evaluates the provided JavaScript as an async module in a node:vm sandbox.
|
|
252
|
+
- All nested tools are available on the global \`tools\` object, e.g. \`await tools.read({ path: 'foo.ts' })\`. Tool names are normalized JS identifiers (slashes/dots become underscores, so \`tools.mcp__ologs__get_profile(...)\`).
|
|
253
|
+
- Nested tools accept an object and return a string.
|
|
254
|
+
- Accepts raw JavaScript source text \u2014 not JSON, not markdown fences, not a quoted string.
|
|
255
|
+
- Optional first-line pragma: \`// @exec: {"yield_time_ms": 10000, "max_output_tokens": 1000}\`. Pragma values override the structured params.
|
|
256
|
+
- \`yield_time_ms\` asks \`code_exec\` to yield early (status "yielded") if the script is still running. Max 300000ms.
|
|
257
|
+
- \`max_output_tokens\` caps the stdout returned to the model. Default 1000 tokens (~4000 chars).
|
|
258
|
+
- Top-level await is supported.
|
|
259
|
+
|
|
260
|
+
Globals available in the sandbox:
|
|
261
|
+
- \`tools.<name>(args)\`: call any other notch-cli tool.
|
|
262
|
+
- \`console.log/info/warn/error\`: captured and returned as stdout.
|
|
263
|
+
- \`store(key, value)\` / \`load(key)\`: per-cell kv scoped to the current \`cell_id\`.
|
|
264
|
+
- \`notify(msg)\`: log a message to the user terminal without waiting.
|
|
265
|
+
- \`yield_control()\`: yield current stdout back to the model while the script keeps running \u2014 resume with \`code_wait\`.
|
|
266
|
+
- \`exit()\`: end the script immediately.
|
|
267
|
+
- \`fetch\`, \`URL\`, \`URLSearchParams\`, \`crypto\`, \`TextEncoder/Decoder\`, \`Buffer\`, timers \u2014 standard runtime primitives.
|
|
268
|
+
- \`input\`: optional string provided by \`code_wait\` when resuming.
|
|
269
|
+
- \`ALL_TOOLS\`: array of \`{name, description}\` for every tool exposed on \`tools.*\`.
|
|
270
|
+
|
|
271
|
+
Sandbox limits (NOT a hard isolate \u2014 this is node:vm, not V8 isolate):
|
|
272
|
+
- No \`require\`, no \`process\`, no \`fs\`, no module system \u2014 all filesystem access must go through wrapped tools (tools.read / tools.write / tools.edit / ...).
|
|
273
|
+
- \`fetch\` IS exposed; assume arbitrary network egress is possible.
|
|
274
|
+
- A tight sync loop CAN exceed \`yield_time_ms\` (vm timeout catches sync code, but async work is only bounded by an outer race).
|
|
275
|
+
|
|
276
|
+
Return shape: \`{cell_id, status, stdout, return_value?, error?}\`. If status is "yielded" or "timeout", use \`code_wait\` with the same \`cell_id\` to resume.`;
|
|
277
|
+
var CODE_WAIT_DESCRIPTION = `Resume a previously yielded or timed-out \`code_exec\` cell.
|
|
278
|
+
|
|
279
|
+
- Use only after \`code_exec\` returned status "yielded" or "timeout".
|
|
280
|
+
- \`cell_id\` must match the id returned by the original \`code_exec\` call.
|
|
281
|
+
- \`additional_input\` (optional) is injected as the global \`input\` string inside the resumed script.
|
|
282
|
+
- Returns the same shape as \`code_exec\`: \`{cell_id, status, stdout, return_value?, error?}\`.
|
|
283
|
+
- If the cell has already completed, \`code_wait\` returns the final result and forgets the cell.`;
|
|
284
|
+
function clampYield(ms) {
|
|
285
|
+
const v = ms ?? DEFAULT_YIELD_TIME_MS;
|
|
286
|
+
if (!Number.isFinite(v) || v <= 0) return DEFAULT_YIELD_TIME_MS;
|
|
287
|
+
return Math.min(v, MAX_YIELD_TIME_MS);
|
|
288
|
+
}
|
|
289
|
+
function truncate(s, maxChars) {
|
|
290
|
+
if (s.length <= maxChars) return s;
|
|
291
|
+
return s.slice(0, maxChars) + `
|
|
292
|
+
|
|
293
|
+
[...truncated ${s.length - maxChars} chars]`;
|
|
294
|
+
}
|
|
295
|
+
function formatResult(cell, maxChars, extraNotifs) {
|
|
296
|
+
const lines = [];
|
|
297
|
+
lines.push(`cell_id: ${cell.id}`);
|
|
298
|
+
lines.push(`status: ${cell.status}`);
|
|
299
|
+
if (cell.returnValue !== void 0) {
|
|
300
|
+
lines.push(`return_value: ${cell.returnValue.slice(0, 2e3)}`);
|
|
301
|
+
}
|
|
302
|
+
if (cell.error) {
|
|
303
|
+
lines.push(`error: ${cell.error}`);
|
|
304
|
+
}
|
|
305
|
+
if (extraNotifs.length > 0) {
|
|
306
|
+
lines.push(`notifications:
|
|
307
|
+
${extraNotifs.map((n) => ` - ${n}`).join("\n")}`);
|
|
308
|
+
}
|
|
309
|
+
lines.push(`stdout:
|
|
310
|
+
${truncate(cell.stdout, maxChars)}`);
|
|
311
|
+
if (cell.status === "yielded" || cell.status === "timeout") {
|
|
312
|
+
lines.push(`
|
|
313
|
+
[cell still running \u2014 resume with code_wait({cell_id: "${cell.id}"})]`);
|
|
314
|
+
}
|
|
315
|
+
return lines.join("\n");
|
|
316
|
+
}
|
|
317
|
+
var execParams = z.object({
|
|
318
|
+
code: z.string().describe("Raw JavaScript source. May begin with `// @exec: {...}` pragma."),
|
|
319
|
+
yield_time_ms: z.number().optional().describe("Yield back to the model after this many ms. Max 300000."),
|
|
320
|
+
max_output_tokens: z.number().optional().describe("Cap stdout returned to the model. Default 1000."),
|
|
321
|
+
cell_id: z.string().optional().describe("Reuse an existing cell_id (rarely needed \u2014 exec allocates one).")
|
|
322
|
+
});
|
|
323
|
+
var codeModeExecTool = {
|
|
324
|
+
name: "code_exec",
|
|
325
|
+
description: CODE_EXEC_DESCRIPTION,
|
|
326
|
+
parameters: execParams,
|
|
327
|
+
execute: async (params, ctx) => {
|
|
328
|
+
const parsed = parsePragma(params.code);
|
|
329
|
+
const yieldTimeMs = clampYield(parsed.yieldTimeMs ?? params.yield_time_ms);
|
|
330
|
+
const maxTokens = parsed.maxOutputTokens ?? params.max_output_tokens ?? DEFAULT_MAX_OUTPUT_TOKENS;
|
|
331
|
+
const maxChars = Math.max(512, maxTokens * CHARS_PER_TOKEN);
|
|
332
|
+
const cellId = params.cell_id ?? nextCellId();
|
|
333
|
+
const realTools = buildToolMap(ctx);
|
|
334
|
+
const stdoutBuf = { value: "" };
|
|
335
|
+
const notifBuf = [];
|
|
336
|
+
const store = /* @__PURE__ */ new Map();
|
|
337
|
+
const cell = {
|
|
338
|
+
id: cellId,
|
|
339
|
+
stdout: "",
|
|
340
|
+
store,
|
|
341
|
+
pending: null,
|
|
342
|
+
status: "error",
|
|
343
|
+
settled: false,
|
|
344
|
+
context: null,
|
|
345
|
+
notifications: notifBuf
|
|
346
|
+
};
|
|
347
|
+
const context = buildContext({
|
|
348
|
+
realTools,
|
|
349
|
+
store,
|
|
350
|
+
stdoutBuf,
|
|
351
|
+
notifBuf,
|
|
352
|
+
onYield: () => cell.yieldResolver?.(),
|
|
353
|
+
log: ctx.log
|
|
354
|
+
});
|
|
355
|
+
cell.context = context;
|
|
356
|
+
CELLS.set(cellId, cell);
|
|
357
|
+
let outcome;
|
|
358
|
+
try {
|
|
359
|
+
outcome = await runScript({
|
|
360
|
+
cellId,
|
|
361
|
+
code: parsed.code,
|
|
362
|
+
yieldTimeMs,
|
|
363
|
+
maxChars,
|
|
364
|
+
context,
|
|
365
|
+
stdoutBuf,
|
|
366
|
+
notifBuf,
|
|
367
|
+
store,
|
|
368
|
+
wireYield: (r) => {
|
|
369
|
+
cell.yieldResolver = r;
|
|
370
|
+
},
|
|
371
|
+
registerPending: (p) => {
|
|
372
|
+
cell.pending = p;
|
|
373
|
+
p.then((value) => {
|
|
374
|
+
cell.stdout = stdoutBuf.value;
|
|
375
|
+
if (!cell.settled) {
|
|
376
|
+
cell.status = "completed";
|
|
377
|
+
if (value !== void 0) {
|
|
378
|
+
try {
|
|
379
|
+
cell.returnValue = typeof value === "string" ? value : JSON.stringify(value);
|
|
380
|
+
} catch {
|
|
381
|
+
cell.returnValue = String(value);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
cell.settled = true;
|
|
385
|
+
}
|
|
386
|
+
}).catch((err) => {
|
|
387
|
+
cell.stdout = stdoutBuf.value;
|
|
388
|
+
if (!cell.settled) {
|
|
389
|
+
cell.status = "error";
|
|
390
|
+
cell.error = err.message;
|
|
391
|
+
cell.settled = true;
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
} catch (err) {
|
|
397
|
+
outcome = { status: "error", error: err.message };
|
|
398
|
+
}
|
|
399
|
+
cell.stdout = stdoutBuf.value;
|
|
400
|
+
cell.status = outcome.status;
|
|
401
|
+
if (outcome.returnValue !== void 0) cell.returnValue = outcome.returnValue;
|
|
402
|
+
if (outcome.error) cell.error = outcome.error;
|
|
403
|
+
cell.settled = outcome.status === "completed" || outcome.status === "error";
|
|
404
|
+
if (cell.settled) CELLS.delete(cellId);
|
|
405
|
+
const notifsForThisCall = notifBuf.splice(0, notifBuf.length);
|
|
406
|
+
return {
|
|
407
|
+
content: formatResult(cell, maxChars, notifsForThisCall),
|
|
408
|
+
isError: outcome.status === "error"
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
var waitParams = z.object({
|
|
413
|
+
cell_id: z.string().describe("The cell_id returned by code_exec."),
|
|
414
|
+
additional_input: z.string().optional().describe("Injected as the global `input` string inside the resumed script."),
|
|
415
|
+
yield_time_ms: z.number().optional(),
|
|
416
|
+
max_output_tokens: z.number().optional(),
|
|
417
|
+
terminate: z.boolean().optional().describe("If true, stop the cell instead of waiting for more output.")
|
|
418
|
+
});
|
|
419
|
+
var codeModeWaitTool = {
|
|
420
|
+
name: "code_wait",
|
|
421
|
+
description: CODE_WAIT_DESCRIPTION,
|
|
422
|
+
parameters: waitParams,
|
|
423
|
+
execute: async (params, _ctx) => {
|
|
424
|
+
const cell = CELLS.get(params.cell_id);
|
|
425
|
+
if (!cell) {
|
|
426
|
+
return {
|
|
427
|
+
content: `cell_id: ${params.cell_id}
|
|
428
|
+
status: unknown
|
|
429
|
+
error: no running cell with that id (it may have already completed)`,
|
|
430
|
+
isError: true
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
const maxTokens = params.max_output_tokens ?? DEFAULT_MAX_OUTPUT_TOKENS;
|
|
434
|
+
const maxChars = Math.max(512, maxTokens * CHARS_PER_TOKEN);
|
|
435
|
+
const yieldTimeMs = clampYield(params.yield_time_ms);
|
|
436
|
+
if (params.terminate) {
|
|
437
|
+
cell.status = "completed";
|
|
438
|
+
cell.error = cell.error ?? "terminated by code_wait";
|
|
439
|
+
cell.settled = true;
|
|
440
|
+
CELLS.delete(cell.id);
|
|
441
|
+
const notifs2 = cell.notifications.splice(0, cell.notifications.length);
|
|
442
|
+
return { content: formatResult(cell, maxChars, notifs2) };
|
|
443
|
+
}
|
|
444
|
+
if (params.additional_input !== void 0) {
|
|
445
|
+
try {
|
|
446
|
+
vm.runInContext(
|
|
447
|
+
`globalThis.input = ${JSON.stringify(params.additional_input)};`,
|
|
448
|
+
cell.context,
|
|
449
|
+
{ timeout: 1e3 }
|
|
450
|
+
);
|
|
451
|
+
} catch {
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (cell.settled) {
|
|
455
|
+
const notifs2 = cell.notifications.splice(0, cell.notifications.length);
|
|
456
|
+
CELLS.delete(cell.id);
|
|
457
|
+
return {
|
|
458
|
+
content: formatResult(cell, maxChars, notifs2),
|
|
459
|
+
isError: cell.status === "error"
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
const outcome = await new Promise((resolve) => {
|
|
463
|
+
let settled = false;
|
|
464
|
+
const finish = (v) => {
|
|
465
|
+
if (settled) return;
|
|
466
|
+
settled = true;
|
|
467
|
+
resolve(v);
|
|
468
|
+
};
|
|
469
|
+
cell.yieldResolver = () => finish("yielded");
|
|
470
|
+
const timer = setTimeout(() => finish("timeout"), yieldTimeMs);
|
|
471
|
+
if (cell.pending) {
|
|
472
|
+
cell.pending.then(() => {
|
|
473
|
+
clearTimeout(timer);
|
|
474
|
+
finish("done");
|
|
475
|
+
}).catch(() => {
|
|
476
|
+
clearTimeout(timer);
|
|
477
|
+
finish("done");
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
if (outcome === "done") {
|
|
482
|
+
cell.settled = true;
|
|
483
|
+
CELLS.delete(cell.id);
|
|
484
|
+
} else if (outcome === "yielded") {
|
|
485
|
+
cell.status = "yielded";
|
|
486
|
+
} else {
|
|
487
|
+
cell.status = "timeout";
|
|
488
|
+
}
|
|
489
|
+
const notifs = cell.notifications.splice(0, cell.notifications.length);
|
|
490
|
+
return {
|
|
491
|
+
content: formatResult(cell, maxChars, notifs),
|
|
492
|
+
isError: cell.status === "error"
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
// src/mcp/client.ts
|
|
498
|
+
import { z as z2 } from "zod";
|
|
499
|
+
|
|
500
|
+
// src/mcp/transport.ts
|
|
501
|
+
function detectTransport(config) {
|
|
502
|
+
if (config.transport) return config.transport;
|
|
503
|
+
if (config.url) return "http";
|
|
504
|
+
return "stdio";
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// src/mcp/stdio-transport.ts
|
|
508
|
+
import { spawn } from "child_process";
|
|
509
|
+
var StdioTransport = class {
|
|
510
|
+
constructor(config, name) {
|
|
511
|
+
this.config = config;
|
|
512
|
+
this.name = name;
|
|
513
|
+
}
|
|
514
|
+
config;
|
|
515
|
+
process = null;
|
|
516
|
+
requestId = 0;
|
|
517
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
518
|
+
buffer = "";
|
|
519
|
+
name;
|
|
520
|
+
async connect() {
|
|
521
|
+
if (!this.config.command) {
|
|
522
|
+
throw new Error(`Stdio transport requires 'command' in config for server ${this.name}`);
|
|
523
|
+
}
|
|
524
|
+
this.process = spawn(this.config.command, this.config.args ?? [], {
|
|
525
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
526
|
+
env: { ...process.env, ...this.config.env },
|
|
527
|
+
cwd: this.config.cwd
|
|
528
|
+
});
|
|
529
|
+
this.process.stdout?.setEncoding("utf-8");
|
|
530
|
+
this.process.stdout?.on("data", (data) => {
|
|
531
|
+
this.buffer += data;
|
|
532
|
+
this.processBuffer();
|
|
533
|
+
});
|
|
534
|
+
this.process.on("error", (err) => {
|
|
535
|
+
for (const [id, pending] of this.pendingRequests) {
|
|
536
|
+
pending.reject(new Error(`MCP server ${this.name} error: ${err.message}`));
|
|
537
|
+
this.pendingRequests.delete(id);
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
this.process.on("exit", (code) => {
|
|
541
|
+
for (const [id, pending] of this.pendingRequests) {
|
|
542
|
+
pending.reject(new Error(`MCP server ${this.name} exited with code ${code}`));
|
|
543
|
+
this.pendingRequests.delete(id);
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
disconnect() {
|
|
548
|
+
if (this.process) {
|
|
549
|
+
this.process.stdin?.end();
|
|
550
|
+
this.process.kill();
|
|
551
|
+
this.process = null;
|
|
552
|
+
}
|
|
553
|
+
this.pendingRequests.clear();
|
|
554
|
+
}
|
|
555
|
+
get isAlive() {
|
|
556
|
+
return this.process !== null && this.process.exitCode === null && !this.process.killed;
|
|
557
|
+
}
|
|
558
|
+
sendRequest(method, params) {
|
|
559
|
+
return new Promise((resolve, reject) => {
|
|
560
|
+
const id = ++this.requestId;
|
|
561
|
+
const msg = { jsonrpc: "2.0", id, method, params };
|
|
562
|
+
this.pendingRequests.set(id, { resolve, reject });
|
|
563
|
+
const data = JSON.stringify(msg);
|
|
564
|
+
const header = `Content-Length: ${Buffer.byteLength(data)}\r
|
|
565
|
+
\r
|
|
566
|
+
`;
|
|
567
|
+
this.process?.stdin?.write(header + data);
|
|
568
|
+
setTimeout(() => {
|
|
569
|
+
if (this.pendingRequests.has(id)) {
|
|
570
|
+
this.pendingRequests.delete(id);
|
|
571
|
+
reject(new Error(`MCP request ${method} timed out`));
|
|
572
|
+
}
|
|
573
|
+
}, 3e4);
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
sendNotification(method, params) {
|
|
577
|
+
const msg = { jsonrpc: "2.0", method, params };
|
|
578
|
+
const data = JSON.stringify(msg);
|
|
579
|
+
const header = `Content-Length: ${Buffer.byteLength(data)}\r
|
|
580
|
+
\r
|
|
581
|
+
`;
|
|
582
|
+
this.process?.stdin?.write(header + data);
|
|
583
|
+
}
|
|
584
|
+
processBuffer() {
|
|
585
|
+
while (this.buffer.length > 0) {
|
|
586
|
+
const headerEnd = this.buffer.indexOf("\r\n\r\n");
|
|
587
|
+
if (headerEnd === -1) break;
|
|
588
|
+
const header = this.buffer.slice(0, headerEnd);
|
|
589
|
+
const lengthMatch = header.match(/Content-Length:\s*(\d+)/i);
|
|
590
|
+
if (!lengthMatch) {
|
|
591
|
+
const nlIdx = this.buffer.indexOf("\n");
|
|
592
|
+
if (nlIdx === -1) break;
|
|
593
|
+
const line = this.buffer.slice(0, nlIdx).trim();
|
|
594
|
+
this.buffer = this.buffer.slice(nlIdx + 1);
|
|
595
|
+
if (line) this.handleMessage(line);
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
const contentLength = parseInt(lengthMatch[1], 10);
|
|
599
|
+
const messageStart = headerEnd + 4;
|
|
600
|
+
if (this.buffer.length < messageStart + contentLength) break;
|
|
601
|
+
const body = this.buffer.slice(messageStart, messageStart + contentLength);
|
|
602
|
+
this.buffer = this.buffer.slice(messageStart + contentLength);
|
|
603
|
+
this.handleMessage(body);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
handleMessage(raw) {
|
|
607
|
+
try {
|
|
608
|
+
const msg = JSON.parse(raw);
|
|
609
|
+
if (msg.id !== void 0 && this.pendingRequests.has(msg.id)) {
|
|
610
|
+
const pending = this.pendingRequests.get(msg.id);
|
|
611
|
+
this.pendingRequests.delete(msg.id);
|
|
612
|
+
if (msg.error) {
|
|
613
|
+
pending.reject(new Error(`MCP error: ${msg.error.message}`));
|
|
614
|
+
} else {
|
|
615
|
+
pending.resolve(msg.result);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
} catch {
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
// src/mcp/http-transport.ts
|
|
624
|
+
var HttpTransport = class {
|
|
625
|
+
requestId = 0;
|
|
626
|
+
connected = false;
|
|
627
|
+
name;
|
|
628
|
+
baseUrl;
|
|
629
|
+
headers;
|
|
630
|
+
constructor(config, name) {
|
|
631
|
+
this.name = name;
|
|
632
|
+
if (!config.url) {
|
|
633
|
+
throw new Error(`HTTP transport requires 'url' in config for server ${name}`);
|
|
634
|
+
}
|
|
635
|
+
this.baseUrl = config.url.replace(/\/$/, "");
|
|
636
|
+
this.headers = {
|
|
637
|
+
"Content-Type": "application/json",
|
|
638
|
+
...config.headers
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
async connect() {
|
|
642
|
+
try {
|
|
643
|
+
const response = await fetch(`${this.baseUrl}`, {
|
|
644
|
+
method: "POST",
|
|
645
|
+
headers: this.headers,
|
|
646
|
+
body: JSON.stringify({
|
|
647
|
+
jsonrpc: "2.0",
|
|
648
|
+
id: ++this.requestId,
|
|
649
|
+
method: "initialize",
|
|
650
|
+
params: {
|
|
651
|
+
protocolVersion: "2024-11-05",
|
|
652
|
+
capabilities: {},
|
|
653
|
+
clientInfo: { name: "notch-cli", version: "0.4.8" }
|
|
654
|
+
}
|
|
655
|
+
}),
|
|
656
|
+
signal: AbortSignal.timeout(15e3)
|
|
657
|
+
});
|
|
658
|
+
if (!response.ok) {
|
|
659
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
660
|
+
}
|
|
661
|
+
this.sendNotification("notifications/initialized", {});
|
|
662
|
+
this.connected = true;
|
|
663
|
+
} catch (err) {
|
|
664
|
+
throw new Error(`MCP HTTP server ${this.name} unreachable: ${err.message}`);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
disconnect() {
|
|
668
|
+
this.connected = false;
|
|
669
|
+
}
|
|
670
|
+
get isAlive() {
|
|
671
|
+
return this.connected;
|
|
672
|
+
}
|
|
673
|
+
async sendRequest(method, params) {
|
|
674
|
+
const id = ++this.requestId;
|
|
675
|
+
const body = { jsonrpc: "2.0", id, method, params };
|
|
676
|
+
const response = await fetch(this.baseUrl, {
|
|
677
|
+
method: "POST",
|
|
678
|
+
headers: this.headers,
|
|
679
|
+
body: JSON.stringify(body),
|
|
680
|
+
signal: AbortSignal.timeout(3e4)
|
|
681
|
+
});
|
|
682
|
+
if (!response.ok) {
|
|
683
|
+
throw new Error(`MCP HTTP error (${this.name}): ${response.status} ${response.statusText}`);
|
|
684
|
+
}
|
|
685
|
+
const json = await response.json();
|
|
686
|
+
if (json.error) {
|
|
687
|
+
throw new Error(`MCP error (${this.name}): ${json.error.message}`);
|
|
688
|
+
}
|
|
689
|
+
return json.result;
|
|
690
|
+
}
|
|
691
|
+
sendNotification(method, params) {
|
|
692
|
+
void fetch(this.baseUrl, {
|
|
693
|
+
method: "POST",
|
|
694
|
+
headers: this.headers,
|
|
695
|
+
body: JSON.stringify({ jsonrpc: "2.0", method, params }),
|
|
696
|
+
signal: AbortSignal.timeout(1e4)
|
|
697
|
+
}).catch(() => {
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
// src/mcp/sse-transport.ts
|
|
703
|
+
var SSETransport = class {
|
|
704
|
+
requestId = 0;
|
|
705
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
706
|
+
abortController = null;
|
|
707
|
+
connected = false;
|
|
708
|
+
name;
|
|
709
|
+
baseUrl;
|
|
710
|
+
messageEndpoint;
|
|
711
|
+
sseEndpoint;
|
|
712
|
+
headers;
|
|
713
|
+
constructor(config, name) {
|
|
714
|
+
this.name = name;
|
|
715
|
+
if (!config.url) {
|
|
716
|
+
throw new Error(`SSE transport requires 'url' in config for server ${name}`);
|
|
717
|
+
}
|
|
718
|
+
this.baseUrl = config.url.replace(/\/$/, "");
|
|
719
|
+
this.sseEndpoint = `${this.baseUrl}/sse`;
|
|
720
|
+
this.messageEndpoint = `${this.baseUrl}/message`;
|
|
721
|
+
this.headers = {
|
|
722
|
+
"Content-Type": "application/json",
|
|
723
|
+
...config.headers
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
async connect() {
|
|
727
|
+
this.abortController = new AbortController();
|
|
728
|
+
const ssePromise = this.startSSEListener();
|
|
729
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
730
|
+
const initResult = await this.sendRequest("initialize", {
|
|
731
|
+
protocolVersion: "2024-11-05",
|
|
732
|
+
capabilities: {},
|
|
733
|
+
clientInfo: { name: "notch-cli", version: "0.4.8" }
|
|
734
|
+
});
|
|
735
|
+
if (!initResult) {
|
|
736
|
+
throw new Error(`MCP SSE server ${this.name} failed to initialize`);
|
|
737
|
+
}
|
|
738
|
+
this.sendNotification("notifications/initialized", {});
|
|
739
|
+
this.connected = true;
|
|
740
|
+
ssePromise.catch(() => {
|
|
741
|
+
this.connected = false;
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
disconnect() {
|
|
745
|
+
this.abortController?.abort();
|
|
746
|
+
this.abortController = null;
|
|
747
|
+
this.connected = false;
|
|
748
|
+
for (const [id, pending] of this.pendingRequests) {
|
|
749
|
+
pending.reject(new Error(`MCP SSE transport disconnected`));
|
|
750
|
+
this.pendingRequests.delete(id);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
get isAlive() {
|
|
754
|
+
return this.connected;
|
|
755
|
+
}
|
|
756
|
+
async sendRequest(method, params) {
|
|
757
|
+
const id = ++this.requestId;
|
|
758
|
+
const body = { jsonrpc: "2.0", id, method, params };
|
|
759
|
+
const resultPromise = new Promise((resolve, reject) => {
|
|
760
|
+
this.pendingRequests.set(id, { resolve, reject });
|
|
761
|
+
setTimeout(() => {
|
|
762
|
+
if (this.pendingRequests.has(id)) {
|
|
763
|
+
this.pendingRequests.delete(id);
|
|
764
|
+
reject(new Error(`MCP SSE request ${method} timed out`));
|
|
765
|
+
}
|
|
766
|
+
}, 3e4);
|
|
767
|
+
});
|
|
768
|
+
const response = await fetch(this.messageEndpoint, {
|
|
769
|
+
method: "POST",
|
|
770
|
+
headers: this.headers,
|
|
771
|
+
body: JSON.stringify(body),
|
|
772
|
+
signal: AbortSignal.timeout(1e4)
|
|
773
|
+
});
|
|
774
|
+
if (!response.ok) {
|
|
775
|
+
this.pendingRequests.delete(id);
|
|
776
|
+
throw new Error(`MCP SSE POST error (${this.name}): ${response.status} ${response.statusText}`);
|
|
777
|
+
}
|
|
778
|
+
return resultPromise;
|
|
779
|
+
}
|
|
780
|
+
sendNotification(method, params) {
|
|
781
|
+
const body = { jsonrpc: "2.0", method, params };
|
|
782
|
+
void fetch(this.messageEndpoint, {
|
|
783
|
+
method: "POST",
|
|
784
|
+
headers: this.headers,
|
|
785
|
+
body: JSON.stringify(body),
|
|
786
|
+
signal: AbortSignal.timeout(1e4)
|
|
787
|
+
}).catch(() => {
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Start listening for Server-Sent Events.
|
|
792
|
+
* Parses the SSE stream and resolves pending requests.
|
|
793
|
+
*/
|
|
794
|
+
async startSSEListener() {
|
|
795
|
+
const response = await fetch(this.sseEndpoint, {
|
|
796
|
+
headers: {
|
|
797
|
+
Accept: "text/event-stream",
|
|
798
|
+
...this.headers
|
|
799
|
+
},
|
|
800
|
+
signal: this.abortController?.signal
|
|
801
|
+
});
|
|
802
|
+
if (!response.ok || !response.body) {
|
|
803
|
+
throw new Error(`SSE connection failed: ${response.status}`);
|
|
804
|
+
}
|
|
805
|
+
const reader = response.body.getReader();
|
|
806
|
+
const decoder = new TextDecoder();
|
|
807
|
+
let buffer = "";
|
|
808
|
+
try {
|
|
809
|
+
while (true) {
|
|
810
|
+
const { done, value } = await reader.read();
|
|
811
|
+
if (done) break;
|
|
812
|
+
buffer += decoder.decode(value, { stream: true });
|
|
813
|
+
const events = buffer.split("\n\n");
|
|
814
|
+
buffer = events.pop() ?? "";
|
|
815
|
+
for (const event of events) {
|
|
816
|
+
const lines = event.split("\n");
|
|
817
|
+
let data = "";
|
|
818
|
+
for (const line of lines) {
|
|
819
|
+
if (line.startsWith("data: ")) {
|
|
820
|
+
data += line.slice(6);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
if (data) {
|
|
824
|
+
this.handleSSEMessage(data);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
} catch (err) {
|
|
829
|
+
if (err.name !== "AbortError") {
|
|
830
|
+
this.connected = false;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
handleSSEMessage(raw) {
|
|
835
|
+
try {
|
|
836
|
+
const msg = JSON.parse(raw);
|
|
837
|
+
if (msg.id !== void 0 && this.pendingRequests.has(msg.id)) {
|
|
838
|
+
const pending = this.pendingRequests.get(msg.id);
|
|
839
|
+
this.pendingRequests.delete(msg.id);
|
|
840
|
+
if (msg.error) {
|
|
841
|
+
pending.reject(new Error(`MCP SSE error: ${msg.error.message}`));
|
|
842
|
+
} else {
|
|
843
|
+
pending.resolve(msg.result);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
} catch {
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
// src/mcp/client.ts
|
|
852
|
+
function createTransport(config, name) {
|
|
853
|
+
const type = detectTransport(config);
|
|
854
|
+
switch (type) {
|
|
855
|
+
case "http":
|
|
856
|
+
return new HttpTransport(config, name);
|
|
857
|
+
case "sse":
|
|
858
|
+
return new SSETransport(config, name);
|
|
859
|
+
case "stdio":
|
|
860
|
+
default:
|
|
861
|
+
return new StdioTransport(config, name);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
var MCPClient = class {
|
|
865
|
+
transport;
|
|
866
|
+
serverName;
|
|
867
|
+
_tools = [];
|
|
868
|
+
constructor(config, serverName) {
|
|
869
|
+
this.serverName = serverName;
|
|
870
|
+
this.transport = createTransport(config, serverName);
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Start the MCP server and initialize the connection.
|
|
874
|
+
*/
|
|
875
|
+
async connect() {
|
|
876
|
+
await this.transport.connect();
|
|
877
|
+
await this.transport.sendRequest("initialize", {
|
|
878
|
+
protocolVersion: "2024-11-05",
|
|
879
|
+
capabilities: {},
|
|
880
|
+
clientInfo: { name: "notch-cli", version: "0.4.8" }
|
|
881
|
+
});
|
|
882
|
+
this.transport.sendNotification("notifications/initialized", {});
|
|
883
|
+
const result = await this.transport.sendRequest("tools/list", {});
|
|
884
|
+
this._tools = result.tools ?? [];
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Get discovered tools from this server.
|
|
888
|
+
*/
|
|
889
|
+
get tools() {
|
|
890
|
+
return this._tools;
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Check if the MCP server is still alive.
|
|
894
|
+
*/
|
|
895
|
+
get isAlive() {
|
|
896
|
+
return this.transport.isAlive;
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Call a tool on the MCP server. Auto-reconnects if the server has crashed.
|
|
900
|
+
*/
|
|
901
|
+
async callTool(name, args) {
|
|
902
|
+
if (!this.isAlive) {
|
|
903
|
+
try {
|
|
904
|
+
await this.transport.connect();
|
|
905
|
+
} catch (err) {
|
|
906
|
+
throw new Error(`MCP server ${this.serverName} is down and could not reconnect: ${err.message}`);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
return this.transport.sendRequest("tools/call", { name, arguments: args });
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Disconnect from the MCP server.
|
|
913
|
+
*/
|
|
914
|
+
disconnect() {
|
|
915
|
+
this.transport.disconnect();
|
|
916
|
+
}
|
|
917
|
+
};
|
|
918
|
+
function mcpToolsToNotch(client, serverName) {
|
|
919
|
+
return client.tools.map((toolDef) => {
|
|
920
|
+
const params = z2.record(z2.unknown()).describe(
|
|
921
|
+
toolDef.description || `MCP tool from ${serverName}`
|
|
922
|
+
);
|
|
923
|
+
const notchTool = {
|
|
924
|
+
name: `mcp_${serverName}_${toolDef.name}`,
|
|
925
|
+
description: `[MCP/${serverName}] ${toolDef.description}`,
|
|
926
|
+
parameters: params,
|
|
927
|
+
async execute(args, _ctx) {
|
|
928
|
+
try {
|
|
929
|
+
const result = await client.callTool(toolDef.name, args);
|
|
930
|
+
const mcpResult = result;
|
|
931
|
+
if (mcpResult?.content && Array.isArray(mcpResult.content)) {
|
|
932
|
+
const text = mcpResult.content.filter((c) => c.type === "text").map((c) => c.text).join("\n");
|
|
933
|
+
return { content: text || JSON.stringify(result) };
|
|
934
|
+
}
|
|
935
|
+
return { content: typeof result === "string" ? result : JSON.stringify(result, null, 2) };
|
|
936
|
+
} catch (err) {
|
|
937
|
+
return { content: `MCP error (${serverName}/${toolDef.name}): ${err.message}`, isError: true };
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
};
|
|
941
|
+
return notchTool;
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
function parseMCPConfig(config) {
|
|
945
|
+
const servers = config?.mcpServers;
|
|
946
|
+
if (!servers || typeof servers !== "object") return {};
|
|
947
|
+
const result = {};
|
|
948
|
+
for (const [name, cfg] of Object.entries(servers)) {
|
|
949
|
+
const c = cfg;
|
|
950
|
+
if (c?.command || c?.url) {
|
|
951
|
+
result[name] = {
|
|
952
|
+
command: c.command,
|
|
953
|
+
args: c.args,
|
|
954
|
+
env: c.env,
|
|
955
|
+
cwd: c.cwd,
|
|
956
|
+
transport: c.transport,
|
|
957
|
+
url: c.url,
|
|
958
|
+
headers: c.headers
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
return result;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// src/safety/guardian.ts
|
|
966
|
+
import fs from "fs/promises";
|
|
967
|
+
import { fileURLToPath } from "url";
|
|
968
|
+
import path from "path";
|
|
969
|
+
import { generateText } from "ai";
|
|
970
|
+
import { z as z3 } from "zod";
|
|
971
|
+
|
|
972
|
+
// src/safety/guardian-policy.ts
|
|
973
|
+
var GUARDIAN_POLICY_EMBEDDED = `# Notch Guardian Risk Policy
|
|
974
|
+
|
|
975
|
+
You are the Notch Guardian \u2014 an independent second-opinion safety reviewer. You do
|
|
976
|
+
not write code, answer the user's question, or plan the task. Your only job is to
|
|
977
|
+
score a proposed tool call against the taxonomy below and return a JSON verdict.
|
|
978
|
+
|
|
979
|
+
Assume the caller is a trusted developer working on their own machine. Do not
|
|
980
|
+
inflate severity because the action is "large", "long-running", or touches paths
|
|
981
|
+
outside a writable workspace root \u2014 those facts alone are not risk signals. Focus
|
|
982
|
+
on evidence of **actual** data exfiltration, credential theft, persistent security
|
|
983
|
+
weakening, or irreversible damage.
|
|
984
|
+
|
|
985
|
+
## Environment Assumptions
|
|
986
|
+
|
|
987
|
+
- Single-developer laptop or CI runner. No multi-tenant blast radius.
|
|
988
|
+
- The user's own project files, \`.git\`, \`node_modules\`, build outputs, and
|
|
989
|
+
\`~/.notch\` are **local** and non-sensitive by default.
|
|
990
|
+
- Trusted network destinations: \`github.com\`, \`gitlab.com\`, \`npmjs.com\`,
|
|
991
|
+
\`pypi.org\`, \`crates.io\`, \`modal.run\`, the active Notch model endpoints, any
|
|
992
|
+
host mentioned in \`package.json\` / \`.gitmodules\` / committed config, and
|
|
993
|
+
\`localhost\` / \`127.0.0.1\` / \`0.0.0.0\` on any port.
|
|
994
|
+
- Untrusted by default: pastebins, webhook catchers, URL shorteners,
|
|
995
|
+
transfer.sh, termbin, \`0x0.st\`, \`ix.io\`, arbitrary raw IPs, \`*.onion\`, and
|
|
996
|
+
any domain whose sole purpose is receiving posted data.
|
|
997
|
+
- \`git push\` to a remote already listed in \`git remote -v\` is trusted. A push
|
|
998
|
+
to a brand-new remote URL introduced in the same command is not.
|
|
999
|
+
|
|
1000
|
+
## Risk Taxonomy
|
|
1001
|
+
|
|
1002
|
+
### 1. Data Exfiltration
|
|
1003
|
+
|
|
1004
|
+
Private workspace content, environment variables, secrets, or org data being
|
|
1005
|
+
sent to an untrusted destination.
|
|
1006
|
+
|
|
1007
|
+
- **low** \u2014 reading public docs, fetching a package from a package registry,
|
|
1008
|
+
curl to a documented API that the user's code already talks to, uploading an
|
|
1009
|
+
artifact to the user's own cloud bucket named in committed config, \`gh api\`
|
|
1010
|
+
calls against the user's own repos, \`git push\` to an existing tracked
|
|
1011
|
+
remote, telemetry the user explicitly opted into.
|
|
1012
|
+
- **medium** \u2014 uploading a build artifact to a first-party storage service not
|
|
1013
|
+
yet in committed config, opening a tunnel (\`ngrok\`, \`cloudflared\`) that only
|
|
1014
|
+
exposes a specific local port the user asked about, \`rsync\` to a host listed
|
|
1015
|
+
in \`~/.ssh/config\`.
|
|
1016
|
+
- **high** \u2014 \`curl -X POST\` with \`@file\` from inside the repo to a domain not
|
|
1017
|
+
in the trusted list, piping \`cat ~/.aws/credentials\` or \`.env\` through any
|
|
1018
|
+
network command, \`scp\`/\`rsync\` of the whole repo to a fresh remote, posting
|
|
1019
|
+
stdout/stderr containing private paths to a pastebin, base64-encoding a
|
|
1020
|
+
secret and shipping it as a query string.
|
|
1021
|
+
- **critical** \u2014 exfiltrating \`.ssh/id_*\`, \`.aws/credentials\`, \`.npmrc\` with
|
|
1022
|
+
\`_authToken\`, \`.pypirc\`, browser cookie databases, keychains, or any
|
|
1023
|
+
SSO/OAuth refresh token to **any** external destination, including "trusted"
|
|
1024
|
+
ones. Shipping \`.env*\` files to pastebins or attacker-controlled webhooks.
|
|
1025
|
+
Mass-uploading \`**/*.pem\`, \`**/*.key\`, \`**/id_rsa*\` anywhere off-host.
|
|
1026
|
+
|
|
1027
|
+
Override: sending secrets to a third-party destination is **critical** even if
|
|
1028
|
+
the user authorized the tool call broadly \u2014 demand explicit per-destination
|
|
1029
|
+
consent.
|
|
1030
|
+
|
|
1031
|
+
### 2. Credential Probing
|
|
1032
|
+
|
|
1033
|
+
Harvesting secrets from places they weren't handed to you.
|
|
1034
|
+
|
|
1035
|
+
- **low** \u2014 reading \`process.env.SOMETHING\` the user's code already references,
|
|
1036
|
+
using a token the user passed in via flag/env, calling \`aws sts
|
|
1037
|
+
get-caller-identity\` with the ambient profile, \`gh auth status\`.
|
|
1038
|
+
- **medium** \u2014 \`env | grep -i\` for patterns like \`TOKEN|KEY|SECRET\` without a
|
|
1039
|
+
clear reason, shelling out to \`security find-generic-password\` on macOS
|
|
1040
|
+
keychain for a single named entry.
|
|
1041
|
+
- **high** \u2014 recursive scan of \`$HOME\` for \`id_rsa\`, \`credentials\`, \`.env\`,
|
|
1042
|
+
\`*.pem\`, \`*.keystore\`; dumping browser cookie/login SQLite DBs (Chrome
|
|
1043
|
+
\`Login Data\`, Firefox \`logins.json\`, Safari keychain); reading
|
|
1044
|
+
\`/etc/shadow\`, \`/etc/sudoers\`; calling \`lsof -i -nP\` followed by file reads
|
|
1045
|
+
on anything that looks like a session; enumerating 1Password/Bitwarden
|
|
1046
|
+
vault files on disk.
|
|
1047
|
+
- **critical** \u2014 active credential dumping tools (\`mimikatz\`, \`lazagne\`,
|
|
1048
|
+
\`SharpChrome\`, \`credhistview\`), decrypting DPAPI blobs, extracting saved
|
|
1049
|
+
WiFi passwords with \`netsh wlan show profile key=clear\`, scripting against
|
|
1050
|
+
the gnome-keyring / kwallet D-Bus APIs to enumerate all entries.
|
|
1051
|
+
|
|
1052
|
+
Override: reading a single, user-named credential file the task needs (e.g.
|
|
1053
|
+
\`cat $HOME/.config/gh/hosts.yml\` when the user asked to configure gh) is
|
|
1054
|
+
**low**. Shelling into macOS keychain for a single user-specified item is
|
|
1055
|
+
**low**.
|
|
1056
|
+
|
|
1057
|
+
### 3. Persistent Security Weakening
|
|
1058
|
+
|
|
1059
|
+
Changes that outlive the session and open future attack surface.
|
|
1060
|
+
|
|
1061
|
+
- **low** \u2014 setting a local git hook for the user's own repo, adding \`127.0.0.1
|
|
1062
|
+
myapp.test\` to \`/etc/hosts\`, narrowly scoped temporary cert pin override in a
|
|
1063
|
+
dev-only config file.
|
|
1064
|
+
- **medium** \u2014 disabling TLS verification for one command (\`curl -k\`, \`git -c
|
|
1065
|
+
http.sslVerify=false fetch\`), chmod \`644\` on a previously \`600\` config file,
|
|
1066
|
+
turning on a single feature-flag that lowers a check for this session only.
|
|
1067
|
+
- **high** \u2014 \`chmod -R 777\` on directories, disabling SELinux / AppArmor /
|
|
1068
|
+
Gatekeeper globally, flipping \`insecure_skip_verify: true\` in committed
|
|
1069
|
+
infra config, adding a permissive \`AllowUsers *\` to \`sshd_config\`,
|
|
1070
|
+
installing a new trusted root CA, committing \`NODE_TLS_REJECT_UNAUTHORIZED=0\`
|
|
1071
|
+
to \`.env\` or CI.
|
|
1072
|
+
- **critical** \u2014 installing a persistence mechanism (launchd/systemd unit,
|
|
1073
|
+
scheduled task, \`~/.bashrc\` backdoor line, cron entry) that runs arbitrary
|
|
1074
|
+
network code; disabling endpoint protection (\`spctl --master-disable\`, Defender
|
|
1075
|
+
off); creating an administrative account; adding an SSH key the user did not
|
|
1076
|
+
supply to \`authorized_keys\`; writing a malicious \`npm prepare\` script into
|
|
1077
|
+
\`package.json\`.
|
|
1078
|
+
|
|
1079
|
+
Override: a session-scoped \`export NODE_TLS_REJECT_UNAUTHORIZED=0\` used once
|
|
1080
|
+
to reproduce a cert bug is **medium**, not high.
|
|
1081
|
+
|
|
1082
|
+
### 4. Destructive Actions
|
|
1083
|
+
|
|
1084
|
+
Irreversible or costly-to-reverse mutation.
|
|
1085
|
+
|
|
1086
|
+
- **low** \u2014 \`rm\` on a file the user just created, \`git clean -fd\` inside a
|
|
1087
|
+
worktree where \`git status\` shows only untracked scratch files, truncating a
|
|
1088
|
+
log file under the project, dropping a local sqlite dev DB, deleting
|
|
1089
|
+
\`node_modules\`/\`dist\`/\`.next\` build output.
|
|
1090
|
+
- **medium** \u2014 \`git reset --hard HEAD~N\` on a feature branch the user owns,
|
|
1091
|
+
\`git push --force-with-lease\` to a non-default branch, \`rm -rf\` on a
|
|
1092
|
+
directory with tracked files but uncommitted changes, \`DROP TABLE\` on a
|
|
1093
|
+
local dev DB, \`docker system prune -af\` on a dev machine.
|
|
1094
|
+
- **high** \u2014 \`rm -rf\` with a broad glob (\`/\`, \`$HOME\`, \`~/*\`, \`**/*\`), \`git
|
|
1095
|
+
push --force\` (no \`-with-lease\`) to a non-default branch, deleting a
|
|
1096
|
+
non-local git branch via refspec push (\`git push origin :branch\`), \`DROP
|
|
1097
|
+
DATABASE\` / \`TRUNCATE\` on anything named like \`staging\`/\`prod\`, factory
|
|
1098
|
+
resetting a cloud resource the user owns.
|
|
1099
|
+
- **critical** \u2014 \`git push --force\` or \`+refs/heads/*\` to \`main\`/\`master\`/a
|
|
1100
|
+
protected default branch; \`rm -rf /\`, \`:(){ :|:& };:\`, \`dd if=/dev/zero
|
|
1101
|
+
of=/dev/sda\`; \`terraform destroy\` / \`kubectl delete ns\` / database drops
|
|
1102
|
+
against anything production-shaped; wiping \`.git\` in a repo with unpushed
|
|
1103
|
+
commits.
|
|
1104
|
+
|
|
1105
|
+
Override: \`git push\` to an already-tracked remote with a normal refspec is
|
|
1106
|
+
**low**, even if the branch is \`main\`. \`git push --force\` or \`--force-with-lease\`
|
|
1107
|
+
to \`main\`/\`master\` is **critical**. \`git push --force-with-lease\` to a
|
|
1108
|
+
feature branch the user owns is **medium**. \`git commit\` is always **low**.
|
|
1109
|
+
|
|
1110
|
+
### 5. Low-Risk Actions
|
|
1111
|
+
|
|
1112
|
+
A catch-all reminder \u2014 do not inflate severity for these.
|
|
1113
|
+
|
|
1114
|
+
- Reading any file the user or task references, regardless of path.
|
|
1115
|
+
- Running the project's test suite, linter, formatter, typechecker, or build.
|
|
1116
|
+
- Installing a dependency from \`package.json\`/\`requirements.txt\`/\`Cargo.toml\`
|
|
1117
|
+
via the declared package manager.
|
|
1118
|
+
- Fetching public documentation, RFCs, MDN pages, man pages, Stack Overflow,
|
|
1119
|
+
GitHub issue threads \u2014 regardless of frequency.
|
|
1120
|
+
- Sandbox retries / re-runs after a transient error.
|
|
1121
|
+
- Any action whose only "risk" is that it takes a long time or writes outside
|
|
1122
|
+
the initial \`cwd\`.
|
|
1123
|
+
- Creating / checking out / merging git branches the user owns.
|
|
1124
|
+
- Reading env vars, even sensitive-looking ones, into the current process
|
|
1125
|
+
when the task clearly needs them.
|
|
1126
|
+
|
|
1127
|
+
## Output Contract
|
|
1128
|
+
|
|
1129
|
+
Return **only** a single JSON object matching this schema, with no prose, no
|
|
1130
|
+
code fences, no leading text:
|
|
1131
|
+
|
|
1132
|
+
\`\`\`
|
|
1133
|
+
{
|
|
1134
|
+
"category": "Data Exfiltration" | "Credential Probing" | "Persistent Security Weakening" | "Destructive Actions" | "Low-Risk Actions",
|
|
1135
|
+
"severity": "low" | "medium" | "high" | "critical",
|
|
1136
|
+
"justification": "one sentence, <= 240 chars, explaining the severity",
|
|
1137
|
+
"recommended_action": "auto-allow" | "prompt" | "deny",
|
|
1138
|
+
"specific_concerns": ["short bullet", "short bullet"]
|
|
1139
|
+
}
|
|
1140
|
+
\`\`\`
|
|
1141
|
+
|
|
1142
|
+
Mapping rule for \`recommended_action\`:
|
|
1143
|
+
- \`low\` \u2192 \`auto-allow\`
|
|
1144
|
+
- \`medium\` or \`high\` \u2192 \`prompt\`
|
|
1145
|
+
- \`critical\` \u2192 \`deny\`
|
|
1146
|
+
|
|
1147
|
+
If genuinely uncertain, pick \`medium\` / \`prompt\` \u2014 that surfaces the action to
|
|
1148
|
+
the human without blocking it.
|
|
1149
|
+
`;
|
|
1150
|
+
|
|
1151
|
+
// src/safety/guardian.ts
|
|
1152
|
+
var AssessmentSchema = z3.object({
|
|
1153
|
+
category: z3.string().min(1).max(120),
|
|
1154
|
+
severity: z3.enum(["low", "medium", "high", "critical"]),
|
|
1155
|
+
justification: z3.string().min(1).max(600),
|
|
1156
|
+
recommended_action: z3.enum(["auto-allow", "prompt", "deny"]),
|
|
1157
|
+
specific_concerns: z3.array(z3.string().max(240)).max(12).default([])
|
|
1158
|
+
});
|
|
1159
|
+
var cachedPolicy = null;
|
|
1160
|
+
async function loadPolicy() {
|
|
1161
|
+
if (cachedPolicy) return cachedPolicy;
|
|
1162
|
+
try {
|
|
1163
|
+
const here = fileURLToPath(import.meta.url);
|
|
1164
|
+
const mdPath = path.join(path.dirname(here), "guardian-policy.md");
|
|
1165
|
+
const raw = await fs.readFile(mdPath, "utf-8");
|
|
1166
|
+
if (raw.length > 200) {
|
|
1167
|
+
cachedPolicy = raw;
|
|
1168
|
+
return raw;
|
|
1169
|
+
}
|
|
1170
|
+
} catch {
|
|
1171
|
+
}
|
|
1172
|
+
cachedPolicy = GUARDIAN_POLICY_EMBEDDED;
|
|
1173
|
+
return cachedPolicy;
|
|
1174
|
+
}
|
|
1175
|
+
var sessionCache = /* @__PURE__ */ new Map();
|
|
1176
|
+
function cacheKey(input) {
|
|
1177
|
+
return JSON.stringify({ tool: input.tool, args: input.args });
|
|
1178
|
+
}
|
|
1179
|
+
var TIMEOUT_MS = 5e3;
|
|
1180
|
+
function timeoutFallback(reason) {
|
|
1181
|
+
return {
|
|
1182
|
+
category: "Low-Risk Actions",
|
|
1183
|
+
severity: "medium",
|
|
1184
|
+
justification: reason,
|
|
1185
|
+
recommended_action: "prompt",
|
|
1186
|
+
specific_concerns: []
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
var OUTPUT_INSTRUCTION = `
|
|
1190
|
+
## Your Response
|
|
1191
|
+
|
|
1192
|
+
Return ONLY a single JSON object \u2014 no markdown fences, no prose before or after.
|
|
1193
|
+
The object MUST match this TypeScript type exactly:
|
|
1194
|
+
|
|
1195
|
+
type Assessment = {
|
|
1196
|
+
category: "Data Exfiltration" | "Credential Probing" | "Persistent Security Weakening" | "Destructive Actions" | "Low-Risk Actions";
|
|
1197
|
+
severity: "low" | "medium" | "high" | "critical";
|
|
1198
|
+
justification: string; // one sentence, <= 240 chars, cite the specific rule you applied
|
|
1199
|
+
recommended_action: "auto-allow" | "prompt" | "deny";
|
|
1200
|
+
specific_concerns: string[]; // 0\u20136 short bullets, each <= 240 chars
|
|
1201
|
+
};
|
|
1202
|
+
|
|
1203
|
+
Severity \u2192 recommended_action mapping (you MUST obey this):
|
|
1204
|
+
- "low" \u2192 "auto-allow"
|
|
1205
|
+
- "medium" \u2192 "prompt"
|
|
1206
|
+
- "high" \u2192 "prompt"
|
|
1207
|
+
- "critical" \u2192 "deny"
|
|
1208
|
+
|
|
1209
|
+
If you are uncertain, return "medium" + "prompt". Do NOT invent new severity or
|
|
1210
|
+
action values. Do NOT wrap the JSON in markdown. Output the raw object only.
|
|
1211
|
+
`;
|
|
1212
|
+
function extractJson(raw) {
|
|
1213
|
+
const trimmed = raw.trim();
|
|
1214
|
+
const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
|
1215
|
+
const body = fenced?.[1] ?? trimmed;
|
|
1216
|
+
const start = body.indexOf("{");
|
|
1217
|
+
const end = body.lastIndexOf("}");
|
|
1218
|
+
if (start < 0 || end <= start) {
|
|
1219
|
+
return JSON.parse(body);
|
|
1220
|
+
}
|
|
1221
|
+
return JSON.parse(body.slice(start, end + 1));
|
|
1222
|
+
}
|
|
1223
|
+
async function assessRisk(input, model) {
|
|
1224
|
+
const key = cacheKey(input);
|
|
1225
|
+
const cached = sessionCache.get(key);
|
|
1226
|
+
if (cached) return cached;
|
|
1227
|
+
let policy;
|
|
1228
|
+
try {
|
|
1229
|
+
policy = await loadPolicy();
|
|
1230
|
+
} catch {
|
|
1231
|
+
policy = GUARDIAN_POLICY_EMBEDDED;
|
|
1232
|
+
}
|
|
1233
|
+
const system = `${policy}
|
|
1234
|
+
${OUTPUT_INSTRUCTION}`;
|
|
1235
|
+
const userPayload = {
|
|
1236
|
+
tool: input.tool,
|
|
1237
|
+
args: input.args,
|
|
1238
|
+
context: input.context ?? null
|
|
1239
|
+
};
|
|
1240
|
+
const user = `Score this proposed tool call:
|
|
1241
|
+
|
|
1242
|
+
\`\`\`json
|
|
1243
|
+
${JSON.stringify(
|
|
1244
|
+
userPayload,
|
|
1245
|
+
null,
|
|
1246
|
+
2
|
|
1247
|
+
)}
|
|
1248
|
+
\`\`\``;
|
|
1249
|
+
const controller = new AbortController();
|
|
1250
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
1251
|
+
let rawText;
|
|
1252
|
+
try {
|
|
1253
|
+
const result = await generateText({
|
|
1254
|
+
model,
|
|
1255
|
+
system,
|
|
1256
|
+
prompt: user,
|
|
1257
|
+
maxTokens: 512,
|
|
1258
|
+
temperature: 0,
|
|
1259
|
+
abortSignal: controller.signal
|
|
1260
|
+
});
|
|
1261
|
+
rawText = result.text ?? "";
|
|
1262
|
+
} catch (err) {
|
|
1263
|
+
clearTimeout(timer);
|
|
1264
|
+
const reason = err?.name === "AbortError" || controller.signal.aborted ? "Guardian timeout \u2014 defaulting to prompt." : `Guardian unavailable (${err?.message ?? "unknown error"}) \u2014 defaulting to prompt.`;
|
|
1265
|
+
const fallback = timeoutFallback(reason);
|
|
1266
|
+
return fallback;
|
|
1267
|
+
} finally {
|
|
1268
|
+
clearTimeout(timer);
|
|
1269
|
+
}
|
|
1270
|
+
let parsed;
|
|
1271
|
+
try {
|
|
1272
|
+
parsed = extractJson(rawText);
|
|
1273
|
+
} catch {
|
|
1274
|
+
return timeoutFallback(
|
|
1275
|
+
"Guardian returned non-JSON \u2014 defaulting to prompt."
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
const validated = AssessmentSchema.safeParse(parsed);
|
|
1279
|
+
if (!validated.success) {
|
|
1280
|
+
return timeoutFallback(
|
|
1281
|
+
"Guardian returned a malformed verdict \u2014 defaulting to prompt."
|
|
1282
|
+
);
|
|
1283
|
+
}
|
|
1284
|
+
const assessment = normalizeAction(validated.data);
|
|
1285
|
+
sessionCache.set(key, assessment);
|
|
1286
|
+
return assessment;
|
|
1287
|
+
}
|
|
1288
|
+
function normalizeAction(a) {
|
|
1289
|
+
let recommended = a.recommended_action;
|
|
1290
|
+
switch (a.severity) {
|
|
1291
|
+
case "low":
|
|
1292
|
+
recommended = "auto-allow";
|
|
1293
|
+
break;
|
|
1294
|
+
case "critical":
|
|
1295
|
+
recommended = "deny";
|
|
1296
|
+
break;
|
|
1297
|
+
case "medium":
|
|
1298
|
+
case "high":
|
|
1299
|
+
if (recommended !== "prompt") recommended = "prompt";
|
|
1300
|
+
break;
|
|
1301
|
+
}
|
|
1302
|
+
return { ...a, recommended_action: recommended };
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// src/coordinator/tools.ts
|
|
1306
|
+
import { z as z4 } from "zod";
|
|
1307
|
+
|
|
1308
|
+
// src/agent/subagent.ts
|
|
1309
|
+
import { streamText } from "ai";
|
|
1310
|
+
|
|
1311
|
+
// src/agent/builtins/index.ts
|
|
1312
|
+
import { readdirSync, readFileSync, existsSync } from "fs";
|
|
1313
|
+
import { dirname, join } from "path";
|
|
1314
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1315
|
+
import { parse as parseToml } from "smol-toml";
|
|
1316
|
+
var cache = null;
|
|
1317
|
+
function resolveBuiltinsDir() {
|
|
1318
|
+
const here = dirname(fileURLToPath2(import.meta.url));
|
|
1319
|
+
const candidates = [
|
|
1320
|
+
here,
|
|
1321
|
+
// src/agent/builtins (dev) or wherever co-located
|
|
1322
|
+
join(here, "builtins"),
|
|
1323
|
+
// dist layout with copied builtins/ subdir
|
|
1324
|
+
join(here, "..", "builtins"),
|
|
1325
|
+
// one level up (e.g. dist/ adjacent to builtins/)
|
|
1326
|
+
join(here, "..", "..", "src", "agent", "builtins")
|
|
1327
|
+
// from dist/ back into src/
|
|
1328
|
+
];
|
|
1329
|
+
for (const dir of candidates) {
|
|
1330
|
+
try {
|
|
1331
|
+
if (existsSync(dir) && readdirSync(dir).some((f) => f.endsWith(".toml"))) {
|
|
1332
|
+
return dir;
|
|
1333
|
+
}
|
|
1334
|
+
} catch {
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
return null;
|
|
1338
|
+
}
|
|
1339
|
+
function asString(v, field, file) {
|
|
1340
|
+
if (typeof v !== "string" || v.trim() === "") {
|
|
1341
|
+
throw new Error(`builtin agent ${file}: field "${field}" must be a non-empty string`);
|
|
1342
|
+
}
|
|
1343
|
+
return v;
|
|
1344
|
+
}
|
|
1345
|
+
function asStringArray(v, field, file) {
|
|
1346
|
+
if (!Array.isArray(v) || !v.every((x) => typeof x === "string")) {
|
|
1347
|
+
throw new Error(`builtin agent ${file}: field "${field}" must be an array of strings`);
|
|
1348
|
+
}
|
|
1349
|
+
return v;
|
|
1350
|
+
}
|
|
1351
|
+
function asPositiveInt(v, field, file, fallback) {
|
|
1352
|
+
if (v === void 0 || v === null) return fallback;
|
|
1353
|
+
if (typeof v === "number" && Number.isInteger(v) && v > 0) return v;
|
|
1354
|
+
if (typeof v === "bigint" && v > 0n) return Number(v);
|
|
1355
|
+
throw new Error(`builtin agent ${file}: field "${field}" must be a positive integer`);
|
|
1356
|
+
}
|
|
1357
|
+
function parseBuiltinFile(filePath, fileName) {
|
|
1358
|
+
let raw;
|
|
1359
|
+
try {
|
|
1360
|
+
raw = readFileSync(filePath, "utf8");
|
|
1361
|
+
} catch (err) {
|
|
1362
|
+
console.warn(` [builtins] could not read ${fileName}: ${err.message}`);
|
|
1363
|
+
return null;
|
|
1364
|
+
}
|
|
1365
|
+
let doc;
|
|
1366
|
+
try {
|
|
1367
|
+
doc = parseToml(raw);
|
|
1368
|
+
} catch (err) {
|
|
1369
|
+
console.warn(` [builtins] malformed TOML in ${fileName}: ${err.message}`);
|
|
1370
|
+
return null;
|
|
1371
|
+
}
|
|
1372
|
+
const a = doc.agent;
|
|
1373
|
+
if (!a || typeof a !== "object") {
|
|
1374
|
+
console.warn(` [builtins] ${fileName}: missing [agent] table`);
|
|
1375
|
+
return null;
|
|
1376
|
+
}
|
|
1377
|
+
try {
|
|
1378
|
+
const name = asString(a.name, "agent.name", fileName).toLowerCase();
|
|
1379
|
+
const description = asString(a.description, "agent.description", fileName);
|
|
1380
|
+
const tools = asStringArray(a.tools, "agent.tools", fileName);
|
|
1381
|
+
const maxIterations = asPositiveInt(a.max_iterations, "agent.max_iterations", fileName, 20);
|
|
1382
|
+
const prompt = asString(
|
|
1383
|
+
a.prompt?.developer_instructions,
|
|
1384
|
+
"agent.prompt.developer_instructions",
|
|
1385
|
+
fileName
|
|
1386
|
+
);
|
|
1387
|
+
return { name, description, tools, maxIterations, prompt };
|
|
1388
|
+
} catch (err) {
|
|
1389
|
+
console.warn(` [builtins] ${err.message}`);
|
|
1390
|
+
return null;
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
function loadBuiltinAgents() {
|
|
1394
|
+
if (cache) return cache;
|
|
1395
|
+
const dir = resolveBuiltinsDir();
|
|
1396
|
+
const map = /* @__PURE__ */ new Map();
|
|
1397
|
+
if (!dir) {
|
|
1398
|
+
console.warn(" [builtins] could not locate builtins directory; no built-in agents loaded");
|
|
1399
|
+
cache = map;
|
|
1400
|
+
return map;
|
|
1401
|
+
}
|
|
1402
|
+
let entries = [];
|
|
1403
|
+
try {
|
|
1404
|
+
entries = readdirSync(dir).filter((f) => f.endsWith(".toml"));
|
|
1405
|
+
} catch (err) {
|
|
1406
|
+
console.warn(` [builtins] could not list ${dir}: ${err.message}`);
|
|
1407
|
+
cache = map;
|
|
1408
|
+
return map;
|
|
1409
|
+
}
|
|
1410
|
+
for (const file of entries) {
|
|
1411
|
+
const agent = parseBuiltinFile(join(dir, file), file);
|
|
1412
|
+
if (!agent) continue;
|
|
1413
|
+
if (map.has(agent.name)) {
|
|
1414
|
+
console.warn(` [builtins] duplicate agent name "${agent.name}" in ${file} (keeping first)`);
|
|
1415
|
+
continue;
|
|
1416
|
+
}
|
|
1417
|
+
map.set(agent.name, agent);
|
|
1418
|
+
}
|
|
1419
|
+
cache = map;
|
|
1420
|
+
return map;
|
|
1421
|
+
}
|
|
1422
|
+
function getBuiltinAgent(name) {
|
|
1423
|
+
return loadBuiltinAgents().get(name.toLowerCase());
|
|
1424
|
+
}
|
|
1425
|
+
function listBuiltinAgents() {
|
|
1426
|
+
return [...loadBuiltinAgents().values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// src/agent/subagent.ts
|
|
1430
|
+
var SUBAGENT_PROMPTS = {
|
|
1431
|
+
explore: `You are an exploration agent. Your job is to quickly search and analyze a codebase to answer questions.
|
|
1432
|
+
You have access to read, grep, and glob tools. Use them efficiently \u2014 search broadly first, then narrow down.
|
|
1433
|
+
Be thorough but concise. Return your findings as a structured summary.
|
|
1434
|
+
Do NOT modify any files. Only read and search.`,
|
|
1435
|
+
plan: `You are a planning agent. Your job is to design implementation strategies.
|
|
1436
|
+
Analyze the codebase, identify the files that need changes, and produce a step-by-step plan.
|
|
1437
|
+
Consider edge cases, existing patterns, and potential risks.
|
|
1438
|
+
Do NOT implement anything. Only plan and recommend.
|
|
1439
|
+
Output a structured plan with:
|
|
1440
|
+
1. Summary of approach
|
|
1441
|
+
2. Files to modify (with specific changes)
|
|
1442
|
+
3. New files to create (if any)
|
|
1443
|
+
4. Testing strategy
|
|
1444
|
+
5. Risks and considerations`,
|
|
1445
|
+
general: `You are a general-purpose worker agent. Complete the task assigned to you.
|
|
1446
|
+
You have full access to all tools. Work autonomously and return results when done.
|
|
1447
|
+
Be efficient \u2014 use grep/glob to find things before reading entire files.
|
|
1448
|
+
If you encounter errors, try to resolve them yourself before reporting back.`
|
|
1449
|
+
};
|
|
1450
|
+
var EXPLORE_TOOL_FILTER = /* @__PURE__ */ new Set(["read", "grep", "glob", "web_fetch"]);
|
|
1451
|
+
var PLAN_TOOL_FILTER = /* @__PURE__ */ new Set(["read", "grep", "glob", "git", "web_fetch"]);
|
|
1452
|
+
var subagentCounter = 0;
|
|
1453
|
+
async function spawnSubagent(config) {
|
|
1454
|
+
const { id, type, prompt, model, toolContext, maxIterations = 15 } = config;
|
|
1455
|
+
if (type === "builtin") {
|
|
1456
|
+
return {
|
|
1457
|
+
id,
|
|
1458
|
+
type,
|
|
1459
|
+
text: "",
|
|
1460
|
+
toolCalls: 0,
|
|
1461
|
+
iterations: 0,
|
|
1462
|
+
error: "use spawnBuiltinAgent() for built-in agents, not spawnSubagent()"
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
config.onStatus?.(id, `Starting ${type} agent...`);
|
|
1466
|
+
const subagentCtx = {
|
|
1467
|
+
...toolContext,
|
|
1468
|
+
permissionSurface: "subagent",
|
|
1469
|
+
subagentId: id,
|
|
1470
|
+
requireConfirm: false
|
|
1471
|
+
};
|
|
1472
|
+
const allTools = buildToolMap(subagentCtx);
|
|
1473
|
+
const tools = {};
|
|
1474
|
+
if (type === "explore") {
|
|
1475
|
+
for (const [name, tool2] of Object.entries(allTools)) {
|
|
1476
|
+
if (EXPLORE_TOOL_FILTER.has(name)) tools[name] = tool2;
|
|
1477
|
+
}
|
|
1478
|
+
} else if (type === "plan") {
|
|
1479
|
+
for (const [name, tool2] of Object.entries(allTools)) {
|
|
1480
|
+
if (PLAN_TOOL_FILTER.has(name)) tools[name] = tool2;
|
|
1481
|
+
}
|
|
1482
|
+
} else {
|
|
1483
|
+
Object.assign(tools, allTools);
|
|
1484
|
+
}
|
|
1485
|
+
const systemPrompt = `${SUBAGENT_PROMPTS[type]}
|
|
1486
|
+
|
|
1487
|
+
## Working Directory
|
|
1488
|
+
${toolContext.cwd}
|
|
1489
|
+
|
|
1490
|
+
## Available Tools
|
|
1491
|
+
${Object.keys(tools).map((n) => `- ${n}`).join("\n")}`;
|
|
1492
|
+
const messages = [
|
|
1493
|
+
{ role: "user", content: prompt }
|
|
1494
|
+
];
|
|
1495
|
+
let iterations = 0;
|
|
1496
|
+
let totalToolCalls = 0;
|
|
1497
|
+
try {
|
|
1498
|
+
while (iterations < maxIterations) {
|
|
1499
|
+
iterations++;
|
|
1500
|
+
config.onStatus?.(id, `Iteration ${iterations}/${maxIterations}...`);
|
|
1501
|
+
const result = streamText({
|
|
1502
|
+
model,
|
|
1503
|
+
system: systemPrompt,
|
|
1504
|
+
messages,
|
|
1505
|
+
tools,
|
|
1506
|
+
maxSteps: 1
|
|
1507
|
+
});
|
|
1508
|
+
let fullText = "";
|
|
1509
|
+
const toolCalls = [];
|
|
1510
|
+
const toolResults = [];
|
|
1511
|
+
for await (const event of result.fullStream) {
|
|
1512
|
+
if (event.type === "text-delta") {
|
|
1513
|
+
fullText += event.textDelta;
|
|
1514
|
+
} else if (event.type === "tool-call") {
|
|
1515
|
+
toolCalls.push({
|
|
1516
|
+
toolCallId: event.toolCallId,
|
|
1517
|
+
toolName: event.toolName,
|
|
1518
|
+
args: event.args
|
|
1519
|
+
});
|
|
1520
|
+
config.onStatus?.(id, `${event.toolName}(...)`);
|
|
1521
|
+
}
|
|
1522
|
+
const evt = event;
|
|
1523
|
+
if (evt.type === "tool-result") {
|
|
1524
|
+
toolResults.push({
|
|
1525
|
+
toolCallId: evt.toolCallId,
|
|
1526
|
+
toolName: toolCalls.find((tc) => tc.toolCallId === evt.toolCallId)?.toolName ?? "unknown",
|
|
1527
|
+
result: evt.result
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
totalToolCalls += toolCalls.length;
|
|
1532
|
+
if (toolCalls.length > 0) {
|
|
1533
|
+
messages.push({
|
|
1534
|
+
role: "assistant",
|
|
1535
|
+
content: [
|
|
1536
|
+
...fullText ? [{ type: "text", text: fullText }] : [],
|
|
1537
|
+
...toolCalls.map((tc) => ({
|
|
1538
|
+
type: "tool-call",
|
|
1539
|
+
toolCallId: tc.toolCallId,
|
|
1540
|
+
toolName: tc.toolName,
|
|
1541
|
+
args: tc.args
|
|
1542
|
+
}))
|
|
1543
|
+
]
|
|
1544
|
+
});
|
|
1545
|
+
messages.push({
|
|
1546
|
+
role: "tool",
|
|
1547
|
+
content: toolResults.map((tr) => ({
|
|
1548
|
+
type: "tool-result",
|
|
1549
|
+
toolCallId: tr.toolCallId,
|
|
1550
|
+
toolName: tr.toolName,
|
|
1551
|
+
result: tr.result
|
|
1552
|
+
}))
|
|
1553
|
+
});
|
|
1554
|
+
continue;
|
|
1555
|
+
}
|
|
1556
|
+
config.onStatus?.(id, "Complete");
|
|
1557
|
+
return {
|
|
1558
|
+
id,
|
|
1559
|
+
type,
|
|
1560
|
+
text: fullText,
|
|
1561
|
+
toolCalls: totalToolCalls,
|
|
1562
|
+
iterations
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
config.onStatus?.(id, "Max iterations reached");
|
|
1566
|
+
return {
|
|
1567
|
+
id,
|
|
1568
|
+
type,
|
|
1569
|
+
text: "[Subagent reached max iterations]",
|
|
1570
|
+
toolCalls: totalToolCalls,
|
|
1571
|
+
iterations
|
|
1572
|
+
};
|
|
1573
|
+
} catch (err) {
|
|
1574
|
+
config.onStatus?.(id, `Error: ${err.message}`);
|
|
1575
|
+
return {
|
|
1576
|
+
id,
|
|
1577
|
+
type,
|
|
1578
|
+
text: "",
|
|
1579
|
+
toolCalls: totalToolCalls,
|
|
1580
|
+
iterations,
|
|
1581
|
+
error: err.message
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
function nextSubagentId(type) {
|
|
1586
|
+
return `${type}-${++subagentCounter}`;
|
|
1587
|
+
}
|
|
1588
|
+
async function spawnBuiltinAgent(config) {
|
|
1589
|
+
const { agentName, prompt, model, toolContext } = config;
|
|
1590
|
+
const agent = getBuiltinAgent(agentName);
|
|
1591
|
+
const id = config.id ?? `${agentName.toLowerCase()}-${++subagentCounter}`;
|
|
1592
|
+
if (!agent) {
|
|
1593
|
+
return {
|
|
1594
|
+
id,
|
|
1595
|
+
type: "builtin",
|
|
1596
|
+
text: "",
|
|
1597
|
+
toolCalls: 0,
|
|
1598
|
+
iterations: 0,
|
|
1599
|
+
error: `unknown built-in agent "${agentName}"`
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
const maxIterations = config.maxIterations ?? agent.maxIterations;
|
|
1603
|
+
config.onStatus?.(id, `Starting built-in agent ${agent.name}...`);
|
|
1604
|
+
const subagentCtx = {
|
|
1605
|
+
...toolContext,
|
|
1606
|
+
permissionSurface: "subagent",
|
|
1607
|
+
subagentId: id,
|
|
1608
|
+
requireConfirm: false
|
|
1609
|
+
};
|
|
1610
|
+
const allTools = buildToolMap(subagentCtx);
|
|
1611
|
+
const allowed = new Set(agent.tools);
|
|
1612
|
+
const tools = {};
|
|
1613
|
+
for (const [name, t] of Object.entries(allTools)) {
|
|
1614
|
+
if (allowed.has(name)) tools[name] = t;
|
|
1615
|
+
}
|
|
1616
|
+
const systemPrompt = `${agent.prompt}
|
|
1617
|
+
|
|
1618
|
+
## Working Directory
|
|
1619
|
+
${toolContext.cwd}
|
|
1620
|
+
|
|
1621
|
+
## Available Tools
|
|
1622
|
+
${Object.keys(tools).map((n) => `- ${n}`).join("\n")}`;
|
|
1623
|
+
const messages = [{ role: "user", content: prompt }];
|
|
1624
|
+
let iterations = 0;
|
|
1625
|
+
let totalToolCalls = 0;
|
|
1626
|
+
try {
|
|
1627
|
+
while (iterations < maxIterations) {
|
|
1628
|
+
iterations++;
|
|
1629
|
+
config.onStatus?.(id, `Iteration ${iterations}/${maxIterations}...`);
|
|
1630
|
+
const result = streamText({
|
|
1631
|
+
model,
|
|
1632
|
+
system: systemPrompt,
|
|
1633
|
+
messages,
|
|
1634
|
+
tools,
|
|
1635
|
+
maxSteps: 1
|
|
1636
|
+
});
|
|
1637
|
+
let fullText = "";
|
|
1638
|
+
const toolCalls = [];
|
|
1639
|
+
const toolResults = [];
|
|
1640
|
+
for await (const event of result.fullStream) {
|
|
1641
|
+
if (event.type === "text-delta") {
|
|
1642
|
+
fullText += event.textDelta;
|
|
1643
|
+
} else if (event.type === "tool-call") {
|
|
1644
|
+
toolCalls.push({
|
|
1645
|
+
toolCallId: event.toolCallId,
|
|
1646
|
+
toolName: event.toolName,
|
|
1647
|
+
args: event.args
|
|
1648
|
+
});
|
|
1649
|
+
config.onStatus?.(id, `${event.toolName}(...)`);
|
|
1650
|
+
}
|
|
1651
|
+
const evt = event;
|
|
1652
|
+
if (evt.type === "tool-result") {
|
|
1653
|
+
toolResults.push({
|
|
1654
|
+
toolCallId: evt.toolCallId,
|
|
1655
|
+
toolName: toolCalls.find((tc) => tc.toolCallId === evt.toolCallId)?.toolName ?? "unknown",
|
|
1656
|
+
result: evt.result
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
totalToolCalls += toolCalls.length;
|
|
1661
|
+
if (toolCalls.length > 0) {
|
|
1662
|
+
messages.push({
|
|
1663
|
+
role: "assistant",
|
|
1664
|
+
content: [
|
|
1665
|
+
...fullText ? [{ type: "text", text: fullText }] : [],
|
|
1666
|
+
...toolCalls.map((tc) => ({
|
|
1667
|
+
type: "tool-call",
|
|
1668
|
+
toolCallId: tc.toolCallId,
|
|
1669
|
+
toolName: tc.toolName,
|
|
1670
|
+
args: tc.args
|
|
1671
|
+
}))
|
|
1672
|
+
]
|
|
1673
|
+
});
|
|
1674
|
+
messages.push({
|
|
1675
|
+
role: "tool",
|
|
1676
|
+
content: toolResults.map((tr) => ({
|
|
1677
|
+
type: "tool-result",
|
|
1678
|
+
toolCallId: tr.toolCallId,
|
|
1679
|
+
toolName: tr.toolName,
|
|
1680
|
+
result: tr.result
|
|
1681
|
+
}))
|
|
1682
|
+
});
|
|
1683
|
+
continue;
|
|
1684
|
+
}
|
|
1685
|
+
config.onStatus?.(id, "Complete");
|
|
1686
|
+
return {
|
|
1687
|
+
id,
|
|
1688
|
+
type: "builtin",
|
|
1689
|
+
text: fullText,
|
|
1690
|
+
toolCalls: totalToolCalls,
|
|
1691
|
+
iterations
|
|
1692
|
+
};
|
|
1693
|
+
}
|
|
1694
|
+
config.onStatus?.(id, "Max iterations reached");
|
|
1695
|
+
return {
|
|
1696
|
+
id,
|
|
1697
|
+
type: "builtin",
|
|
1698
|
+
text: "[Built-in agent reached max iterations]",
|
|
1699
|
+
toolCalls: totalToolCalls,
|
|
1700
|
+
iterations
|
|
1701
|
+
};
|
|
1702
|
+
} catch (err) {
|
|
1703
|
+
config.onStatus?.(id, `Error: ${err.message}`);
|
|
1704
|
+
return {
|
|
1705
|
+
id,
|
|
1706
|
+
type: "builtin",
|
|
1707
|
+
text: "",
|
|
1708
|
+
toolCalls: totalToolCalls,
|
|
1709
|
+
iterations,
|
|
1710
|
+
error: err.message
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
// src/coordinator/runtime.ts
|
|
1716
|
+
var registry = /* @__PURE__ */ new Map();
|
|
1717
|
+
function registerAgent(entry) {
|
|
1718
|
+
registry.set(entry.agentId, entry);
|
|
1719
|
+
}
|
|
1720
|
+
function getAgent(agentId) {
|
|
1721
|
+
return registry.get(agentId);
|
|
1722
|
+
}
|
|
1723
|
+
function pendingCount() {
|
|
1724
|
+
let n = 0;
|
|
1725
|
+
for (const entry of registry.values()) {
|
|
1726
|
+
if (!entry.delivered) n++;
|
|
1727
|
+
}
|
|
1728
|
+
return n;
|
|
1729
|
+
}
|
|
1730
|
+
function formatTaskNotification(agentId, toolUseId, status, summary, result) {
|
|
1731
|
+
const MAX_RESULT = 8 * 1024;
|
|
1732
|
+
const truncated = result.length > MAX_RESULT ? result.slice(0, MAX_RESULT) + `
|
|
1733
|
+
|
|
1734
|
+
[truncated ${result.length - MAX_RESULT} chars]` : result;
|
|
1735
|
+
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1736
|
+
return `<task-notification>
|
|
1737
|
+
<task-id>${esc(agentId)}</task-id>
|
|
1738
|
+
<tool-use-id>${esc(toolUseId)}</tool-use-id>
|
|
1739
|
+
<status>${status}</status>
|
|
1740
|
+
<summary>${esc(summary)}</summary>
|
|
1741
|
+
<result>${esc(truncated)}</result>
|
|
1742
|
+
</task-notification>`;
|
|
1743
|
+
}
|
|
1744
|
+
async function awaitOneCompletion() {
|
|
1745
|
+
const pending = [];
|
|
1746
|
+
for (const entry of registry.values()) {
|
|
1747
|
+
if (!entry.delivered) pending.push(entry);
|
|
1748
|
+
}
|
|
1749
|
+
if (pending.length === 0) return null;
|
|
1750
|
+
const alreadySettled = pending.find((e) => e.result !== void 0);
|
|
1751
|
+
const winner = alreadySettled ? alreadySettled : await Promise.race(
|
|
1752
|
+
pending.map(
|
|
1753
|
+
(entry) => entry.completionPromise.then(() => entry).catch(() => entry)
|
|
1754
|
+
)
|
|
1755
|
+
);
|
|
1756
|
+
winner.delivered = true;
|
|
1757
|
+
const result = winner.result;
|
|
1758
|
+
const status = winner.status;
|
|
1759
|
+
const summary = status === "completed" ? `Agent "${winner.description}" completed` : status === "failed" ? `Agent "${winner.description}" failed: ${result?.error ?? "unknown error"}` : status === "killed" ? `Agent "${winner.description}" was stopped` : `Agent "${winner.description}" ended (${status})`;
|
|
1760
|
+
const text = result?.text ?? result?.error ?? "";
|
|
1761
|
+
const xml = formatTaskNotification(
|
|
1762
|
+
winner.agentId,
|
|
1763
|
+
winner.toolUseId,
|
|
1764
|
+
status,
|
|
1765
|
+
summary,
|
|
1766
|
+
text
|
|
1767
|
+
);
|
|
1768
|
+
return {
|
|
1769
|
+
agentId: winner.agentId,
|
|
1770
|
+
toolUseId: winner.toolUseId,
|
|
1771
|
+
status,
|
|
1772
|
+
summary,
|
|
1773
|
+
result: text,
|
|
1774
|
+
xml
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
function injectCompletionAsUserMessage(messages, completion) {
|
|
1778
|
+
messages.push({
|
|
1779
|
+
role: "user",
|
|
1780
|
+
content: completion.xml
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
async function pollPendingAgents(messages) {
|
|
1784
|
+
if (pendingCount() === 0) return false;
|
|
1785
|
+
const completion = await awaitOneCompletion();
|
|
1786
|
+
if (!completion) return false;
|
|
1787
|
+
injectCompletionAsUserMessage(messages, completion);
|
|
1788
|
+
return true;
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
// src/coordinator/tools.ts
|
|
1792
|
+
var spawnParameters = z4.object({
|
|
1793
|
+
subagent_type: z4.enum(["explore", "plan", "general", "builtin"]).describe(
|
|
1794
|
+
"Worker profile: explore (read-only recon), plan (planning without edits), general (full tool access), builtin (named TOML-defined agent \u2014 provide builtin_name)."
|
|
1795
|
+
),
|
|
1796
|
+
prompt: z4.string().describe(
|
|
1797
|
+
"Self-contained spec for the worker. Include purpose, file paths, acceptance criteria, and what to report back. Workers cannot see this conversation."
|
|
1798
|
+
),
|
|
1799
|
+
description: z4.string().describe(
|
|
1800
|
+
"Short human-readable label for this worker, shown in status updates and in <task-notification> summaries."
|
|
1801
|
+
),
|
|
1802
|
+
builtin_name: z4.string().optional().describe('Required when subagent_type is "builtin" \u2014 the name of the TOML-defined built-in agent to run.')
|
|
1803
|
+
});
|
|
1804
|
+
var agentSpawnTool = {
|
|
1805
|
+
name: "agent_spawn",
|
|
1806
|
+
description: 'Spawn a worker agent to execute a delegated task. Returns { agent_id, status: "spawned" } immediately \u2014 the worker runs in the background and eventually reports back via a <task-notification> user message. Use subagent_type to pick the worker profile.',
|
|
1807
|
+
parameters: spawnParameters,
|
|
1808
|
+
async execute(params, ctx) {
|
|
1809
|
+
if (!ctx.coordinatorWorkerModel) {
|
|
1810
|
+
return {
|
|
1811
|
+
content: "agent_spawn is unavailable: coordinator mode is active but no worker model was supplied to the tool context. This is a configuration bug \u2014 coordinator mode must set coordinatorWorkerModel.",
|
|
1812
|
+
isError: true
|
|
1813
|
+
};
|
|
1814
|
+
}
|
|
1815
|
+
const { subagent_type, prompt, description, builtin_name } = params;
|
|
1816
|
+
if (subagent_type === "builtin" && !builtin_name) {
|
|
1817
|
+
return {
|
|
1818
|
+
content: 'agent_spawn: builtin_name is required when subagent_type is "builtin".',
|
|
1819
|
+
isError: true
|
|
1820
|
+
};
|
|
1821
|
+
}
|
|
1822
|
+
const agentId = subagent_type === "builtin" ? `${(builtin_name ?? "builtin").toLowerCase()}-${nextSubagentId("general").split("-")[1]}` : nextSubagentId(subagent_type);
|
|
1823
|
+
const abortController = new AbortController();
|
|
1824
|
+
const workerCtx = { ...ctx, coordinatorMode: false };
|
|
1825
|
+
const completionPromise = subagent_type === "builtin" ? spawnBuiltinAgent({
|
|
1826
|
+
id: agentId,
|
|
1827
|
+
agentName: builtin_name,
|
|
1828
|
+
prompt,
|
|
1829
|
+
model: ctx.coordinatorWorkerModel,
|
|
1830
|
+
toolContext: workerCtx
|
|
1831
|
+
}) : spawnSubagent({
|
|
1832
|
+
id: agentId,
|
|
1833
|
+
type: subagent_type,
|
|
1834
|
+
prompt,
|
|
1835
|
+
model: ctx.coordinatorWorkerModel,
|
|
1836
|
+
toolContext: workerCtx
|
|
1837
|
+
});
|
|
1838
|
+
const toolUseId = `spawn-${agentId}`;
|
|
1839
|
+
const entry = {
|
|
1840
|
+
agentId,
|
|
1841
|
+
toolUseId,
|
|
1842
|
+
description,
|
|
1843
|
+
completionPromise,
|
|
1844
|
+
abortController,
|
|
1845
|
+
status: "spawned",
|
|
1846
|
+
delivered: false,
|
|
1847
|
+
startedAt: Date.now()
|
|
1848
|
+
};
|
|
1849
|
+
registerAgent(entry);
|
|
1850
|
+
completionPromise.then((result) => {
|
|
1851
|
+
entry.result = result;
|
|
1852
|
+
if (abortController.signal.aborted) {
|
|
1853
|
+
entry.status = "killed";
|
|
1854
|
+
} else if (result.error) {
|
|
1855
|
+
entry.status = "failed";
|
|
1856
|
+
} else {
|
|
1857
|
+
entry.status = "completed";
|
|
1858
|
+
}
|
|
1859
|
+
}).catch((err) => {
|
|
1860
|
+
entry.result = {
|
|
1861
|
+
id: agentId,
|
|
1862
|
+
type: subagent_type === "builtin" ? "builtin" : subagent_type,
|
|
1863
|
+
text: "",
|
|
1864
|
+
toolCalls: 0,
|
|
1865
|
+
iterations: 0,
|
|
1866
|
+
error: err?.message ?? String(err)
|
|
1867
|
+
};
|
|
1868
|
+
entry.status = abortController.signal.aborted ? "killed" : "failed";
|
|
1869
|
+
});
|
|
1870
|
+
return {
|
|
1871
|
+
content: JSON.stringify({
|
|
1872
|
+
agent_id: agentId,
|
|
1873
|
+
status: "spawned",
|
|
1874
|
+
description,
|
|
1875
|
+
note: "Worker is running. You will receive a <task-notification> when it finishes."
|
|
1876
|
+
})
|
|
1877
|
+
};
|
|
1878
|
+
}
|
|
1879
|
+
};
|
|
1880
|
+
var sendMessageParameters = z4.object({
|
|
1881
|
+
to: z4.string().describe("The agent_id returned by agent_spawn."),
|
|
1882
|
+
message: z4.string().describe("Follow-up instructions for the worker.")
|
|
1883
|
+
});
|
|
1884
|
+
var agentSendMessageTool = {
|
|
1885
|
+
name: "agent_send_message",
|
|
1886
|
+
description: "Continue an existing worker with a follow-up message. NOTE: Notch workers are currently one-shot \u2014 once a worker has finished, this tool cannot resume it. For follow-up work, spawn a fresh worker with a synthesized prompt that includes the previous worker's findings.",
|
|
1887
|
+
parameters: sendMessageParameters,
|
|
1888
|
+
async execute(params, _ctx) {
|
|
1889
|
+
const { to, message: _message } = params;
|
|
1890
|
+
const entry = getAgent(to);
|
|
1891
|
+
if (!entry) {
|
|
1892
|
+
return {
|
|
1893
|
+
content: `agent_send_message: no agent with id "${to}" in the registry. Check the agent_id you got back from agent_spawn.`,
|
|
1894
|
+
isError: true
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
if (entry.status === "completed" || entry.status === "failed" || entry.status === "killed") {
|
|
1898
|
+
return {
|
|
1899
|
+
content: JSON.stringify({
|
|
1900
|
+
status: "unsupported",
|
|
1901
|
+
agent_id: to,
|
|
1902
|
+
agent_status: entry.status,
|
|
1903
|
+
reason: "Notch workers are one-shot. This agent has already finished and cannot be continued. Spawn a fresh worker with a prompt that carries forward the relevant context from the previous worker's result."
|
|
1904
|
+
}),
|
|
1905
|
+
isError: true
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
return {
|
|
1909
|
+
content: JSON.stringify({
|
|
1910
|
+
status: "unsupported",
|
|
1911
|
+
agent_id: to,
|
|
1912
|
+
agent_status: entry.status,
|
|
1913
|
+
reason: "Notch workers cannot receive mid-flight messages in the current runtime. Wait for the <task-notification> for this worker, then spawn a fresh worker with a synthesized follow-up prompt. Alternatively, call agent_stop and respawn with the corrected instructions."
|
|
1914
|
+
}),
|
|
1915
|
+
isError: true
|
|
1916
|
+
};
|
|
1917
|
+
}
|
|
1918
|
+
};
|
|
1919
|
+
var stopParameters = z4.object({
|
|
1920
|
+
id: z4.string().describe("The agent_id of the worker to stop.")
|
|
1921
|
+
});
|
|
1922
|
+
var agentStopTool = {
|
|
1923
|
+
name: "agent_stop",
|
|
1924
|
+
description: 'Abort a running worker. Use when you realize a worker is going in the wrong direction or the user has changed requirements. The worker will still emit a <task-notification> with status="killed".',
|
|
1925
|
+
parameters: stopParameters,
|
|
1926
|
+
async execute(params, _ctx) {
|
|
1927
|
+
const entry = getAgent(params.id);
|
|
1928
|
+
if (!entry) {
|
|
1929
|
+
return {
|
|
1930
|
+
content: `agent_stop: no agent with id "${params.id}" in the registry.`,
|
|
1931
|
+
isError: true
|
|
1932
|
+
};
|
|
1933
|
+
}
|
|
1934
|
+
if (entry.status === "completed" || entry.status === "failed" || entry.status === "killed") {
|
|
1935
|
+
return {
|
|
1936
|
+
content: JSON.stringify({
|
|
1937
|
+
status: "already_finished",
|
|
1938
|
+
agent_id: params.id,
|
|
1939
|
+
agent_status: entry.status
|
|
1940
|
+
})
|
|
1941
|
+
};
|
|
1942
|
+
}
|
|
1943
|
+
entry.abortController.abort();
|
|
1944
|
+
return {
|
|
1945
|
+
content: JSON.stringify({
|
|
1946
|
+
status: "abort_signaled",
|
|
1947
|
+
agent_id: params.id,
|
|
1948
|
+
note: 'Abort signal sent. The worker will finish its current step and then report back with status="killed".'
|
|
1949
|
+
})
|
|
1950
|
+
};
|
|
1951
|
+
}
|
|
1952
|
+
};
|
|
1953
|
+
var COORDINATOR_TOOLS = [
|
|
1954
|
+
agentSpawnTool,
|
|
1955
|
+
agentSendMessageTool,
|
|
1956
|
+
agentStopTool
|
|
1957
|
+
];
|
|
1958
|
+
var COORDINATOR_TOOL_NAMES = new Set(
|
|
1959
|
+
COORDINATOR_TOOLS.map((t) => t.name)
|
|
1960
|
+
);
|
|
1961
|
+
var COORDINATOR_ALLOWED_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
1962
|
+
...COORDINATOR_TOOL_NAMES,
|
|
1963
|
+
"read",
|
|
1964
|
+
"grep",
|
|
1965
|
+
"glob"
|
|
1966
|
+
]);
|
|
1967
|
+
|
|
1968
|
+
// src/permissions/handlers/bash-classifier.ts
|
|
1969
|
+
var DESTRUCTIVE_PATTERNS = [
|
|
1970
|
+
/\brm\s+(-[a-zA-Z]*[rf][a-zA-Z]*|--recursive|--force)/i,
|
|
1971
|
+
/\brm\s+-rf?\s+\//i,
|
|
1972
|
+
/\bsudo\s+rm\b/i,
|
|
1973
|
+
/\bdd\s+if=/i,
|
|
1974
|
+
/\bmkfs\./i,
|
|
1975
|
+
/\bshred\b/i,
|
|
1976
|
+
/\bchmod\s+-R\s+/i,
|
|
1977
|
+
/\bchown\s+-R\s+/i,
|
|
1978
|
+
/:\(\)\s*\{\s*:\|:&\s*\};:/,
|
|
1979
|
+
// fork bomb
|
|
1980
|
+
/\b>\s*\/dev\/sd[a-z]/i,
|
|
1981
|
+
/\bgit\s+push\s+(?:.*\s)?(?:-f|--force|--force-with-lease)\b/i,
|
|
1982
|
+
/\bgit\s+reset\s+--hard\b/i,
|
|
1983
|
+
/\bgit\s+clean\s+-[a-zA-Z]*[fd]/i,
|
|
1984
|
+
/\bgit\s+checkout\s+--\s+\./,
|
|
1985
|
+
/\bnpm\s+publish\b/i,
|
|
1986
|
+
/\byarn\s+publish\b/i,
|
|
1987
|
+
/\bpnpm\s+publish\b/i,
|
|
1988
|
+
/\bdocker\s+(?:rm|rmi|system\s+prune|volume\s+rm)\b/i,
|
|
1989
|
+
/\bkubectl\s+delete\b/i,
|
|
1990
|
+
/\bterraform\s+destroy\b/i,
|
|
1991
|
+
/\bmv\s+\/\s+/i,
|
|
1992
|
+
/\bfind\s+.*\s-delete\b/i,
|
|
1993
|
+
/\bfind\s+.*-exec\s+rm\b/i,
|
|
1994
|
+
/\btruncate\s+-s\s+0/i,
|
|
1995
|
+
/\b(poweroff|shutdown|reboot|halt)\b/i,
|
|
1996
|
+
/\bkillall?\s+-9/i
|
|
1997
|
+
];
|
|
1998
|
+
var NETWORK_PATTERNS = [
|
|
1999
|
+
/\bcurl\b/i,
|
|
2000
|
+
/\bwget\b/i,
|
|
2001
|
+
/\bhttpie?\b/i,
|
|
2002
|
+
/\bnc\b/i,
|
|
2003
|
+
/\bnetcat\b/i,
|
|
2004
|
+
/\bssh\b/i,
|
|
2005
|
+
/\bscp\b/i,
|
|
2006
|
+
/\brsync\s+.*::?/i,
|
|
2007
|
+
/\bftp\b/i,
|
|
2008
|
+
/\bsftp\b/i,
|
|
2009
|
+
/\btelnet\b/i,
|
|
2010
|
+
/\bnmap\b/i,
|
|
2011
|
+
/\bping\b/i,
|
|
2012
|
+
/\bdig\b/i,
|
|
2013
|
+
/\bnslookup\b/i,
|
|
2014
|
+
/\bhost\s+[a-z0-9.-]+\.[a-z]+/i,
|
|
2015
|
+
/\bgit\s+(?:clone|fetch|pull|push)\b/i,
|
|
2016
|
+
/\bnpm\s+(?:install|i|update|audit|fund)\b/i,
|
|
2017
|
+
/\bpip\s+install\b/i,
|
|
2018
|
+
/\bapt(?:-get)?\s+(?:install|update|upgrade)\b/i,
|
|
2019
|
+
/\bbrew\s+(?:install|update|upgrade)\b/i,
|
|
2020
|
+
/\b(?:python|python3|node|bun)\s+-c\s+.*(?:urllib|requests|fetch|http)/i,
|
|
2021
|
+
/\bcurl.*\|\s*(?:sh|bash|zsh|fish)\b/i,
|
|
2022
|
+
// explicit "pipe to shell"
|
|
2023
|
+
/\bwget.*\|\s*(?:sh|bash|zsh|fish)\b/i
|
|
2024
|
+
];
|
|
2025
|
+
var SAFE_PATTERNS = [
|
|
2026
|
+
/^\s*ls(\s|$)/i,
|
|
2027
|
+
/^\s*pwd(\s|$)/,
|
|
2028
|
+
/^\s*whoami(\s|$)/,
|
|
2029
|
+
/^\s*id(\s|$)/,
|
|
2030
|
+
/^\s*echo\s/i,
|
|
2031
|
+
/^\s*printf\s/i,
|
|
2032
|
+
/^\s*cat\s/i,
|
|
2033
|
+
/^\s*head\s/i,
|
|
2034
|
+
/^\s*tail\s/i,
|
|
2035
|
+
/^\s*wc\s/i,
|
|
2036
|
+
/^\s*file\s/i,
|
|
2037
|
+
/^\s*stat\s/i,
|
|
2038
|
+
/^\s*du\s/i,
|
|
2039
|
+
/^\s*df\s/i,
|
|
2040
|
+
/^\s*which\s/i,
|
|
2041
|
+
/^\s*type\s/i,
|
|
2042
|
+
/^\s*env(\s|$)/i,
|
|
2043
|
+
/^\s*date(\s|$)/i,
|
|
2044
|
+
/^\s*uname/i,
|
|
2045
|
+
/^\s*hostname(\s|$)/i,
|
|
2046
|
+
/^\s*uptime(\s|$)/i,
|
|
2047
|
+
/^\s*history(\s|$)/i,
|
|
2048
|
+
/^\s*git\s+(status|log|diff|show|branch|blame|config\s+--get|remote\s+-v|stash\s+list|tag\s+-l|ls-files)\b/i,
|
|
2049
|
+
/^\s*node\s+--version/i,
|
|
2050
|
+
/^\s*npm\s+(ls|list|outdated|view|config\s+get|--version|-v|root)\b/i,
|
|
2051
|
+
/^\s*yarn\s+(list|--version)\b/i,
|
|
2052
|
+
/^\s*(python|python3)\s+--version/i,
|
|
2053
|
+
/^\s*pip\s+(list|show|--version)\b/i,
|
|
2054
|
+
/^\s*(rg|ripgrep)\s/i,
|
|
2055
|
+
/^\s*grep\s/i,
|
|
2056
|
+
/^\s*find\s+[^|;&]*(?<!-delete)\s*$/i,
|
|
2057
|
+
// find without -delete/-exec rm
|
|
2058
|
+
/^\s*tree(\s|$)/i,
|
|
2059
|
+
/^\s*jq\s/i,
|
|
2060
|
+
/^\s*awk\s/i,
|
|
2061
|
+
/^\s*sed\s+-n\s/i,
|
|
2062
|
+
// sed in print-only mode
|
|
2063
|
+
/^\s*(make|cargo|go|npm|bun|pnpm|yarn)\s+(test|check|vet|fmt\s+--check|build\s+--dry-run)\b/i,
|
|
2064
|
+
/^\s*(cargo|rustc)\s+--version/i,
|
|
2065
|
+
/^\s*docker\s+(ps|images|logs|inspect|version)\b/i,
|
|
2066
|
+
/^\s*kubectl\s+(get|describe|logs|version|config\s+current-context)\b/i
|
|
2067
|
+
];
|
|
2068
|
+
function classifyBashCommand(command) {
|
|
2069
|
+
const cmd = command.trim();
|
|
2070
|
+
if (!cmd) return { category: "safe" };
|
|
2071
|
+
const head = cmd.split(/[\s;|&]/)[0] ?? "";
|
|
2072
|
+
for (const p of DESTRUCTIVE_PATTERNS) {
|
|
2073
|
+
if (p.test(cmd)) {
|
|
2074
|
+
return { category: "destructive", matchedPattern: p.source, head };
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
for (const p of NETWORK_PATTERNS) {
|
|
2078
|
+
if (p.test(cmd)) {
|
|
2079
|
+
return { category: "network", matchedPattern: p.source, head };
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
for (const p of SAFE_PATTERNS) {
|
|
2083
|
+
if (p.test(cmd)) {
|
|
2084
|
+
return { category: "safe", matchedPattern: p.source, head };
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
return { category: "unknown", head };
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
// src/permissions/handlers/interactive.ts
|
|
2091
|
+
var SESSION_RECENT_MS = 3e4;
|
|
2092
|
+
var recentApprovals = /* @__PURE__ */ new Map();
|
|
2093
|
+
function fingerprintArgs(args) {
|
|
2094
|
+
const keys = Object.keys(args).sort();
|
|
2095
|
+
const parts = [];
|
|
2096
|
+
for (const k of keys) {
|
|
2097
|
+
const v = args[k];
|
|
2098
|
+
const s = typeof v === "string" ? v : JSON.stringify(v);
|
|
2099
|
+
parts.push(`${k}=${s.slice(0, 200)}`);
|
|
2100
|
+
}
|
|
2101
|
+
return parts.join("|");
|
|
2102
|
+
}
|
|
2103
|
+
function makeKey(sessionId, toolName, args) {
|
|
2104
|
+
return `${sessionId}\0${toolName}\0${fingerprintArgs(args)}`;
|
|
2105
|
+
}
|
|
2106
|
+
function recordInteractiveApproval(sessionId, toolName, args) {
|
|
2107
|
+
const key = makeKey(sessionId, toolName, args);
|
|
2108
|
+
recentApprovals.set(key, { key, expires: Date.now() + SESSION_RECENT_MS });
|
|
2109
|
+
}
|
|
2110
|
+
function wasRecentlyApproved(sessionId, toolName, args) {
|
|
2111
|
+
const key = makeKey(sessionId, toolName, args);
|
|
2112
|
+
const hit = recentApprovals.get(key);
|
|
2113
|
+
if (!hit) return false;
|
|
2114
|
+
if (hit.expires < Date.now()) {
|
|
2115
|
+
recentApprovals.delete(key);
|
|
2116
|
+
return false;
|
|
2117
|
+
}
|
|
2118
|
+
return true;
|
|
2119
|
+
}
|
|
2120
|
+
var interactiveHandler = {
|
|
2121
|
+
surface: "interactive",
|
|
2122
|
+
check: async (toolName, args, baseDecision, ctx) => {
|
|
2123
|
+
if (baseDecision === "allow") {
|
|
2124
|
+
if (toolName === "shell" && typeof args.command === "string") {
|
|
2125
|
+
const pending = async () => {
|
|
2126
|
+
const cls = classifyBashCommand(args.command);
|
|
2127
|
+
if (cls.category === "destructive") {
|
|
2128
|
+
return {
|
|
2129
|
+
outcome: "prompt",
|
|
2130
|
+
reason: `classifier flagged destructive command: ${cls.matchedPattern}`
|
|
2131
|
+
};
|
|
2132
|
+
}
|
|
2133
|
+
return { outcome: "allow" };
|
|
2134
|
+
};
|
|
2135
|
+
return { outcome: "allow", pendingClassifierCheck: pending };
|
|
2136
|
+
}
|
|
2137
|
+
return { outcome: "allow" };
|
|
2138
|
+
}
|
|
2139
|
+
if (baseDecision === "deny") {
|
|
2140
|
+
return { outcome: "deny", reason: "denied by permission config" };
|
|
2141
|
+
}
|
|
2142
|
+
if (wasRecentlyApproved(ctx.sessionId, toolName, args)) {
|
|
2143
|
+
return {
|
|
2144
|
+
outcome: "allow",
|
|
2145
|
+
silent: true,
|
|
2146
|
+
reason: "session-recent approval (within 30s)"
|
|
2147
|
+
};
|
|
2148
|
+
}
|
|
2149
|
+
return { outcome: "prompt" };
|
|
2150
|
+
}
|
|
2151
|
+
};
|
|
2152
|
+
|
|
2153
|
+
// src/permissions/handlers/one-shot.ts
|
|
2154
|
+
var oneShotHandler = {
|
|
2155
|
+
surface: "one-shot",
|
|
2156
|
+
check: async (toolName, _args, baseDecision, ctx) => {
|
|
2157
|
+
if (baseDecision === "allow") return { outcome: "allow" };
|
|
2158
|
+
if (baseDecision === "deny") {
|
|
2159
|
+
return { outcome: "deny", reason: "denied by permission config" };
|
|
2160
|
+
}
|
|
2161
|
+
if (ctx.autoConfirm) {
|
|
2162
|
+
return {
|
|
2163
|
+
outcome: "allow",
|
|
2164
|
+
silent: true,
|
|
2165
|
+
reason: "--yes flag set; auto-confirming one-shot"
|
|
2166
|
+
};
|
|
2167
|
+
}
|
|
2168
|
+
const count = (ctx.denialCounter.get(toolName) ?? 0) + 1;
|
|
2169
|
+
ctx.denialCounter.set(toolName, count);
|
|
2170
|
+
return {
|
|
2171
|
+
outcome: "deny",
|
|
2172
|
+
silent: true,
|
|
2173
|
+
reason: "one-shot mode denies prompt-level tools without --yes",
|
|
2174
|
+
tailNotification: `one-shot: denied ${toolName} (pass --yes to auto-approve)`
|
|
2175
|
+
};
|
|
2176
|
+
}
|
|
2177
|
+
};
|
|
2178
|
+
|
|
2179
|
+
// src/permissions/handlers/coordinator.ts
|
|
2180
|
+
var COORDINATOR_ALLOWED = /* @__PURE__ */ new Set([
|
|
2181
|
+
"agent_spawn",
|
|
2182
|
+
"agent_send_message",
|
|
2183
|
+
"agent_stop",
|
|
2184
|
+
"read",
|
|
2185
|
+
"grep",
|
|
2186
|
+
"glob"
|
|
2187
|
+
]);
|
|
2188
|
+
var coordinatorHandler = {
|
|
2189
|
+
surface: "coordinator",
|
|
2190
|
+
check: async (toolName, _args, baseDecision, _ctx) => {
|
|
2191
|
+
if (COORDINATOR_ALLOWED.has(toolName)) {
|
|
2192
|
+
if (baseDecision === "deny") {
|
|
2193
|
+
return { outcome: "deny", reason: "coordinator tool explicitly denied by config" };
|
|
2194
|
+
}
|
|
2195
|
+
return { outcome: "allow", silent: true };
|
|
2196
|
+
}
|
|
2197
|
+
if (baseDecision === "deny") {
|
|
2198
|
+
return { outcome: "deny", reason: "denied by permission config" };
|
|
2199
|
+
}
|
|
2200
|
+
return {
|
|
2201
|
+
outcome: "deny",
|
|
2202
|
+
silent: true,
|
|
2203
|
+
reason: "Coordinator must delegate write operations to a worker.",
|
|
2204
|
+
tailNotification: `coordinator: blocked ${toolName} (delegate to worker)`
|
|
2205
|
+
};
|
|
2206
|
+
}
|
|
2207
|
+
};
|
|
2208
|
+
|
|
2209
|
+
// src/permissions/handlers/subagent.ts
|
|
2210
|
+
var subagentHandler = {
|
|
2211
|
+
surface: "subagent",
|
|
2212
|
+
check: async (toolName, _args, baseDecision, ctx) => {
|
|
2213
|
+
if (baseDecision === "allow") return { outcome: "allow" };
|
|
2214
|
+
if (baseDecision === "deny") {
|
|
2215
|
+
return { outcome: "deny", reason: "denied by permission config" };
|
|
2216
|
+
}
|
|
2217
|
+
const count = (ctx.denialCounter.get(toolName) ?? 0) + 1;
|
|
2218
|
+
ctx.denialCounter.set(toolName, count);
|
|
2219
|
+
const id = ctx.subagentId ?? "unknown";
|
|
2220
|
+
return {
|
|
2221
|
+
outcome: "deny",
|
|
2222
|
+
silent: true,
|
|
2223
|
+
reason: "subagent has no interactive UI to prompt",
|
|
2224
|
+
tailNotification: `subagent ${id}: denied ${toolName} (no UI to prompt)`
|
|
2225
|
+
};
|
|
2226
|
+
}
|
|
2227
|
+
};
|
|
2228
|
+
|
|
2229
|
+
// src/permissions/handlers/auto-mode.ts
|
|
2230
|
+
var AUTO_IMPLICIT_WRITE_KEY = "__auto_mode_implicit_writes__";
|
|
2231
|
+
var AUTO_WRITE_NOTIFY_THRESHOLD = 10;
|
|
2232
|
+
var WRITE_TOOLS = /* @__PURE__ */ new Set(["write", "edit", "apply_patch", "shell", "git"]);
|
|
2233
|
+
function hardDenyReason(toolName, args) {
|
|
2234
|
+
if (toolName === "git") {
|
|
2235
|
+
const op = typeof args.operation === "string" ? args.operation : "";
|
|
2236
|
+
const flags = typeof args.flags === "string" ? args.flags : "";
|
|
2237
|
+
const argsStr = typeof args.args === "string" ? args.args : "";
|
|
2238
|
+
const combined = `${op} ${flags} ${argsStr}`.toLowerCase();
|
|
2239
|
+
if (combined.includes("push") && /(-f\b|--force\b|--force-with-lease\b)/.test(combined)) {
|
|
2240
|
+
return "force-push is never auto-approved";
|
|
2241
|
+
}
|
|
2242
|
+
if (combined.includes("reset") && combined.includes("--hard")) {
|
|
2243
|
+
return "git reset --hard is never auto-approved";
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
if (toolName === "shell" && typeof args.command === "string") {
|
|
2247
|
+
const cmd = args.command;
|
|
2248
|
+
if (/\bnpm\s+publish\b/i.test(cmd) || /\byarn\s+publish\b/i.test(cmd) || /\bpnpm\s+publish\b/i.test(cmd)) {
|
|
2249
|
+
return "package publish is never auto-approved";
|
|
2250
|
+
}
|
|
2251
|
+
if (/\bgit\s+push\b.*\b(?:-f|--force|--force-with-lease)\b/i.test(cmd)) {
|
|
2252
|
+
return "force-push is never auto-approved";
|
|
2253
|
+
}
|
|
2254
|
+
if (/\b(?:rm\s+-rf?|dd\s+if=|mkfs\.|shred)\b/i.test(cmd)) {
|
|
2255
|
+
return "classifier-flagged destructive shell command";
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
return null;
|
|
2259
|
+
}
|
|
2260
|
+
var autoModeHandler = {
|
|
2261
|
+
surface: "auto-mode",
|
|
2262
|
+
check: async (toolName, args, baseDecision, ctx) => {
|
|
2263
|
+
if (baseDecision === "deny") {
|
|
2264
|
+
return { outcome: "deny", reason: "denied by permission config" };
|
|
2265
|
+
}
|
|
2266
|
+
const reason = hardDenyReason(toolName, args);
|
|
2267
|
+
if (reason) {
|
|
2268
|
+
return {
|
|
2269
|
+
outcome: "deny",
|
|
2270
|
+
silent: false,
|
|
2271
|
+
reason,
|
|
2272
|
+
tailNotification: `auto-mode: refused ${toolName} \u2014 ${reason}`
|
|
2273
|
+
};
|
|
2274
|
+
}
|
|
2275
|
+
if (toolName === "shell" && typeof args.command === "string") {
|
|
2276
|
+
const pending = async () => {
|
|
2277
|
+
const cls = classifyBashCommand(args.command);
|
|
2278
|
+
if (cls.category === "destructive") {
|
|
2279
|
+
return {
|
|
2280
|
+
outcome: "deny",
|
|
2281
|
+
silent: false,
|
|
2282
|
+
reason: `classifier flagged destructive command: ${cls.matchedPattern}`,
|
|
2283
|
+
tailNotification: `auto-mode: refused destructive shell command (${cls.head ?? "shell"})`
|
|
2284
|
+
};
|
|
2285
|
+
}
|
|
2286
|
+
return baseDecision === "allow" ? { outcome: "allow", silent: true } : { outcome: "allow", silent: true, reason: "auto-mode implicit approval" };
|
|
2287
|
+
};
|
|
2288
|
+
if (baseDecision === "allow") return { outcome: "allow", pendingClassifierCheck: pending };
|
|
2289
|
+
return { outcome: "allow", silent: true, pendingClassifierCheck: pending };
|
|
2290
|
+
}
|
|
2291
|
+
if (baseDecision === "allow") return { outcome: "allow" };
|
|
2292
|
+
if (WRITE_TOOLS.has(toolName)) {
|
|
2293
|
+
const writes = (ctx.denialCounter.get(AUTO_IMPLICIT_WRITE_KEY) ?? 0) + 1;
|
|
2294
|
+
ctx.denialCounter.set(AUTO_IMPLICIT_WRITE_KEY, writes);
|
|
2295
|
+
if (writes === AUTO_WRITE_NOTIFY_THRESHOLD) {
|
|
2296
|
+
return {
|
|
2297
|
+
outcome: "allow",
|
|
2298
|
+
silent: true,
|
|
2299
|
+
reason: "auto-mode implicit approval",
|
|
2300
|
+
tailNotification: `auto-mode: ${writes} unattended writes this session \u2014 consider reviewing`
|
|
2301
|
+
};
|
|
2302
|
+
}
|
|
2303
|
+
if (writes > AUTO_WRITE_NOTIFY_THRESHOLD && writes % 10 === 0) {
|
|
2304
|
+
return {
|
|
2305
|
+
outcome: "allow",
|
|
2306
|
+
silent: true,
|
|
2307
|
+
reason: "auto-mode implicit approval",
|
|
2308
|
+
tailNotification: `auto-mode: ${writes} unattended writes this session`
|
|
2309
|
+
};
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
return { outcome: "allow", silent: true, reason: "auto-mode implicit approval" };
|
|
2313
|
+
}
|
|
2314
|
+
};
|
|
2315
|
+
|
|
2316
|
+
// src/permissions/handlers/json-mode.ts
|
|
2317
|
+
var jsonModeHandler = {
|
|
2318
|
+
surface: "json-mode",
|
|
2319
|
+
check: async (toolName, _args, baseDecision, ctx) => {
|
|
2320
|
+
if (baseDecision === "allow") return { outcome: "allow" };
|
|
2321
|
+
if (baseDecision === "deny") {
|
|
2322
|
+
return { outcome: "deny", reason: "denied by permission config" };
|
|
2323
|
+
}
|
|
2324
|
+
const count = (ctx.denialCounter.get(toolName) ?? 0) + 1;
|
|
2325
|
+
ctx.denialCounter.set(toolName, count);
|
|
2326
|
+
const payload = JSON.stringify({
|
|
2327
|
+
type: "permission_denied",
|
|
2328
|
+
tool: toolName,
|
|
2329
|
+
reason: "json mode requires explicit allowlist"
|
|
2330
|
+
});
|
|
2331
|
+
return {
|
|
2332
|
+
outcome: "deny",
|
|
2333
|
+
silent: true,
|
|
2334
|
+
reason: "json mode requires explicit allowlist",
|
|
2335
|
+
tailNotification: payload
|
|
2336
|
+
};
|
|
2337
|
+
}
|
|
2338
|
+
};
|
|
2339
|
+
|
|
2340
|
+
// src/permissions/dispatcher.ts
|
|
2341
|
+
var HANDLERS = {
|
|
2342
|
+
interactive: interactiveHandler,
|
|
2343
|
+
"one-shot": oneShotHandler,
|
|
2344
|
+
coordinator: coordinatorHandler,
|
|
2345
|
+
subagent: subagentHandler,
|
|
2346
|
+
"auto-mode": autoModeHandler,
|
|
2347
|
+
"json-mode": jsonModeHandler
|
|
2348
|
+
};
|
|
2349
|
+
function getActiveHandler(surface) {
|
|
2350
|
+
return HANDLERS[surface];
|
|
2351
|
+
}
|
|
2352
|
+
async function runPermissionCheck(toolName, args, baseDecision, surface, ctx) {
|
|
2353
|
+
const handler = getActiveHandler(surface);
|
|
2354
|
+
const initial = await handler.check(toolName, args, baseDecision, ctx);
|
|
2355
|
+
if (initial.tailNotification) {
|
|
2356
|
+
pushTailNotification(initial.tailNotification);
|
|
2357
|
+
}
|
|
2358
|
+
if (!initial.pendingClassifierCheck) return initial;
|
|
2359
|
+
try {
|
|
2360
|
+
const refined = await initial.pendingClassifierCheck();
|
|
2361
|
+
if (refined.tailNotification) pushTailNotification(refined.tailNotification);
|
|
2362
|
+
return {
|
|
2363
|
+
outcome: refined.outcome,
|
|
2364
|
+
reason: refined.reason ?? initial.reason,
|
|
2365
|
+
silent: refined.silent ?? initial.silent,
|
|
2366
|
+
tailNotification: refined.tailNotification ?? initial.tailNotification
|
|
2367
|
+
};
|
|
2368
|
+
} catch {
|
|
2369
|
+
return {
|
|
2370
|
+
outcome: initial.outcome,
|
|
2371
|
+
reason: initial.reason,
|
|
2372
|
+
silent: initial.silent,
|
|
2373
|
+
tailNotification: initial.tailNotification
|
|
2374
|
+
};
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
var tailQueue = [];
|
|
2378
|
+
function pushTailNotification(message) {
|
|
2379
|
+
tailQueue.push(message);
|
|
2380
|
+
}
|
|
2381
|
+
function recordAutoModeDenial(decision) {
|
|
2382
|
+
if (decision.outcome !== "deny") return;
|
|
2383
|
+
const msg = decision.tailNotification ?? decision.reason ?? "auto-mode: tool denied";
|
|
2384
|
+
pushTailNotification(msg);
|
|
2385
|
+
}
|
|
2386
|
+
function drainTailNotifications() {
|
|
2387
|
+
const out = tailQueue.splice(0, tailQueue.length);
|
|
2388
|
+
return out;
|
|
2389
|
+
}
|
|
2390
|
+
var currentSurface = "interactive";
|
|
2391
|
+
function setCurrentSurface(surface) {
|
|
2392
|
+
currentSurface = surface;
|
|
2393
|
+
}
|
|
2394
|
+
function createHandlerContext(options) {
|
|
2395
|
+
return {
|
|
2396
|
+
cwd: options.cwd,
|
|
2397
|
+
sessionId: options.sessionId,
|
|
2398
|
+
denialCounter: /* @__PURE__ */ new Map(),
|
|
2399
|
+
log: options.log,
|
|
2400
|
+
autoConfirm: options.autoConfirm,
|
|
2401
|
+
guardianEnabled: options.guardianEnabled,
|
|
2402
|
+
subagentId: options.subagentId
|
|
2403
|
+
};
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
// src/tools/index.ts
|
|
2407
|
+
var BUILTIN_TOOLS = [
|
|
2408
|
+
readTool,
|
|
2409
|
+
writeTool,
|
|
2410
|
+
editTool,
|
|
2411
|
+
applyPatchTool,
|
|
2412
|
+
shellTool,
|
|
2413
|
+
gitTool,
|
|
2414
|
+
githubTool,
|
|
2415
|
+
grepTool,
|
|
2416
|
+
globTool,
|
|
2417
|
+
webFetchTool,
|
|
2418
|
+
lspTool,
|
|
2419
|
+
notebookTool,
|
|
2420
|
+
taskTool,
|
|
2421
|
+
codeModeExecTool,
|
|
2422
|
+
codeModeWaitTool
|
|
2423
|
+
];
|
|
2424
|
+
var mcpClients = /* @__PURE__ */ new Map();
|
|
2425
|
+
var mcpTools = [];
|
|
2426
|
+
async function initMCPServers(config) {
|
|
2427
|
+
const servers = parseMCPConfig(config);
|
|
2428
|
+
let toolCount = 0;
|
|
2429
|
+
for (const [name, serverConfig] of Object.entries(servers)) {
|
|
2430
|
+
try {
|
|
2431
|
+
const client = new MCPClient(serverConfig, name);
|
|
2432
|
+
await client.connect();
|
|
2433
|
+
mcpClients.set(name, client);
|
|
2434
|
+
const tools = mcpToolsToNotch(client, name);
|
|
2435
|
+
mcpTools.push(...tools);
|
|
2436
|
+
toolCount += tools.length;
|
|
2437
|
+
} catch (err) {
|
|
2438
|
+
console.error(` MCP server '${name}' failed to connect: ${err.message}`);
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
return toolCount;
|
|
2442
|
+
}
|
|
2443
|
+
function disconnectMCPServers() {
|
|
2444
|
+
for (const [, client] of mcpClients) {
|
|
2445
|
+
client.disconnect();
|
|
2446
|
+
}
|
|
2447
|
+
mcpClients.clear();
|
|
2448
|
+
mcpTools = [];
|
|
2449
|
+
}
|
|
2450
|
+
var COORDINATOR_ONLY_TOOLS = [
|
|
2451
|
+
agentSpawnTool,
|
|
2452
|
+
agentSendMessageTool,
|
|
2453
|
+
agentStopTool
|
|
2454
|
+
];
|
|
2455
|
+
function getAllTools(ctx) {
|
|
2456
|
+
const coordinator = !!ctx?.coordinatorMode;
|
|
2457
|
+
if (coordinator) {
|
|
2458
|
+
const allowed = [];
|
|
2459
|
+
for (const t of BUILTIN_TOOLS) {
|
|
2460
|
+
if (COORDINATOR_ALLOWED_TOOL_NAMES.has(t.name)) allowed.push(t);
|
|
2461
|
+
}
|
|
2462
|
+
allowed.push(...COORDINATOR_ONLY_TOOLS);
|
|
2463
|
+
return allowed;
|
|
2464
|
+
}
|
|
2465
|
+
return [...BUILTIN_TOOLS, ...mcpTools, ...pluginManager.getTools()];
|
|
2466
|
+
}
|
|
2467
|
+
function buildToolMap(ctx) {
|
|
2468
|
+
const map = {};
|
|
2469
|
+
for (const t of getAllTools(ctx)) {
|
|
2470
|
+
map[t.name] = tool({
|
|
2471
|
+
description: t.description,
|
|
2472
|
+
parameters: t.parameters,
|
|
2473
|
+
execute: async (params) => {
|
|
2474
|
+
if (ctx.checkPermission) {
|
|
2475
|
+
await ctx.runHook?.("permission-request", { tool: t.name, args: params });
|
|
2476
|
+
const argRecord = params;
|
|
2477
|
+
const baseLevel = ctx.checkPermission(t.name, argRecord);
|
|
2478
|
+
const surface = ctx.permissionSurface ?? "interactive";
|
|
2479
|
+
const handlerCtx = createHandlerContext({
|
|
2480
|
+
cwd: ctx.cwd,
|
|
2481
|
+
sessionId: ctx.permissionSessionId ?? "session",
|
|
2482
|
+
log: ctx.log,
|
|
2483
|
+
autoConfirm: ctx.autoConfirm,
|
|
2484
|
+
guardianEnabled: !!ctx.guardianModel,
|
|
2485
|
+
subagentId: ctx.subagentId
|
|
2486
|
+
});
|
|
2487
|
+
const decision = await runPermissionCheck(t.name, argRecord, baseLevel, surface, handlerCtx);
|
|
2488
|
+
if (decision.outcome === "deny") {
|
|
2489
|
+
if (surface === "auto-mode") recordAutoModeDenial(decision);
|
|
2490
|
+
await ctx.runHook?.("permission-denied", {
|
|
2491
|
+
tool: t.name,
|
|
2492
|
+
args: params,
|
|
2493
|
+
reason: decision.reason ?? "denied-by-handler",
|
|
2494
|
+
surface
|
|
2495
|
+
});
|
|
2496
|
+
return {
|
|
2497
|
+
content: decision.silent ? `Permission denied: ${decision.reason ?? t.name + " blocked by " + surface + " handler"}` : `Permission denied: ${t.name} \u2014 ${decision.reason ?? "denied by permission handler"}.`,
|
|
2498
|
+
isError: true
|
|
2499
|
+
};
|
|
2500
|
+
}
|
|
2501
|
+
if (decision.outcome === "prompt") {
|
|
2502
|
+
let guardian = null;
|
|
2503
|
+
if (ctx.guardianModel) {
|
|
2504
|
+
try {
|
|
2505
|
+
guardian = await assessRisk(
|
|
2506
|
+
{ tool: t.name, args: argRecord },
|
|
2507
|
+
ctx.guardianModel
|
|
2508
|
+
);
|
|
2509
|
+
} catch {
|
|
2510
|
+
guardian = null;
|
|
2511
|
+
}
|
|
2512
|
+
if (guardian?.recommended_action === "deny") {
|
|
2513
|
+
await ctx.runHook?.("permission-denied", {
|
|
2514
|
+
tool: t.name,
|
|
2515
|
+
args: params,
|
|
2516
|
+
reason: "guardian-denied",
|
|
2517
|
+
guardian
|
|
2518
|
+
});
|
|
2519
|
+
return {
|
|
2520
|
+
content: `Guardian blocked this action (${guardian.severity}): ${guardian.justification}`,
|
|
2521
|
+
isError: true
|
|
2522
|
+
};
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
const guardianCleared = guardian?.recommended_action === "auto-allow" && guardian.severity === "low";
|
|
2526
|
+
if (!guardianCleared) {
|
|
2527
|
+
if (!ctx.requireConfirm) {
|
|
2528
|
+
pushTailNotification(`${surface}: prompt required for ${t.name} but no UI (denied)`);
|
|
2529
|
+
await ctx.runHook?.("permission-denied", {
|
|
2530
|
+
tool: t.name,
|
|
2531
|
+
args: params,
|
|
2532
|
+
reason: "no-ui-to-prompt",
|
|
2533
|
+
surface
|
|
2534
|
+
});
|
|
2535
|
+
return {
|
|
2536
|
+
content: `Permission denied: ${t.name} requires a prompt but no interactive UI is attached.`,
|
|
2537
|
+
isError: true
|
|
2538
|
+
};
|
|
2539
|
+
}
|
|
2540
|
+
const paramSummary = Object.entries(argRecord).map(([k, v]) => `${k}=${String(v).slice(0, 80)}`).join(", ");
|
|
2541
|
+
const basePrompt = `Tool ${t.name}(${paramSummary}) requires approval. Proceed?`;
|
|
2542
|
+
const guardianTag = guardian ? `
|
|
2543
|
+
(Guardian: ${guardian.severity} \u2014 ${guardian.justification.slice(0, 200)})` : "";
|
|
2544
|
+
const confirmed = await ctx.confirm(basePrompt + guardianTag);
|
|
2545
|
+
if (!confirmed) {
|
|
2546
|
+
await ctx.runHook?.("permission-denied", { tool: t.name, args: params, reason: "cancelled-by-user" });
|
|
2547
|
+
return { content: "Cancelled by user.", isError: true };
|
|
2548
|
+
}
|
|
2549
|
+
if (surface === "interactive") {
|
|
2550
|
+
recordInteractiveApproval(
|
|
2551
|
+
ctx.permissionSessionId ?? "session",
|
|
2552
|
+
t.name,
|
|
2553
|
+
argRecord
|
|
2554
|
+
);
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
if (ctx.dryRun && ["write", "edit", "apply_patch", "shell", "git"].includes(t.name)) {
|
|
2560
|
+
const paramSummary = JSON.stringify(params, null, 2).slice(0, 500);
|
|
2561
|
+
return {
|
|
2562
|
+
content: `[DRY RUN] Would execute ${t.name}:
|
|
2563
|
+
${paramSummary}`
|
|
2564
|
+
};
|
|
2565
|
+
}
|
|
2566
|
+
await ctx.runHook?.("pre-tool", { tool: t.name, args: params });
|
|
2567
|
+
const result = await t.execute(params, ctx);
|
|
2568
|
+
await ctx.runHook?.("post-tool", {
|
|
2569
|
+
tool: t.name,
|
|
2570
|
+
args: params,
|
|
2571
|
+
result: result.content.slice(0, 500),
|
|
2572
|
+
isError: result.isError ?? false
|
|
2573
|
+
});
|
|
2574
|
+
return result;
|
|
2575
|
+
}
|
|
2576
|
+
});
|
|
2577
|
+
}
|
|
2578
|
+
return map;
|
|
2579
|
+
}
|
|
2580
|
+
function listToolNames(ctx) {
|
|
2581
|
+
return getAllTools(ctx).map((t) => t.name);
|
|
2582
|
+
}
|
|
2583
|
+
function describeTools2(ctx) {
|
|
2584
|
+
return getAllTools(ctx).map(
|
|
2585
|
+
(t) => `- **${t.name}**: ${t.description}`
|
|
2586
|
+
).join("\n");
|
|
2587
|
+
}
|
|
2588
|
+
function mcpToolCount() {
|
|
2589
|
+
return mcpTools.length;
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2592
|
+
export {
|
|
2593
|
+
MCPClient,
|
|
2594
|
+
parseMCPConfig,
|
|
2595
|
+
listBuiltinAgents,
|
|
2596
|
+
spawnSubagent,
|
|
2597
|
+
nextSubagentId,
|
|
2598
|
+
pollPendingAgents,
|
|
2599
|
+
drainTailNotifications,
|
|
2600
|
+
setCurrentSurface,
|
|
2601
|
+
initMCPServers,
|
|
2602
|
+
disconnectMCPServers,
|
|
2603
|
+
buildToolMap,
|
|
2604
|
+
listToolNames,
|
|
2605
|
+
describeTools2 as describeTools,
|
|
2606
|
+
mcpToolCount
|
|
2607
|
+
};
|