@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.
Files changed (44) hide show
  1. package/dist/apply-patch-D5PDUXUC.js +14 -0
  2. package/dist/{auth-S3FIB42I.js → auth-JQX6MHJG.js} +0 -1
  3. package/dist/builtins/archimedes.toml +18 -0
  4. package/dist/builtins/awaiter.toml +18 -0
  5. package/dist/builtins/euclid.toml +18 -0
  6. package/dist/builtins/hypatia.toml +18 -0
  7. package/dist/builtins/kepler.toml +18 -0
  8. package/dist/builtins/plato.toml +18 -0
  9. package/dist/builtins/ptolemy.toml +18 -0
  10. package/dist/builtins/pythagoras.toml +18 -0
  11. package/dist/chunk-3QUV4JEX.js +162 -0
  12. package/dist/chunk-6CZCFY6H.js +98 -0
  13. package/dist/chunk-6U3ZAGYA.js +38 -0
  14. package/dist/chunk-C4CPDDMN.js +246 -0
  15. package/dist/chunk-CQMAVWLJ.js +134 -0
  16. package/dist/chunk-FAULT7VE.js +139 -0
  17. package/dist/chunk-FFB7GK3Y.js +72 -0
  18. package/dist/chunk-GBZGR6ID.js +174 -0
  19. package/dist/chunk-KZAS754V.js +118 -0
  20. package/dist/chunk-O3WZW7GS.js +35 -0
  21. package/dist/chunk-TH6GKC7E.js +315 -0
  22. package/dist/chunk-UR4XL6OM.js +104 -0
  23. package/dist/chunk-W4FAGQFL.js +171 -0
  24. package/dist/chunk-YAYPQTOU.js +53 -0
  25. package/dist/chunk-YBYF7L4A.js +2607 -0
  26. package/dist/{compression-LPFNGAV6.js → compression-UTB2Y4BB.js} +0 -1
  27. package/dist/edit-JEFEK43H.js +6 -0
  28. package/dist/git-5T5TSQTX.js +6 -0
  29. package/dist/github-DWRGWX6U.js +6 -0
  30. package/dist/glob-BI3P4C7Q.js +6 -0
  31. package/dist/grep-VZ3I5GNW.js +6 -0
  32. package/dist/index.js +4398 -3447
  33. package/dist/lsp-UPY6I3L7.js +6 -0
  34. package/dist/notebook-FXJBTSPA.js +6 -0
  35. package/dist/plugins-OG2P75K5.js +6 -0
  36. package/dist/read-OVJG2XKW.js +6 -0
  37. package/dist/server-W7FRCVRZ.js +1477 -0
  38. package/dist/shell-4X545EVN.js +6 -0
  39. package/dist/task-OS3E5F3X.js +10 -0
  40. package/dist/tools-Q7CDHB4K.js +30 -0
  41. package/dist/web-fetch-KNIV3Z3W.js +6 -0
  42. package/dist/write-NNHLOTYK.js +6 -0
  43. package/package.json +5 -4
  44. 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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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
+ };