@freesyntax/notch-cli 0.5.20 → 0.5.22

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 (40) hide show
  1. package/dist/{apply-patch-D5PDUXUC.js → apply-patch-U6K67CMT.js} +1 -0
  2. package/dist/auth-UAMMP5IJ.js +29 -0
  3. package/dist/chunk-4HPRBCSY.js +167 -0
  4. package/dist/chunk-6NKRMZTX.js +198 -0
  5. package/dist/{chunk-YBYF7L4A.js → chunk-EPSOOCNB.js} +1832 -1331
  6. package/dist/chunk-FZVPGJJW.js +511 -0
  7. package/dist/chunk-GFVLHUSS.js +155 -0
  8. package/dist/chunk-J66N6AFH.js +137 -0
  9. package/dist/chunk-JXQ4HZ47.js +544 -0
  10. package/dist/chunk-KCAR5DOB.js +52 -0
  11. package/dist/chunk-KFQGP6VL.js +33 -0
  12. package/dist/chunk-O6AKZ4OH.js +0 -0
  13. package/dist/{chunk-6M6CXXWR.js → chunk-PKZKVOAN.js} +209 -1
  14. package/dist/{chunk-FIFC4V2R.js → chunk-PPEBWOMJ.js} +91 -7
  15. package/dist/compression-YJLWEHCC.js +33 -0
  16. package/dist/config-set-3IWEVZQ4.js +110 -0
  17. package/dist/{edit-JEFEK43H.js → edit-6QYAXVNU.js} +1 -0
  18. package/dist/{git-5T5TSQTX.js → git-DNQ5EELH.js} +1 -0
  19. package/dist/{github-DWRGWX6U.js → github-34T4QQIH.js} +1 -0
  20. package/dist/{glob-BI3P4C7Q.js → glob-XT43LEJ4.js} +1 -0
  21. package/dist/{grep-VZ3I5GNW.js → grep-T2CXYNRI.js} +1 -0
  22. package/dist/index.js +2606 -960
  23. package/dist/{lsp-UPY6I3L7.js → lsp-JXQVU7NP.js} +1 -0
  24. package/dist/model-download-3NDKS3VM.js +176 -0
  25. package/dist/{notebook-FXJBTSPA.js → notebook-MFODW345.js} +1 -0
  26. package/dist/ollama-bench-5V5CCOCQ.js +194 -0
  27. package/dist/ollama-launch-P5KBK7AJ.js +22 -0
  28. package/dist/ollama-usage-3PROM2WC.js +70 -0
  29. package/dist/{plugins-OG2P75K5.js → plugins-PNGRZLFW.js} +1 -0
  30. package/dist/{read-OVJG2XKW.js → read-B64XE7N3.js} +1 -0
  31. package/dist/{server-W7FRCVRZ.js → server-IGOZHW52.js} +17 -15
  32. package/dist/session-index-7FWEVP6E.js +22 -0
  33. package/dist/{shell-4X545EVN.js → shell-BOZTHQUT.js} +1 -0
  34. package/dist/{task-OS3E5F3X.js → task-67G4KLYC.js} +1 -0
  35. package/dist/{tools-Q7CDHB4K.js → tools-XWKCW4RN.js} +4 -1
  36. package/dist/{web-fetch-KNIV3Z3W.js → web-fetch-OTNDICGJ.js} +1 -0
  37. package/dist/{write-NNHLOTYK.js → write-ZOSB7I4J.js} +1 -0
  38. package/package.json +2 -1
  39. package/dist/auth-JQX6MHJG.js +0 -16
  40. package/dist/compression-UTB2Y4BB.js +0 -16
@@ -1,3 +1,6 @@
1
+ import {
2
+ Rollout
3
+ } from "./chunk-6NKRMZTX.js";
1
4
  import {
2
5
  grepTool
3
6
  } from "./chunk-6CZCFY6H.js";
@@ -22,6 +25,10 @@ import {
22
25
  import {
23
26
  pluginManager
24
27
  } from "./chunk-3QUV4JEX.js";
28
+ import {
29
+ init_auth,
30
+ loadCredentials
31
+ } from "./chunk-PPEBWOMJ.js";
25
32
  import {
26
33
  readTool
27
34
  } from "./chunk-CQMAVWLJ.js";
@@ -494,577 +501,1342 @@ error: no running cell with that id (it may have already completed)`,
494
501
  }
495
502
  };
496
503
 
497
- // src/mcp/client.ts
504
+ // src/tools/skill-manage.ts
505
+ init_auth();
498
506
  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);
507
+ var API_BASE_ENV = "NOTCH_API_URL";
508
+ var DEFAULT_API_BASE = "https://freesyntax.dev";
509
+ var AGENT_ID_ENV = "NOTCH_AGENT_ID";
510
+ var ParamsSchema = z2.object({
511
+ action: z2.enum(["create", "edit", "patch", "delete", "toggle", "list"]),
512
+ /**
513
+ * Target agent id. If omitted, falls back to the `NOTCH_AGENT_ID` env var.
514
+ * In Notch Cloud the task-runner sets this env var when the task was
515
+ * created from an agent. For standalone CLI use, pass it explicitly.
516
+ */
517
+ agent_id: z2.string().uuid().optional(),
518
+ /** Required for action=create | edit (optional for edit, keeps existing). */
519
+ name: z2.string().min(1).max(40).optional(),
520
+ label: z2.string().min(1).max(80).optional(),
521
+ category: z2.string().max(40).optional(),
522
+ /** Body of the SKILL.md. Required for create/edit; ignored otherwise. */
523
+ content: z2.string().min(1).max(16 * 1024).optional(),
524
+ /** For patch: appended to the existing body with a `---` separator. */
525
+ appendContent: z2.string().min(1).max(16 * 1024).optional(),
526
+ /** For edit/create/toggle. */
527
+ enabled: z2.boolean().optional(),
528
+ /** For edit/patch/delete/toggle: which existing skill to operate on. */
529
+ skill_id: z2.string().uuid().optional()
530
+ });
531
+ var DESCRIPTION = `Create, edit, patch, delete, toggle, or list durable "skills" (procedural memory) attached to the current agent. Use this when a non-trivial workflow, error-recovery pattern, or learned convention is worth persisting so future sessions inherit it. All writes are scanned server-side; prompt-override/secret/exfil patterns are rejected.
532
+
533
+ Actions:
534
+ - create: new skill from scratch. Requires name, label, content. Optional category.
535
+ - edit: full replace. Requires skill_id + content. Other fields optional.
536
+ - patch: append to existing. Requires skill_id + appendContent.
537
+ - delete: remove. Requires skill_id.
538
+ - toggle: enable/disable without rewriting. Requires skill_id + enabled.
539
+ - list: enumerate the agent's current skills.
540
+
541
+ SKILL.md body should be terse, imperative, and action-oriented. Include triggers ("when X") and the procedure ("then Y"). Avoid prose.`;
542
+ var skillManageTool = {
543
+ name: "skill_manage",
544
+ description: DESCRIPTION,
545
+ parameters: ParamsSchema,
546
+ execute: async (params, ctx) => {
547
+ const creds = await loadCredentials();
548
+ if (!creds) {
549
+ return {
550
+ isError: true,
551
+ content: "Not authenticated. Run `notch login` first so skills can be persisted to your workspace."
552
+ };
604
553
  }
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 {
554
+ const agentId = params.agent_id ?? process.env[AGENT_ID_ENV];
555
+ if (!agentId) {
556
+ return {
557
+ isError: true,
558
+ content: `No agent id available. Pass agent_id explicitly, or ensure the task-runner set ${AGENT_ID_ENV}. For standalone CLI use there is no agent to attach skills to \u2014 use a ~/.notch/skills/ local file instead.`
559
+ };
619
560
  }
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}`);
561
+ const base = process.env[API_BASE_ENV] ?? DEFAULT_API_BASE;
562
+ const url = `${base.replace(/\/+$/, "")}/api/agents/${agentId}/skills/manage`;
563
+ const body = buildRequestBody(params);
564
+ if (!body) {
565
+ return {
566
+ isError: true,
567
+ content: `Action "${params.action}" is missing required fields. Check the parameter docs.`
568
+ };
634
569
  }
635
- this.baseUrl = config.url.replace(/\/$/, "");
636
- this.headers = {
637
- "Content-Type": "application/json",
638
- ...config.headers
639
- };
640
- }
641
- async connect() {
642
570
  try {
643
- const response = await fetch(`${this.baseUrl}`, {
571
+ const res = await fetch(url, {
644
572
  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)
573
+ headers: {
574
+ Authorization: `Bearer ${creds.token}`,
575
+ "Content-Type": "application/json",
576
+ "User-Agent": "notch-cli/skill_manage"
577
+ },
578
+ body: JSON.stringify(body)
657
579
  });
658
- if (!response.ok) {
659
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
580
+ const text = await res.text();
581
+ let parsed = {};
582
+ try {
583
+ parsed = JSON.parse(text);
584
+ } catch {
660
585
  }
661
- this.sendNotification("notifications/initialized", {});
662
- this.connected = true;
586
+ if (!res.ok) {
587
+ const summary = parsed["summary"] ?? String(parsed["error"] ?? res.statusText);
588
+ ctx.log?.(`[skill_manage] ${params.action} \u2192 HTTP ${res.status}: ${summary}`);
589
+ const findings = Array.isArray(parsed["findings"]) ? parsed["findings"] : [];
590
+ return {
591
+ isError: true,
592
+ content: JSON.stringify({
593
+ ok: false,
594
+ status: res.status,
595
+ action: params.action,
596
+ summary,
597
+ findings
598
+ }, null, 2)
599
+ };
600
+ }
601
+ ctx.log?.(`[skill_manage] ${params.action} ok`);
602
+ return {
603
+ content: typeof text === "string" && text.length > 0 ? text : JSON.stringify({ ok: true })
604
+ };
663
605
  } catch (err) {
664
- throw new Error(`MCP HTTP server ${this.name} unreachable: ${err.message}`);
606
+ const message = err instanceof Error ? err.message : String(err);
607
+ return {
608
+ isError: true,
609
+ content: `Network error calling skill_manage: ${message}`
610
+ };
665
611
  }
666
612
  }
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}`);
613
+ };
614
+ function buildRequestBody(p) {
615
+ switch (p.action) {
616
+ case "create": {
617
+ if (!p.name || !p.label || !p.content) return null;
618
+ return {
619
+ action: "create",
620
+ name: p.name,
621
+ label: p.label,
622
+ category: p.category ?? null,
623
+ content: p.content,
624
+ enabled: p.enabled ?? true
625
+ };
684
626
  }
685
- const json = await response.json();
686
- if (json.error) {
687
- throw new Error(`MCP error (${this.name}): ${json.error.message}`);
627
+ case "edit": {
628
+ if (!p.skill_id || !p.content) return null;
629
+ return {
630
+ action: "edit",
631
+ skillId: p.skill_id,
632
+ name: p.name,
633
+ label: p.label,
634
+ category: p.category ?? null,
635
+ content: p.content,
636
+ enabled: p.enabled
637
+ };
688
638
  }
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}`);
639
+ case "patch": {
640
+ if (!p.skill_id || !p.appendContent) return null;
641
+ return {
642
+ action: "patch",
643
+ skillId: p.skill_id,
644
+ appendContent: p.appendContent
645
+ };
717
646
  }
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`);
647
+ case "delete": {
648
+ if (!p.skill_id) return null;
649
+ return { action: "delete", skillId: p.skill_id };
737
650
  }
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);
651
+ case "toggle": {
652
+ if (!p.skill_id || typeof p.enabled !== "boolean") return null;
653
+ return { action: "toggle", skillId: p.skill_id, enabled: p.enabled };
751
654
  }
655
+ case "list":
656
+ return { action: "list" };
752
657
  }
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}`);
658
+ }
659
+
660
+ // src/tools/events.ts
661
+ import { z as z3 } from "zod";
662
+
663
+ // src/permissions/handlers/bash-classifier.ts
664
+ var DESTRUCTIVE_PATTERNS = [
665
+ /\brm\s+(-[a-zA-Z]*[rf][a-zA-Z]*|--recursive|--force)/i,
666
+ /\brm\s+-rf?\s+\//i,
667
+ /\bsudo\s+rm\b/i,
668
+ /\bdd\s+if=/i,
669
+ /\bmkfs\./i,
670
+ /\bshred\b/i,
671
+ /\bchmod\s+-R\s+/i,
672
+ /\bchown\s+-R\s+/i,
673
+ /:\(\)\s*\{\s*:\|:&\s*\};:/,
674
+ // fork bomb
675
+ /\b>\s*\/dev\/sd[a-z]/i,
676
+ /\bgit\s+push\s+(?:.*\s)?(?:-f|--force|--force-with-lease)\b/i,
677
+ /\bgit\s+reset\s+--hard\b/i,
678
+ /\bgit\s+clean\s+-[a-zA-Z]*[fd]/i,
679
+ /\bgit\s+checkout\s+--\s+\./,
680
+ /\bnpm\s+publish\b/i,
681
+ /\byarn\s+publish\b/i,
682
+ /\bpnpm\s+publish\b/i,
683
+ /\bdocker\s+(?:rm|rmi|system\s+prune|volume\s+rm)\b/i,
684
+ /\bkubectl\s+delete\b/i,
685
+ /\bterraform\s+destroy\b/i,
686
+ /\bmv\s+\/\s+/i,
687
+ /\bfind\s+.*\s-delete\b/i,
688
+ /\bfind\s+.*-exec\s+rm\b/i,
689
+ /\btruncate\s+-s\s+0/i,
690
+ /\b(poweroff|shutdown|reboot|halt)\b/i,
691
+ /\bkillall?\s+-9/i
692
+ ];
693
+ var NETWORK_PATTERNS = [
694
+ /\bcurl\b/i,
695
+ /\bwget\b/i,
696
+ /\bhttpie?\b/i,
697
+ /\bnc\b/i,
698
+ /\bnetcat\b/i,
699
+ /\bssh\b/i,
700
+ /\bscp\b/i,
701
+ /\brsync\s+.*::?/i,
702
+ /\bftp\b/i,
703
+ /\bsftp\b/i,
704
+ /\btelnet\b/i,
705
+ /\bnmap\b/i,
706
+ /\bping\b/i,
707
+ /\bdig\b/i,
708
+ /\bnslookup\b/i,
709
+ /\bhost\s+[a-z0-9.-]+\.[a-z]+/i,
710
+ /\bgit\s+(?:clone|fetch|pull|push)\b/i,
711
+ /\bnpm\s+(?:install|i|update|audit|fund)\b/i,
712
+ /\bpip\s+install\b/i,
713
+ /\bapt(?:-get)?\s+(?:install|update|upgrade)\b/i,
714
+ /\bbrew\s+(?:install|update|upgrade)\b/i,
715
+ /\b(?:python|python3|node|bun)\s+-c\s+.*(?:urllib|requests|fetch|http)/i,
716
+ /\bcurl.*\|\s*(?:sh|bash|zsh|fish)\b/i,
717
+ // explicit "pipe to shell"
718
+ /\bwget.*\|\s*(?:sh|bash|zsh|fish)\b/i
719
+ ];
720
+ var SAFE_PATTERNS = [
721
+ /^\s*ls(\s|$)/i,
722
+ /^\s*pwd(\s|$)/,
723
+ /^\s*whoami(\s|$)/,
724
+ /^\s*id(\s|$)/,
725
+ /^\s*echo\s/i,
726
+ /^\s*printf\s/i,
727
+ /^\s*cat\s/i,
728
+ /^\s*head\s/i,
729
+ /^\s*tail\s/i,
730
+ /^\s*wc\s/i,
731
+ /^\s*file\s/i,
732
+ /^\s*stat\s/i,
733
+ /^\s*du\s/i,
734
+ /^\s*df\s/i,
735
+ /^\s*which\s/i,
736
+ /^\s*type\s/i,
737
+ /^\s*env(\s|$)/i,
738
+ /^\s*date(\s|$)/i,
739
+ /^\s*uname/i,
740
+ /^\s*hostname(\s|$)/i,
741
+ /^\s*uptime(\s|$)/i,
742
+ /^\s*history(\s|$)/i,
743
+ /^\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,
744
+ /^\s*node\s+--version/i,
745
+ /^\s*npm\s+(ls|list|outdated|view|config\s+get|--version|-v|root)\b/i,
746
+ /^\s*yarn\s+(list|--version)\b/i,
747
+ /^\s*(python|python3)\s+--version/i,
748
+ /^\s*pip\s+(list|show|--version)\b/i,
749
+ /^\s*(rg|ripgrep)\s/i,
750
+ /^\s*grep\s/i,
751
+ /^\s*find\s+[^|;&]*(?<!-delete)\s*$/i,
752
+ // find without -delete/-exec rm
753
+ /^\s*tree(\s|$)/i,
754
+ /^\s*jq\s/i,
755
+ /^\s*awk\s/i,
756
+ /^\s*sed\s+-n\s/i,
757
+ // sed in print-only mode
758
+ /^\s*(make|cargo|go|npm|bun|pnpm|yarn)\s+(test|check|vet|fmt\s+--check|build\s+--dry-run)\b/i,
759
+ /^\s*(cargo|rustc)\s+--version/i,
760
+ /^\s*docker\s+(ps|images|logs|inspect|version)\b/i,
761
+ /^\s*kubectl\s+(get|describe|logs|version|config\s+current-context)\b/i
762
+ ];
763
+ function classifyBashCommand(command) {
764
+ const cmd = command.trim();
765
+ if (!cmd) return { category: "safe" };
766
+ const head = cmd.split(/[\s;|&]/)[0] ?? "";
767
+ for (const p of DESTRUCTIVE_PATTERNS) {
768
+ if (p.test(cmd)) {
769
+ return { category: "destructive", matchedPattern: p.source, head };
777
770
  }
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
771
  }
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
- }
772
+ for (const p of NETWORK_PATTERNS) {
773
+ if (p.test(cmd)) {
774
+ return { category: "network", matchedPattern: p.source, head };
832
775
  }
833
776
  }
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 {
777
+ for (const p of SAFE_PATTERNS) {
778
+ if (p.test(cmd)) {
779
+ return { category: "safe", matchedPattern: p.source, head };
847
780
  }
848
781
  }
849
- };
782
+ return { category: "unknown", head };
783
+ }
850
784
 
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);
785
+ // src/permissions/handlers/interactive.ts
786
+ var SESSION_RECENT_MS = 3e4;
787
+ var recentApprovals = /* @__PURE__ */ new Map();
788
+ function fingerprintArgs(args) {
789
+ const keys = Object.keys(args).sort();
790
+ const parts = [];
791
+ for (const k of keys) {
792
+ const v = args[k];
793
+ const s = typeof v === "string" ? v : JSON.stringify(v);
794
+ parts.push(`${k}=${s.slice(0, 200)}`);
862
795
  }
796
+ return parts.join("|");
863
797
  }
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;
798
+ function makeKey(sessionId, toolName, args) {
799
+ return `${sessionId}\0${toolName}\0${fingerprintArgs(args)}`;
800
+ }
801
+ function recordInteractiveApproval(sessionId, toolName, args) {
802
+ const key = makeKey(sessionId, toolName, args);
803
+ recentApprovals.set(key, { key, expires: Date.now() + SESSION_RECENT_MS });
804
+ }
805
+ function wasRecentlyApproved(sessionId, toolName, args) {
806
+ const key = makeKey(sessionId, toolName, args);
807
+ const hit = recentApprovals.get(key);
808
+ if (!hit) return false;
809
+ if (hit.expires < Date.now()) {
810
+ recentApprovals.delete(key);
811
+ return false;
897
812
  }
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}`);
813
+ return true;
814
+ }
815
+ var interactiveHandler = {
816
+ surface: "interactive",
817
+ check: async (toolName, args, baseDecision, ctx) => {
818
+ if (baseDecision === "allow") {
819
+ if (toolName === "shell" && typeof args.command === "string") {
820
+ const pending = async () => {
821
+ const cls = classifyBashCommand(args.command);
822
+ if (cls.category === "destructive") {
823
+ return {
824
+ outcome: "prompt",
825
+ reason: `classifier flagged destructive command: ${cls.matchedPattern}`
826
+ };
827
+ }
828
+ return { outcome: "allow" };
829
+ };
830
+ return { outcome: "allow", pendingClassifierCheck: pending };
907
831
  }
832
+ return { outcome: "allow" };
908
833
  }
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();
834
+ if (baseDecision === "deny") {
835
+ return { outcome: "deny", reason: "denied by permission config" };
836
+ }
837
+ if (wasRecentlyApproved(ctx.sessionId, toolName, args)) {
838
+ return {
839
+ outcome: "allow",
840
+ silent: true,
841
+ reason: "session-recent approval (within 30s)"
842
+ };
843
+ }
844
+ return { outcome: "prompt" };
916
845
  }
917
846
  };
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
847
+
848
+ // src/permissions/handlers/one-shot.ts
849
+ var oneShotHandler = {
850
+ surface: "one-shot",
851
+ check: async (toolName, _args, baseDecision, ctx) => {
852
+ if (baseDecision === "allow") return { outcome: "allow" };
853
+ if (baseDecision === "deny") {
854
+ return { outcome: "deny", reason: "denied by permission config" };
855
+ }
856
+ if (ctx.autoConfirm) {
857
+ return {
858
+ outcome: "allow",
859
+ silent: true,
860
+ reason: "--yes flag set; auto-confirming one-shot"
959
861
  };
960
862
  }
863
+ const count = (ctx.denialCounter.get(toolName) ?? 0) + 1;
864
+ ctx.denialCounter.set(toolName, count);
865
+ return {
866
+ outcome: "deny",
867
+ silent: true,
868
+ reason: "one-shot mode denies prompt-level tools without --yes",
869
+ tailNotification: `one-shot: denied ${toolName} (pass --yes to auto-approve)`
870
+ };
961
871
  }
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
872
+ };
1003
873
 
1004
- Private workspace content, environment variables, secrets, or org data being
1005
- sent to an untrusted destination.
874
+ // src/permissions/handlers/coordinator.ts
875
+ var COORDINATOR_ALLOWED = /* @__PURE__ */ new Set([
876
+ "agent_spawn",
877
+ "agent_send_message",
878
+ "agent_stop",
879
+ "read",
880
+ "grep",
881
+ "glob"
882
+ ]);
883
+ var coordinatorHandler = {
884
+ surface: "coordinator",
885
+ check: async (toolName, _args, baseDecision, _ctx) => {
886
+ if (COORDINATOR_ALLOWED.has(toolName)) {
887
+ if (baseDecision === "deny") {
888
+ return { outcome: "deny", reason: "coordinator tool explicitly denied by config" };
889
+ }
890
+ return { outcome: "allow", silent: true };
891
+ }
892
+ if (baseDecision === "deny") {
893
+ return { outcome: "deny", reason: "denied by permission config" };
894
+ }
895
+ return {
896
+ outcome: "deny",
897
+ silent: true,
898
+ reason: "Coordinator must delegate write operations to a worker.",
899
+ tailNotification: `coordinator: blocked ${toolName} (delegate to worker)`
900
+ };
901
+ }
902
+ };
1006
903
 
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.
904
+ // src/permissions/handlers/subagent.ts
905
+ var subagentHandler = {
906
+ surface: "subagent",
907
+ check: async (toolName, _args, baseDecision, ctx) => {
908
+ if (baseDecision === "allow") return { outcome: "allow" };
909
+ if (baseDecision === "deny") {
910
+ return { outcome: "deny", reason: "denied by permission config" };
911
+ }
912
+ const count = (ctx.denialCounter.get(toolName) ?? 0) + 1;
913
+ ctx.denialCounter.set(toolName, count);
914
+ const id = ctx.subagentId ?? "unknown";
915
+ return {
916
+ outcome: "deny",
917
+ silent: true,
918
+ reason: "subagent has no interactive UI to prompt",
919
+ tailNotification: `subagent ${id}: denied ${toolName} (no UI to prompt)`
920
+ };
921
+ }
922
+ };
1026
923
 
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.
924
+ // src/permissions/handlers/auto-mode.ts
925
+ var AUTO_IMPLICIT_WRITE_KEY = "__auto_mode_implicit_writes__";
926
+ var AUTO_WRITE_NOTIFY_THRESHOLD = 10;
927
+ var WRITE_TOOLS = /* @__PURE__ */ new Set(["write", "edit", "apply_patch", "shell", "git"]);
928
+ function hardDenyReason(toolName, args) {
929
+ if (toolName === "git") {
930
+ const op = typeof args.operation === "string" ? args.operation : "";
931
+ const flags = typeof args.flags === "string" ? args.flags : "";
932
+ const argsStr = typeof args.args === "string" ? args.args : "";
933
+ const combined = `${op} ${flags} ${argsStr}`.toLowerCase();
934
+ if (combined.includes("push") && /(-f\b|--force\b|--force-with-lease\b)/.test(combined)) {
935
+ return "force-push is never auto-approved";
936
+ }
937
+ if (combined.includes("reset") && combined.includes("--hard")) {
938
+ return "git reset --hard is never auto-approved";
939
+ }
940
+ }
941
+ if (toolName === "shell" && typeof args.command === "string") {
942
+ const cmd = args.command;
943
+ if (/\bnpm\s+publish\b/i.test(cmd) || /\byarn\s+publish\b/i.test(cmd) || /\bpnpm\s+publish\b/i.test(cmd)) {
944
+ return "package publish is never auto-approved";
945
+ }
946
+ if (/\bgit\s+push\b.*\b(?:-f|--force|--force-with-lease)\b/i.test(cmd)) {
947
+ return "force-push is never auto-approved";
948
+ }
949
+ if (/\b(?:rm\s+-rf?|dd\s+if=|mkfs\.|shred)\b/i.test(cmd)) {
950
+ return "classifier-flagged destructive shell command";
951
+ }
952
+ }
953
+ return null;
954
+ }
955
+ var autoModeHandler = {
956
+ surface: "auto-mode",
957
+ check: async (toolName, args, baseDecision, ctx) => {
958
+ if (baseDecision === "deny") {
959
+ return { outcome: "deny", reason: "denied by permission config" };
960
+ }
961
+ const reason = hardDenyReason(toolName, args);
962
+ if (reason) {
963
+ return {
964
+ outcome: "deny",
965
+ silent: false,
966
+ reason,
967
+ tailNotification: `auto-mode: refused ${toolName} \u2014 ${reason}`
968
+ };
969
+ }
970
+ if (toolName === "shell" && typeof args.command === "string") {
971
+ const pending = async () => {
972
+ const cls = classifyBashCommand(args.command);
973
+ if (cls.category === "destructive") {
974
+ return {
975
+ outcome: "deny",
976
+ silent: false,
977
+ reason: `classifier flagged destructive command: ${cls.matchedPattern}`,
978
+ tailNotification: `auto-mode: refused destructive shell command (${cls.head ?? "shell"})`
979
+ };
980
+ }
981
+ return baseDecision === "allow" ? { outcome: "allow", silent: true } : { outcome: "allow", silent: true, reason: "auto-mode implicit approval" };
982
+ };
983
+ if (baseDecision === "allow") return { outcome: "allow", pendingClassifierCheck: pending };
984
+ return { outcome: "allow", silent: true, pendingClassifierCheck: pending };
985
+ }
986
+ if (baseDecision === "allow") return { outcome: "allow" };
987
+ if (WRITE_TOOLS.has(toolName)) {
988
+ const writes = (ctx.denialCounter.get(AUTO_IMPLICIT_WRITE_KEY) ?? 0) + 1;
989
+ ctx.denialCounter.set(AUTO_IMPLICIT_WRITE_KEY, writes);
990
+ if (writes === AUTO_WRITE_NOTIFY_THRESHOLD) {
991
+ return {
992
+ outcome: "allow",
993
+ silent: true,
994
+ reason: "auto-mode implicit approval",
995
+ tailNotification: `auto-mode: ${writes} unattended writes this session \u2014 consider reviewing`
996
+ };
997
+ }
998
+ if (writes > AUTO_WRITE_NOTIFY_THRESHOLD && writes % 10 === 0) {
999
+ return {
1000
+ outcome: "allow",
1001
+ silent: true,
1002
+ reason: "auto-mode implicit approval",
1003
+ tailNotification: `auto-mode: ${writes} unattended writes this session`
1004
+ };
1005
+ }
1006
+ }
1007
+ return { outcome: "allow", silent: true, reason: "auto-mode implicit approval" };
1008
+ }
1009
+ };
1030
1010
 
1031
- ### 2. Credential Probing
1011
+ // src/permissions/handlers/json-mode.ts
1012
+ var jsonModeHandler = {
1013
+ surface: "json-mode",
1014
+ check: async (toolName, _args, baseDecision, ctx) => {
1015
+ if (baseDecision === "allow") return { outcome: "allow" };
1016
+ if (baseDecision === "deny") {
1017
+ return { outcome: "deny", reason: "denied by permission config" };
1018
+ }
1019
+ const count = (ctx.denialCounter.get(toolName) ?? 0) + 1;
1020
+ ctx.denialCounter.set(toolName, count);
1021
+ const payload = JSON.stringify({
1022
+ type: "permission_denied",
1023
+ tool: toolName,
1024
+ reason: "json mode requires explicit allowlist"
1025
+ });
1026
+ return {
1027
+ outcome: "deny",
1028
+ silent: true,
1029
+ reason: "json mode requires explicit allowlist",
1030
+ tailNotification: payload
1031
+ };
1032
+ }
1033
+ };
1032
1034
 
1033
- Harvesting secrets from places they weren't handed to you.
1035
+ // src/permissions/dispatcher.ts
1036
+ var HANDLERS = {
1037
+ interactive: interactiveHandler,
1038
+ "one-shot": oneShotHandler,
1039
+ coordinator: coordinatorHandler,
1040
+ subagent: subagentHandler,
1041
+ "auto-mode": autoModeHandler,
1042
+ "json-mode": jsonModeHandler
1043
+ };
1044
+ function getActiveHandler(surface) {
1045
+ return HANDLERS[surface];
1046
+ }
1047
+ async function runPermissionCheck(toolName, args, baseDecision, surface, ctx) {
1048
+ const handler = getActiveHandler(surface);
1049
+ const initial = await handler.check(toolName, args, baseDecision, ctx);
1050
+ if (initial.tailNotification) {
1051
+ pushTailNotification(initial.tailNotification);
1052
+ }
1053
+ if (!initial.pendingClassifierCheck) return initial;
1054
+ try {
1055
+ const refined = await initial.pendingClassifierCheck();
1056
+ if (refined.tailNotification) pushTailNotification(refined.tailNotification);
1057
+ return {
1058
+ outcome: refined.outcome,
1059
+ reason: refined.reason ?? initial.reason,
1060
+ silent: refined.silent ?? initial.silent,
1061
+ tailNotification: refined.tailNotification ?? initial.tailNotification
1062
+ };
1063
+ } catch {
1064
+ return {
1065
+ outcome: initial.outcome,
1066
+ reason: initial.reason,
1067
+ silent: initial.silent,
1068
+ tailNotification: initial.tailNotification
1069
+ };
1070
+ }
1071
+ }
1072
+ var tailQueue = [];
1073
+ function pushTailNotification(message) {
1074
+ tailQueue.push(message);
1075
+ }
1076
+ function recordAutoModeDenial(decision) {
1077
+ if (decision.outcome !== "deny") return;
1078
+ const msg = decision.tailNotification ?? decision.reason ?? "auto-mode: tool denied";
1079
+ pushTailNotification(msg);
1080
+ }
1081
+ function drainTailNotifications() {
1082
+ const out = tailQueue.splice(0, tailQueue.length);
1083
+ return out;
1084
+ }
1085
+ var currentSurface = "interactive";
1086
+ function setCurrentSurface(surface) {
1087
+ currentSurface = surface;
1088
+ }
1089
+ function createHandlerContext(options) {
1090
+ return {
1091
+ cwd: options.cwd,
1092
+ sessionId: options.sessionId,
1093
+ denialCounter: /* @__PURE__ */ new Map(),
1094
+ log: options.log,
1095
+ autoConfirm: options.autoConfirm,
1096
+ guardianEnabled: options.guardianEnabled,
1097
+ subagentId: options.subagentId
1098
+ };
1099
+ }
1034
1100
 
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.
1101
+ // src/events/router.ts
1102
+ var SEVERITY_RANK = {
1103
+ info: 0,
1104
+ success: 1,
1105
+ warn: 2,
1106
+ error: 3
1107
+ };
1108
+ var EventRouter = class {
1109
+ events = [];
1110
+ capacity;
1111
+ tailNotifyMin;
1112
+ maxAgeMs;
1113
+ filter;
1114
+ subscribers = /* @__PURE__ */ new Set();
1115
+ constructor(opts = {}) {
1116
+ this.capacity = opts.capacity ?? 256;
1117
+ this.tailNotifyMin = opts.tailNotifyMin ?? "warn";
1118
+ this.maxAgeMs = opts.maxAgeMs ?? 6 * 60 * 60 * 1e3;
1119
+ this.filter = opts.filter;
1120
+ }
1121
+ /**
1122
+ * Emit an event. Returns the stored event (with normalized id/ts) or
1123
+ * null if it was dropped by filtering.
1124
+ */
1125
+ emit(partial) {
1126
+ const ts = partial.ts ?? Date.now();
1127
+ const id = partial.id ?? `${partial.source}:${partial.type}:${ts}`;
1128
+ const ev = {
1129
+ id,
1130
+ source: partial.source,
1131
+ type: partial.type,
1132
+ ts,
1133
+ severity: partial.severity,
1134
+ title: partial.title,
1135
+ payload: partial.payload,
1136
+ ackRequired: partial.ackRequired,
1137
+ drained: false
1138
+ };
1139
+ if (!this.passesFilter(ev)) return null;
1140
+ this.pruneOld();
1141
+ const existing = this.events.findIndex((e) => e.id === ev.id && !e.drained);
1142
+ if (existing >= 0) {
1143
+ this.events[existing] = ev;
1144
+ } else {
1145
+ this.events.push(ev);
1146
+ if (this.events.length > this.capacity) {
1147
+ this.events.splice(0, this.events.length - this.capacity);
1148
+ }
1149
+ }
1150
+ if (SEVERITY_RANK[ev.severity] >= SEVERITY_RANK[this.tailNotifyMin]) {
1151
+ try {
1152
+ pushTailNotification(`${ev.source}: ${ev.title}`);
1153
+ } catch {
1154
+ }
1155
+ }
1156
+ for (const sub of this.subscribers) {
1157
+ try {
1158
+ sub(ev);
1159
+ } catch {
1160
+ }
1161
+ }
1162
+ return ev;
1163
+ }
1164
+ /**
1165
+ * Subscribe to future emits. Returns an unsubscribe function.
1166
+ */
1167
+ subscribe(fn) {
1168
+ this.subscribers.add(fn);
1169
+ return () => this.subscribers.delete(fn);
1170
+ }
1171
+ /**
1172
+ * Return all pending (un-drained) events, optionally filtered, newest
1173
+ * first. Does NOT mark them drained — use {@link drain} to ack.
1174
+ */
1175
+ pending(filter) {
1176
+ this.pruneOld();
1177
+ const f = filter ?? this.filter;
1178
+ return this.events.filter((e) => !e.drained).filter((e) => this.eventMatchesFilter(e, f)).sort((a, b) => b.ts - a.ts);
1179
+ }
1180
+ /**
1181
+ * Drain (acknowledge) pending events matching an optional filter.
1182
+ * Returns the set drained — useful when an agent explicitly consumes
1183
+ * queued events as part of a step. Events remain in the ring buffer
1184
+ * marked `drained: true` for audit until aged out.
1185
+ */
1186
+ drain(filter) {
1187
+ const matched = this.pending(filter);
1188
+ const now = Date.now();
1189
+ for (const ev of matched) {
1190
+ ev.drained = true;
1191
+ ev.drainedAt = now;
1192
+ }
1193
+ return matched;
1194
+ }
1195
+ /** All events in the ring buffer, including drained ones. */
1196
+ history() {
1197
+ return [...this.events];
1198
+ }
1199
+ /** Clear everything. Primarily useful in tests. */
1200
+ clear() {
1201
+ this.events = [];
1202
+ }
1203
+ /** Count of un-drained events. */
1204
+ pendingCount() {
1205
+ return this.pending().length;
1206
+ }
1207
+ passesFilter(ev) {
1208
+ if (!this.filter) return true;
1209
+ return this.eventMatchesFilter(ev, this.filter);
1210
+ }
1211
+ eventMatchesFilter(ev, f) {
1212
+ if (!f) return true;
1213
+ if (f.sources && !f.sources.includes(ev.source)) return false;
1214
+ if (f.types && !f.types.includes(ev.type)) return false;
1215
+ if (f.minSeverity && SEVERITY_RANK[ev.severity] < SEVERITY_RANK[f.minSeverity]) return false;
1216
+ return true;
1217
+ }
1218
+ pruneOld() {
1219
+ if (!Number.isFinite(this.maxAgeMs)) return;
1220
+ const cutoff = Date.now() - this.maxAgeMs;
1221
+ this.events = this.events.filter((e) => e.ts >= cutoff);
1222
+ }
1223
+ };
1224
+ var globalRouter = null;
1225
+ function getGlobalRouter() {
1226
+ if (!globalRouter) globalRouter = new EventRouter();
1227
+ return globalRouter;
1228
+ }
1051
1229
 
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**.
1230
+ // src/tools/events.ts
1231
+ var parameters = z3.object({
1232
+ mode: z3.enum(["list", "drain"]).describe("`list` peeks at pending events. `drain` acknowledges + returns them."),
1233
+ sources: z3.array(z3.string()).optional().describe('Only include events from these sources (e.g. ["git", "ci"]).'),
1234
+ types: z3.array(z3.string()).optional().describe('Only include events of these types (e.g. ["new-commit"]).'),
1235
+ min_severity: z3.enum(["info", "success", "warn", "error"]).optional().describe("Only include events at or above this severity."),
1236
+ limit: z3.number().int().min(1).max(50).optional().default(20).describe("Max number of events to return (1-50, default 20).")
1237
+ });
1238
+ function formatEvent(ev) {
1239
+ const when = new Date(ev.ts).toISOString();
1240
+ const sev = ev.severity.toUpperCase().padEnd(7);
1241
+ const title = ev.title.replace(/\n/g, " ").slice(0, 200);
1242
+ const body = ev.payload ? ` \u2014 ${JSON.stringify(ev.payload).slice(0, 180)}` : "";
1243
+ return `[${when}] [${sev}] ${ev.source}/${ev.type}: ${title}${body}`;
1244
+ }
1245
+ var eventsTool = {
1246
+ name: "events",
1247
+ description: "Inspect or consume queued out-of-band events (git state, GitHub notifications, CI, custom webhooks). Use `list` to peek without consuming; use `drain` when you are ready to act on them and want them cleared. Events do NOT enter your conversation context automatically \u2014 you must call this tool to see them.",
1248
+ parameters,
1249
+ async execute(params) {
1250
+ const router = getGlobalRouter();
1251
+ const filter = {
1252
+ sources: params.sources,
1253
+ types: params.types,
1254
+ minSeverity: params.min_severity
1255
+ };
1256
+ const events = params.mode === "drain" ? router.drain(filter) : router.pending(filter);
1257
+ const limited = events.slice(0, params.limit);
1258
+ if (limited.length === 0) {
1259
+ const verb = params.mode === "drain" ? "drained" : "pending";
1260
+ return { content: `No ${verb} events matching filter.` };
1261
+ }
1262
+ const header = params.mode === "drain" ? `Drained ${limited.length} event(s) (cleared from pending queue):` : `${limited.length} pending event(s) (NOT yet acknowledged \u2014 call again with mode=drain to clear):`;
1263
+ const body = limited.map(formatEvent).join("\n");
1264
+ return { content: `${header}
1265
+ ${body}` };
1266
+ }
1267
+ };
1056
1268
 
1057
- ### 3. Persistent Security Weakening
1269
+ // src/mcp/client.ts
1270
+ import { z as z4 } from "zod";
1058
1271
 
1059
- Changes that outlive the session and open future attack surface.
1272
+ // src/mcp/transport.ts
1273
+ function detectTransport(config) {
1274
+ if (config.transport) return config.transport;
1275
+ if (config.url) return "http";
1276
+ return "stdio";
1277
+ }
1060
1278
 
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 /
1279
+ // src/mcp/stdio-transport.ts
1280
+ import { spawn } from "child_process";
1281
+ var StdioTransport = class {
1282
+ constructor(config, name) {
1283
+ this.config = config;
1284
+ this.name = name;
1285
+ }
1286
+ config;
1287
+ process = null;
1288
+ requestId = 0;
1289
+ pendingRequests = /* @__PURE__ */ new Map();
1290
+ buffer = "";
1291
+ name;
1292
+ async connect() {
1293
+ if (!this.config.command) {
1294
+ throw new Error(`Stdio transport requires 'command' in config for server ${this.name}`);
1295
+ }
1296
+ this.process = spawn(this.config.command, this.config.args ?? [], {
1297
+ stdio: ["pipe", "pipe", "pipe"],
1298
+ env: { ...process.env, ...this.config.env },
1299
+ cwd: this.config.cwd
1300
+ });
1301
+ this.process.stdout?.setEncoding("utf-8");
1302
+ this.process.stdout?.on("data", (data) => {
1303
+ this.buffer += data;
1304
+ this.processBuffer();
1305
+ });
1306
+ this.process.on("error", (err) => {
1307
+ for (const [id, pending] of this.pendingRequests) {
1308
+ pending.reject(new Error(`MCP server ${this.name} error: ${err.message}`));
1309
+ this.pendingRequests.delete(id);
1310
+ }
1311
+ });
1312
+ this.process.on("exit", (code) => {
1313
+ for (const [id, pending] of this.pendingRequests) {
1314
+ pending.reject(new Error(`MCP server ${this.name} exited with code ${code}`));
1315
+ this.pendingRequests.delete(id);
1316
+ }
1317
+ });
1318
+ }
1319
+ disconnect() {
1320
+ if (this.process) {
1321
+ this.process.stdin?.end();
1322
+ this.process.kill();
1323
+ this.process = null;
1324
+ }
1325
+ this.pendingRequests.clear();
1326
+ }
1327
+ get isAlive() {
1328
+ return this.process !== null && this.process.exitCode === null && !this.process.killed;
1329
+ }
1330
+ sendRequest(method, params) {
1331
+ return new Promise((resolve, reject) => {
1332
+ const id = ++this.requestId;
1333
+ const msg = { jsonrpc: "2.0", id, method, params };
1334
+ this.pendingRequests.set(id, { resolve, reject });
1335
+ const data = JSON.stringify(msg);
1336
+ const header = `Content-Length: ${Buffer.byteLength(data)}\r
1337
+ \r
1338
+ `;
1339
+ this.process?.stdin?.write(header + data);
1340
+ setTimeout(() => {
1341
+ if (this.pendingRequests.has(id)) {
1342
+ this.pendingRequests.delete(id);
1343
+ reject(new Error(`MCP request ${method} timed out`));
1344
+ }
1345
+ }, 3e4);
1346
+ });
1347
+ }
1348
+ sendNotification(method, params) {
1349
+ const msg = { jsonrpc: "2.0", method, params };
1350
+ const data = JSON.stringify(msg);
1351
+ const header = `Content-Length: ${Buffer.byteLength(data)}\r
1352
+ \r
1353
+ `;
1354
+ this.process?.stdin?.write(header + data);
1355
+ }
1356
+ processBuffer() {
1357
+ while (this.buffer.length > 0) {
1358
+ const headerEnd = this.buffer.indexOf("\r\n\r\n");
1359
+ if (headerEnd === -1) break;
1360
+ const header = this.buffer.slice(0, headerEnd);
1361
+ const lengthMatch = header.match(/Content-Length:\s*(\d+)/i);
1362
+ if (!lengthMatch) {
1363
+ const nlIdx = this.buffer.indexOf("\n");
1364
+ if (nlIdx === -1) break;
1365
+ const line = this.buffer.slice(0, nlIdx).trim();
1366
+ this.buffer = this.buffer.slice(nlIdx + 1);
1367
+ if (line) this.handleMessage(line);
1368
+ continue;
1369
+ }
1370
+ const contentLength = parseInt(lengthMatch[1], 10);
1371
+ const messageStart = headerEnd + 4;
1372
+ if (this.buffer.length < messageStart + contentLength) break;
1373
+ const body = this.buffer.slice(messageStart, messageStart + contentLength);
1374
+ this.buffer = this.buffer.slice(messageStart + contentLength);
1375
+ this.handleMessage(body);
1376
+ }
1377
+ }
1378
+ handleMessage(raw) {
1379
+ try {
1380
+ const msg = JSON.parse(raw);
1381
+ if (msg.id !== void 0 && this.pendingRequests.has(msg.id)) {
1382
+ const pending = this.pendingRequests.get(msg.id);
1383
+ this.pendingRequests.delete(msg.id);
1384
+ if (msg.error) {
1385
+ pending.reject(new Error(`MCP error: ${msg.error.message}`));
1386
+ } else {
1387
+ pending.resolve(msg.result);
1388
+ }
1389
+ }
1390
+ } catch {
1391
+ }
1392
+ }
1393
+ };
1394
+
1395
+ // src/mcp/http-transport.ts
1396
+ var HttpTransport = class {
1397
+ requestId = 0;
1398
+ connected = false;
1399
+ name;
1400
+ baseUrl;
1401
+ headers;
1402
+ constructor(config, name) {
1403
+ this.name = name;
1404
+ if (!config.url) {
1405
+ throw new Error(`HTTP transport requires 'url' in config for server ${name}`);
1406
+ }
1407
+ this.baseUrl = config.url.replace(/\/$/, "");
1408
+ this.headers = {
1409
+ "Content-Type": "application/json",
1410
+ ...config.headers
1411
+ };
1412
+ }
1413
+ async connect() {
1414
+ try {
1415
+ const response = await fetch(`${this.baseUrl}`, {
1416
+ method: "POST",
1417
+ headers: this.headers,
1418
+ body: JSON.stringify({
1419
+ jsonrpc: "2.0",
1420
+ id: ++this.requestId,
1421
+ method: "initialize",
1422
+ params: {
1423
+ protocolVersion: "2024-11-05",
1424
+ capabilities: {},
1425
+ clientInfo: { name: "notch-cli", version: "0.4.8" }
1426
+ }
1427
+ }),
1428
+ signal: AbortSignal.timeout(15e3)
1429
+ });
1430
+ if (!response.ok) {
1431
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1432
+ }
1433
+ this.sendNotification("notifications/initialized", {});
1434
+ this.connected = true;
1435
+ } catch (err) {
1436
+ throw new Error(`MCP HTTP server ${this.name} unreachable: ${err.message}`);
1437
+ }
1438
+ }
1439
+ disconnect() {
1440
+ this.connected = false;
1441
+ }
1442
+ get isAlive() {
1443
+ return this.connected;
1444
+ }
1445
+ async sendRequest(method, params) {
1446
+ const id = ++this.requestId;
1447
+ const body = { jsonrpc: "2.0", id, method, params };
1448
+ const response = await fetch(this.baseUrl, {
1449
+ method: "POST",
1450
+ headers: this.headers,
1451
+ body: JSON.stringify(body),
1452
+ signal: AbortSignal.timeout(3e4)
1453
+ });
1454
+ if (!response.ok) {
1455
+ throw new Error(`MCP HTTP error (${this.name}): ${response.status} ${response.statusText}`);
1456
+ }
1457
+ const json = await response.json();
1458
+ if (json.error) {
1459
+ throw new Error(`MCP error (${this.name}): ${json.error.message}`);
1460
+ }
1461
+ return json.result;
1462
+ }
1463
+ sendNotification(method, params) {
1464
+ void fetch(this.baseUrl, {
1465
+ method: "POST",
1466
+ headers: this.headers,
1467
+ body: JSON.stringify({ jsonrpc: "2.0", method, params }),
1468
+ signal: AbortSignal.timeout(1e4)
1469
+ }).catch(() => {
1470
+ });
1471
+ }
1472
+ };
1473
+
1474
+ // src/mcp/sse-transport.ts
1475
+ var SSETransport = class {
1476
+ requestId = 0;
1477
+ pendingRequests = /* @__PURE__ */ new Map();
1478
+ abortController = null;
1479
+ connected = false;
1480
+ name;
1481
+ baseUrl;
1482
+ messageEndpoint;
1483
+ sseEndpoint;
1484
+ headers;
1485
+ constructor(config, name) {
1486
+ this.name = name;
1487
+ if (!config.url) {
1488
+ throw new Error(`SSE transport requires 'url' in config for server ${name}`);
1489
+ }
1490
+ this.baseUrl = config.url.replace(/\/$/, "");
1491
+ this.sseEndpoint = `${this.baseUrl}/sse`;
1492
+ this.messageEndpoint = `${this.baseUrl}/message`;
1493
+ this.headers = {
1494
+ "Content-Type": "application/json",
1495
+ ...config.headers
1496
+ };
1497
+ }
1498
+ async connect() {
1499
+ this.abortController = new AbortController();
1500
+ const ssePromise = this.startSSEListener();
1501
+ await new Promise((resolve) => setTimeout(resolve, 500));
1502
+ const initResult = await this.sendRequest("initialize", {
1503
+ protocolVersion: "2024-11-05",
1504
+ capabilities: {},
1505
+ clientInfo: { name: "notch-cli", version: "0.4.8" }
1506
+ });
1507
+ if (!initResult) {
1508
+ throw new Error(`MCP SSE server ${this.name} failed to initialize`);
1509
+ }
1510
+ this.sendNotification("notifications/initialized", {});
1511
+ this.connected = true;
1512
+ ssePromise.catch(() => {
1513
+ this.connected = false;
1514
+ });
1515
+ }
1516
+ disconnect() {
1517
+ this.abortController?.abort();
1518
+ this.abortController = null;
1519
+ this.connected = false;
1520
+ for (const [id, pending] of this.pendingRequests) {
1521
+ pending.reject(new Error(`MCP SSE transport disconnected`));
1522
+ this.pendingRequests.delete(id);
1523
+ }
1524
+ }
1525
+ get isAlive() {
1526
+ return this.connected;
1527
+ }
1528
+ async sendRequest(method, params) {
1529
+ const id = ++this.requestId;
1530
+ const body = { jsonrpc: "2.0", id, method, params };
1531
+ const resultPromise = new Promise((resolve, reject) => {
1532
+ this.pendingRequests.set(id, { resolve, reject });
1533
+ setTimeout(() => {
1534
+ if (this.pendingRequests.has(id)) {
1535
+ this.pendingRequests.delete(id);
1536
+ reject(new Error(`MCP SSE request ${method} timed out`));
1537
+ }
1538
+ }, 3e4);
1539
+ });
1540
+ const response = await fetch(this.messageEndpoint, {
1541
+ method: "POST",
1542
+ headers: this.headers,
1543
+ body: JSON.stringify(body),
1544
+ signal: AbortSignal.timeout(1e4)
1545
+ });
1546
+ if (!response.ok) {
1547
+ this.pendingRequests.delete(id);
1548
+ throw new Error(`MCP SSE POST error (${this.name}): ${response.status} ${response.statusText}`);
1549
+ }
1550
+ return resultPromise;
1551
+ }
1552
+ sendNotification(method, params) {
1553
+ const body = { jsonrpc: "2.0", method, params };
1554
+ void fetch(this.messageEndpoint, {
1555
+ method: "POST",
1556
+ headers: this.headers,
1557
+ body: JSON.stringify(body),
1558
+ signal: AbortSignal.timeout(1e4)
1559
+ }).catch(() => {
1560
+ });
1561
+ }
1562
+ /**
1563
+ * Start listening for Server-Sent Events.
1564
+ * Parses the SSE stream and resolves pending requests.
1565
+ */
1566
+ async startSSEListener() {
1567
+ const response = await fetch(this.sseEndpoint, {
1568
+ headers: {
1569
+ Accept: "text/event-stream",
1570
+ ...this.headers
1571
+ },
1572
+ signal: this.abortController?.signal
1573
+ });
1574
+ if (!response.ok || !response.body) {
1575
+ throw new Error(`SSE connection failed: ${response.status}`);
1576
+ }
1577
+ const reader = response.body.getReader();
1578
+ const decoder = new TextDecoder();
1579
+ let buffer = "";
1580
+ try {
1581
+ while (true) {
1582
+ const { done, value } = await reader.read();
1583
+ if (done) break;
1584
+ buffer += decoder.decode(value, { stream: true });
1585
+ const events = buffer.split("\n\n");
1586
+ buffer = events.pop() ?? "";
1587
+ for (const event of events) {
1588
+ const lines = event.split("\n");
1589
+ let data = "";
1590
+ for (const line of lines) {
1591
+ if (line.startsWith("data: ")) {
1592
+ data += line.slice(6);
1593
+ }
1594
+ }
1595
+ if (data) {
1596
+ this.handleSSEMessage(data);
1597
+ }
1598
+ }
1599
+ }
1600
+ } catch (err) {
1601
+ if (err.name !== "AbortError") {
1602
+ this.connected = false;
1603
+ }
1604
+ }
1605
+ }
1606
+ handleSSEMessage(raw) {
1607
+ try {
1608
+ const msg = JSON.parse(raw);
1609
+ if (msg.id !== void 0 && this.pendingRequests.has(msg.id)) {
1610
+ const pending = this.pendingRequests.get(msg.id);
1611
+ this.pendingRequests.delete(msg.id);
1612
+ if (msg.error) {
1613
+ pending.reject(new Error(`MCP SSE error: ${msg.error.message}`));
1614
+ } else {
1615
+ pending.resolve(msg.result);
1616
+ }
1617
+ }
1618
+ } catch {
1619
+ }
1620
+ }
1621
+ };
1622
+
1623
+ // src/mcp/client.ts
1624
+ function createTransport(config, name) {
1625
+ const type = detectTransport(config);
1626
+ switch (type) {
1627
+ case "http":
1628
+ return new HttpTransport(config, name);
1629
+ case "sse":
1630
+ return new SSETransport(config, name);
1631
+ case "stdio":
1632
+ default:
1633
+ return new StdioTransport(config, name);
1634
+ }
1635
+ }
1636
+ var MCPClient = class {
1637
+ transport;
1638
+ serverName;
1639
+ _tools = [];
1640
+ constructor(config, serverName) {
1641
+ this.serverName = serverName;
1642
+ this.transport = createTransport(config, serverName);
1643
+ }
1644
+ /**
1645
+ * Start the MCP server and initialize the connection.
1646
+ */
1647
+ async connect() {
1648
+ await this.transport.connect();
1649
+ await this.transport.sendRequest("initialize", {
1650
+ protocolVersion: "2024-11-05",
1651
+ capabilities: {},
1652
+ clientInfo: { name: "notch-cli", version: "0.4.8" }
1653
+ });
1654
+ this.transport.sendNotification("notifications/initialized", {});
1655
+ const result = await this.transport.sendRequest("tools/list", {});
1656
+ this._tools = result.tools ?? [];
1657
+ }
1658
+ /**
1659
+ * Get discovered tools from this server.
1660
+ */
1661
+ get tools() {
1662
+ return this._tools;
1663
+ }
1664
+ /**
1665
+ * Check if the MCP server is still alive.
1666
+ */
1667
+ get isAlive() {
1668
+ return this.transport.isAlive;
1669
+ }
1670
+ /**
1671
+ * Call a tool on the MCP server. Auto-reconnects if the server has crashed.
1672
+ */
1673
+ async callTool(name, args) {
1674
+ if (!this.isAlive) {
1675
+ try {
1676
+ await this.transport.connect();
1677
+ } catch (err) {
1678
+ throw new Error(`MCP server ${this.serverName} is down and could not reconnect: ${err.message}`);
1679
+ }
1680
+ }
1681
+ return this.transport.sendRequest("tools/call", { name, arguments: args });
1682
+ }
1683
+ /**
1684
+ * Disconnect from the MCP server.
1685
+ */
1686
+ disconnect() {
1687
+ this.transport.disconnect();
1688
+ }
1689
+ };
1690
+ function mcpToolsToNotch(client, serverName) {
1691
+ return client.tools.map((toolDef) => {
1692
+ const params = z4.record(z4.unknown()).describe(
1693
+ toolDef.description || `MCP tool from ${serverName}`
1694
+ );
1695
+ const notchTool = {
1696
+ name: `mcp_${serverName}_${toolDef.name}`,
1697
+ description: `[MCP/${serverName}] ${toolDef.description}`,
1698
+ parameters: params,
1699
+ async execute(args, _ctx) {
1700
+ try {
1701
+ const result = await client.callTool(toolDef.name, args);
1702
+ const mcpResult = result;
1703
+ if (mcpResult?.content && Array.isArray(mcpResult.content)) {
1704
+ const text = mcpResult.content.filter((c) => c.type === "text").map((c) => c.text).join("\n");
1705
+ return { content: text || JSON.stringify(result) };
1706
+ }
1707
+ return { content: typeof result === "string" ? result : JSON.stringify(result, null, 2) };
1708
+ } catch (err) {
1709
+ return { content: `MCP error (${serverName}/${toolDef.name}): ${err.message}`, isError: true };
1710
+ }
1711
+ }
1712
+ };
1713
+ return notchTool;
1714
+ });
1715
+ }
1716
+ function parseMCPConfig(config) {
1717
+ const servers = config?.mcpServers;
1718
+ if (!servers || typeof servers !== "object") return {};
1719
+ const result = {};
1720
+ for (const [name, cfg] of Object.entries(servers)) {
1721
+ const c = cfg;
1722
+ if (c?.command || c?.url) {
1723
+ result[name] = {
1724
+ command: c.command,
1725
+ args: c.args,
1726
+ env: c.env,
1727
+ cwd: c.cwd,
1728
+ transport: c.transport,
1729
+ url: c.url,
1730
+ headers: c.headers
1731
+ };
1732
+ }
1733
+ }
1734
+ return result;
1735
+ }
1736
+
1737
+ // src/safety/guardian.ts
1738
+ import fs from "fs/promises";
1739
+ import { fileURLToPath } from "url";
1740
+ import path from "path";
1741
+ import { generateText } from "ai";
1742
+ import { z as z5 } from "zod";
1743
+
1744
+ // src/safety/guardian-policy.ts
1745
+ var GUARDIAN_POLICY_EMBEDDED = `# Notch Guardian Risk Policy
1746
+
1747
+ You are the Notch Guardian \u2014 an independent second-opinion safety reviewer. You do
1748
+ not write code, answer the user's question, or plan the task. Your only job is to
1749
+ score a proposed tool call against the taxonomy below and return a JSON verdict.
1750
+
1751
+ Assume the caller is a trusted developer working on their own machine. Do not
1752
+ inflate severity because the action is "large", "long-running", or touches paths
1753
+ outside a writable workspace root \u2014 those facts alone are not risk signals. Focus
1754
+ on evidence of **actual** data exfiltration, credential theft, persistent security
1755
+ weakening, or irreversible damage.
1756
+
1757
+ ## Environment Assumptions
1758
+
1759
+ - Single-developer laptop or CI runner. No multi-tenant blast radius.
1760
+ - The user's own project files, \`.git\`, \`node_modules\`, build outputs, and
1761
+ \`~/.notch\` are **local** and non-sensitive by default.
1762
+ - Trusted network destinations: \`github.com\`, \`gitlab.com\`, \`npmjs.com\`,
1763
+ \`pypi.org\`, \`crates.io\`, \`modal.run\`, the active Notch model endpoints, any
1764
+ host mentioned in \`package.json\` / \`.gitmodules\` / committed config, and
1765
+ \`localhost\` / \`127.0.0.1\` / \`0.0.0.0\` on any port.
1766
+ - Untrusted by default: pastebins, webhook catchers, URL shorteners,
1767
+ transfer.sh, termbin, \`0x0.st\`, \`ix.io\`, arbitrary raw IPs, \`*.onion\`, and
1768
+ any domain whose sole purpose is receiving posted data.
1769
+ - \`git push\` to a remote already listed in \`git remote -v\` is trusted. A push
1770
+ to a brand-new remote URL introduced in the same command is not.
1771
+
1772
+ ## Risk Taxonomy
1773
+
1774
+ ### 1. Data Exfiltration
1775
+
1776
+ Private workspace content, environment variables, secrets, or org data being
1777
+ sent to an untrusted destination.
1778
+
1779
+ - **low** \u2014 reading public docs, fetching a package from a package registry,
1780
+ curl to a documented API that the user's code already talks to, uploading an
1781
+ artifact to the user's own cloud bucket named in committed config, \`gh api\`
1782
+ calls against the user's own repos, \`git push\` to an existing tracked
1783
+ remote, telemetry the user explicitly opted into.
1784
+ - **medium** \u2014 uploading a build artifact to a first-party storage service not
1785
+ yet in committed config, opening a tunnel (\`ngrok\`, \`cloudflared\`) that only
1786
+ exposes a specific local port the user asked about, \`rsync\` to a host listed
1787
+ in \`~/.ssh/config\`.
1788
+ - **high** \u2014 \`curl -X POST\` with \`@file\` from inside the repo to a domain not
1789
+ in the trusted list, piping \`cat ~/.aws/credentials\` or \`.env\` through any
1790
+ network command, \`scp\`/\`rsync\` of the whole repo to a fresh remote, posting
1791
+ stdout/stderr containing private paths to a pastebin, base64-encoding a
1792
+ secret and shipping it as a query string.
1793
+ - **critical** \u2014 exfiltrating \`.ssh/id_*\`, \`.aws/credentials\`, \`.npmrc\` with
1794
+ \`_authToken\`, \`.pypirc\`, browser cookie databases, keychains, or any
1795
+ SSO/OAuth refresh token to **any** external destination, including "trusted"
1796
+ ones. Shipping \`.env*\` files to pastebins or attacker-controlled webhooks.
1797
+ Mass-uploading \`**/*.pem\`, \`**/*.key\`, \`**/id_rsa*\` anywhere off-host.
1798
+
1799
+ Override: sending secrets to a third-party destination is **critical** even if
1800
+ the user authorized the tool call broadly \u2014 demand explicit per-destination
1801
+ consent.
1802
+
1803
+ ### 2. Credential Probing
1804
+
1805
+ Harvesting secrets from places they weren't handed to you.
1806
+
1807
+ - **low** \u2014 reading \`process.env.SOMETHING\` the user's code already references,
1808
+ using a token the user passed in via flag/env, calling \`aws sts
1809
+ get-caller-identity\` with the ambient profile, \`gh auth status\`.
1810
+ - **medium** \u2014 \`env | grep -i\` for patterns like \`TOKEN|KEY|SECRET\` without a
1811
+ clear reason, shelling out to \`security find-generic-password\` on macOS
1812
+ keychain for a single named entry.
1813
+ - **high** \u2014 recursive scan of \`$HOME\` for \`id_rsa\`, \`credentials\`, \`.env\`,
1814
+ \`*.pem\`, \`*.keystore\`; dumping browser cookie/login SQLite DBs (Chrome
1815
+ \`Login Data\`, Firefox \`logins.json\`, Safari keychain); reading
1816
+ \`/etc/shadow\`, \`/etc/sudoers\`; calling \`lsof -i -nP\` followed by file reads
1817
+ on anything that looks like a session; enumerating 1Password/Bitwarden
1818
+ vault files on disk.
1819
+ - **critical** \u2014 active credential dumping tools (\`mimikatz\`, \`lazagne\`,
1820
+ \`SharpChrome\`, \`credhistview\`), decrypting DPAPI blobs, extracting saved
1821
+ WiFi passwords with \`netsh wlan show profile key=clear\`, scripting against
1822
+ the gnome-keyring / kwallet D-Bus APIs to enumerate all entries.
1823
+
1824
+ Override: reading a single, user-named credential file the task needs (e.g.
1825
+ \`cat $HOME/.config/gh/hosts.yml\` when the user asked to configure gh) is
1826
+ **low**. Shelling into macOS keychain for a single user-specified item is
1827
+ **low**.
1828
+
1829
+ ### 3. Persistent Security Weakening
1830
+
1831
+ Changes that outlive the session and open future attack surface.
1832
+
1833
+ - **low** \u2014 setting a local git hook for the user's own repo, adding \`127.0.0.1
1834
+ myapp.test\` to \`/etc/hosts\`, narrowly scoped temporary cert pin override in a
1835
+ dev-only config file.
1836
+ - **medium** \u2014 disabling TLS verification for one command (\`curl -k\`, \`git -c
1837
+ http.sslVerify=false fetch\`), chmod \`644\` on a previously \`600\` config file,
1838
+ turning on a single feature-flag that lowers a check for this session only.
1839
+ - **high** \u2014 \`chmod -R 777\` on directories, disabling SELinux / AppArmor /
1068
1840
  Gatekeeper globally, flipping \`insecure_skip_verify: true\` in committed
1069
1841
  infra config, adding a permissive \`AllowUsers *\` to \`sshd_config\`,
1070
1842
  installing a new trusted root CA, committing \`NODE_TLS_REJECT_UNAUTHORIZED=0\`
@@ -1149,12 +1921,12 @@ the human without blocking it.
1149
1921
  `;
1150
1922
 
1151
1923
  // 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([])
1924
+ var AssessmentSchema = z5.object({
1925
+ category: z5.string().min(1).max(120),
1926
+ severity: z5.enum(["low", "medium", "high", "critical"]),
1927
+ justification: z5.string().min(1).max(600),
1928
+ recommended_action: z5.enum(["auto-allow", "prompt", "deny"]),
1929
+ specific_concerns: z5.array(z5.string().max(240)).max(12).default([])
1158
1930
  });
1159
1931
  var cachedPolicy = null;
1160
1932
  async function loadPolicy() {
@@ -1303,7 +2075,7 @@ function normalizeAction(a) {
1303
2075
  }
1304
2076
 
1305
2077
  // src/coordinator/tools.ts
1306
- import { z as z4 } from "zod";
2078
+ import { z as z6 } from "zod";
1307
2079
 
1308
2080
  // src/agent/subagent.ts
1309
2081
  import { streamText } from "ai";
@@ -1427,6 +2199,65 @@ function listBuiltinAgents() {
1427
2199
  }
1428
2200
 
1429
2201
  // src/agent/subagent.ts
2202
+ function subagentRolloutId(parentSessionId, subagentId) {
2203
+ return `${parentSessionId}.sub.${subagentId}`;
2204
+ }
2205
+ async function openSubagentRollout(parentSessionId, subagentId, kind, prompt, model) {
2206
+ if (!parentSessionId) return null;
2207
+ try {
2208
+ const id = subagentRolloutId(parentSessionId, subagentId);
2209
+ const roll = new Rollout(id);
2210
+ await roll.openNew({
2211
+ project: `subagent:${kind}:parent=${parentSessionId}`,
2212
+ model: model.modelId ?? "unknown"
2213
+ });
2214
+ await roll.append({
2215
+ type: "user-message",
2216
+ msgId: `${subagentId}.u0`,
2217
+ payload: { role: "user", content: prompt }
2218
+ });
2219
+ return roll;
2220
+ } catch {
2221
+ return null;
2222
+ }
2223
+ }
2224
+ async function recordSubagentStep(roll, subagentId, iteration, assistantText, toolCalls, toolResults) {
2225
+ if (!roll) return;
2226
+ try {
2227
+ await roll.append({
2228
+ type: "assistant-message",
2229
+ msgId: `${subagentId}.a${iteration}`,
2230
+ payload: { text: assistantText, toolCallCount: toolCalls.length }
2231
+ });
2232
+ for (const tc of toolCalls) {
2233
+ await roll.append({
2234
+ type: "tool-call",
2235
+ payload: { id: tc.toolCallId, name: tc.toolName, args: tc.args }
2236
+ });
2237
+ }
2238
+ for (const tr of toolResults) {
2239
+ await roll.append({
2240
+ type: "tool-result",
2241
+ payload: { id: tr.toolCallId, name: tr.toolName, result: tr.result }
2242
+ });
2243
+ }
2244
+ } catch {
2245
+ }
2246
+ }
2247
+ async function closeSubagentRollout(roll, outcome, finalText, errorMsg) {
2248
+ if (!roll) return;
2249
+ try {
2250
+ if (outcome === "error") {
2251
+ await roll.append({ type: "error", payload: { message: errorMsg ?? "unknown" } });
2252
+ }
2253
+ await roll.append({
2254
+ type: "turn-end",
2255
+ payload: { outcome, finalText, ts: (/* @__PURE__ */ new Date()).toISOString() }
2256
+ });
2257
+ await roll.close();
2258
+ } catch {
2259
+ }
2260
+ }
1430
2261
  var SUBAGENT_PROMPTS = {
1431
2262
  explore: `You are an exploration agent. Your job is to quickly search and analyze a codebase to answer questions.
1432
2263
  You have access to read, grep, and glob tools. Use them efficiently \u2014 search broadly first, then narrow down.
@@ -1451,156 +2282,18 @@ var EXPLORE_TOOL_FILTER = /* @__PURE__ */ new Set(["read", "grep", "glob", "web_
1451
2282
  var PLAN_TOOL_FILTER = /* @__PURE__ */ new Set(["read", "grep", "glob", "git", "web_fetch"]);
1452
2283
  var subagentCounter = 0;
1453
2284
  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}`);
2285
+ const { id, type, prompt, model, toolContext, maxIterations = 15 } = config;
2286
+ if (type === "builtin") {
1575
2287
  return {
1576
2288
  id,
1577
2289
  type,
1578
2290
  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
2291
  toolCalls: 0,
1598
2292
  iterations: 0,
1599
- error: `unknown built-in agent "${agentName}"`
2293
+ error: "use spawnBuiltinAgent() for built-in agents, not spawnSubagent()"
1600
2294
  };
1601
2295
  }
1602
- const maxIterations = config.maxIterations ?? agent.maxIterations;
1603
- config.onStatus?.(id, `Starting built-in agent ${agent.name}...`);
2296
+ config.onStatus?.(id, `Starting ${type} agent...`);
1604
2297
  const subagentCtx = {
1605
2298
  ...toolContext,
1606
2299
  permissionSurface: "subagent",
@@ -1608,21 +2301,31 @@ async function spawnBuiltinAgent(config) {
1608
2301
  requireConfirm: false
1609
2302
  };
1610
2303
  const allTools = buildToolMap(subagentCtx);
1611
- const allowed = new Set(agent.tools);
1612
2304
  const tools = {};
1613
- for (const [name, t] of Object.entries(allTools)) {
1614
- if (allowed.has(name)) tools[name] = t;
2305
+ if (type === "explore") {
2306
+ for (const [name, tool2] of Object.entries(allTools)) {
2307
+ if (EXPLORE_TOOL_FILTER.has(name)) tools[name] = tool2;
2308
+ }
2309
+ } else if (type === "plan") {
2310
+ for (const [name, tool2] of Object.entries(allTools)) {
2311
+ if (PLAN_TOOL_FILTER.has(name)) tools[name] = tool2;
2312
+ }
2313
+ } else {
2314
+ Object.assign(tools, allTools);
1615
2315
  }
1616
- const systemPrompt = `${agent.prompt}
2316
+ const systemPrompt = `${SUBAGENT_PROMPTS[type]}
1617
2317
 
1618
2318
  ## Working Directory
1619
2319
  ${toolContext.cwd}
1620
2320
 
1621
2321
  ## Available Tools
1622
2322
  ${Object.keys(tools).map((n) => `- ${n}`).join("\n")}`;
1623
- const messages = [{ role: "user", content: prompt }];
2323
+ const messages = [
2324
+ { role: "user", content: prompt }
2325
+ ];
1624
2326
  let iterations = 0;
1625
2327
  let totalToolCalls = 0;
2328
+ const roll = await openSubagentRollout(config.parentSessionId, id, type, prompt, model);
1626
2329
  try {
1627
2330
  while (iterations < maxIterations) {
1628
2331
  iterations++;
@@ -1658,6 +2361,7 @@ ${Object.keys(tools).map((n) => `- ${n}`).join("\n")}`;
1658
2361
  }
1659
2362
  }
1660
2363
  totalToolCalls += toolCalls.length;
2364
+ await recordSubagentStep(roll, id, iterations, fullText, toolCalls, toolResults);
1661
2365
  if (toolCalls.length > 0) {
1662
2366
  messages.push({
1663
2367
  role: "assistant",
@@ -1683,27 +2387,30 @@ ${Object.keys(tools).map((n) => `- ${n}`).join("\n")}`;
1683
2387
  continue;
1684
2388
  }
1685
2389
  config.onStatus?.(id, "Complete");
2390
+ await closeSubagentRollout(roll, "complete", fullText);
1686
2391
  return {
1687
2392
  id,
1688
- type: "builtin",
2393
+ type,
1689
2394
  text: fullText,
1690
2395
  toolCalls: totalToolCalls,
1691
2396
  iterations
1692
2397
  };
1693
2398
  }
1694
2399
  config.onStatus?.(id, "Max iterations reached");
2400
+ await closeSubagentRollout(roll, "max-iterations", "[Subagent reached max iterations]");
1695
2401
  return {
1696
2402
  id,
1697
- type: "builtin",
1698
- text: "[Built-in agent reached max iterations]",
2403
+ type,
2404
+ text: "[Subagent reached max iterations]",
1699
2405
  toolCalls: totalToolCalls,
1700
2406
  iterations
1701
2407
  };
1702
2408
  } catch (err) {
1703
2409
  config.onStatus?.(id, `Error: ${err.message}`);
2410
+ await closeSubagentRollout(roll, "error", "", err.message);
1704
2411
  return {
1705
2412
  id,
1706
- type: "builtin",
2413
+ type,
1707
2414
  text: "",
1708
2415
  toolCalls: totalToolCalls,
1709
2416
  iterations,
@@ -1711,697 +2418,489 @@ ${Object.keys(tools).map((n) => `- ${n}`).join("\n")}`;
1711
2418
  };
1712
2419
  }
1713
2420
  }
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;
2421
+ function nextSubagentId(type) {
2422
+ return `${type}-${++subagentCounter}`;
1789
2423
  }
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
- });
2424
+ async function spawnBuiltinAgent(config) {
2425
+ const { agentName, prompt, model, toolContext } = config;
2426
+ const agent = getBuiltinAgent(agentName);
2427
+ const id = config.id ?? `${agentName.toLowerCase()}-${++subagentCounter}`;
2428
+ if (!agent) {
1870
2429
  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
- })
2430
+ id,
2431
+ type: "builtin",
2432
+ text: "",
2433
+ toolCalls: 0,
2434
+ iterations: 0,
2435
+ error: `unknown built-in agent "${agentName}"`
1877
2436
  };
1878
2437
  }
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
- };
2438
+ const maxIterations = config.maxIterations ?? agent.maxIterations;
2439
+ config.onStatus?.(id, `Starting built-in agent ${agent.name}...`);
2440
+ const subagentCtx = {
2441
+ ...toolContext,
2442
+ permissionSurface: "subagent",
2443
+ subagentId: id,
2444
+ requireConfirm: false
2445
+ };
2446
+ const allTools = buildToolMap(subagentCtx);
2447
+ const allowed = new Set(agent.tools);
2448
+ const tools = {};
2449
+ for (const [name, t] of Object.entries(allTools)) {
2450
+ if (allowed.has(name)) tools[name] = t;
1917
2451
  }
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") {
2452
+ const systemPrompt = `${agent.prompt}
2453
+
2454
+ ## Working Directory
2455
+ ${toolContext.cwd}
2456
+
2457
+ ## Available Tools
2458
+ ${Object.keys(tools).map((n) => `- ${n}`).join("\n")}`;
2459
+ const messages = [{ role: "user", content: prompt }];
2460
+ let iterations = 0;
2461
+ let totalToolCalls = 0;
2462
+ const roll = await openSubagentRollout(config.parentSessionId, id, `builtin:${agentName}`, prompt, model);
2463
+ try {
2464
+ while (iterations < maxIterations) {
2465
+ iterations++;
2466
+ config.onStatus?.(id, `Iteration ${iterations}/${maxIterations}...`);
2467
+ const result = streamText({
2468
+ model,
2469
+ system: systemPrompt,
2470
+ messages,
2471
+ tools,
2472
+ maxSteps: 1
2473
+ });
2474
+ let fullText = "";
2475
+ const toolCalls = [];
2476
+ const toolResults = [];
2477
+ for await (const event of result.fullStream) {
2478
+ if (event.type === "text-delta") {
2479
+ fullText += event.textDelta;
2480
+ } else if (event.type === "tool-call") {
2481
+ toolCalls.push({
2482
+ toolCallId: event.toolCallId,
2483
+ toolName: event.toolName,
2484
+ args: event.args
2485
+ });
2486
+ config.onStatus?.(id, `${event.toolName}(...)`);
2487
+ }
2488
+ const evt = event;
2489
+ if (evt.type === "tool-result") {
2490
+ toolResults.push({
2491
+ toolCallId: evt.toolCallId,
2492
+ toolName: toolCalls.find((tc) => tc.toolCallId === evt.toolCallId)?.toolName ?? "unknown",
2493
+ result: evt.result
2494
+ });
2495
+ }
2496
+ }
2497
+ totalToolCalls += toolCalls.length;
2498
+ await recordSubagentStep(roll, id, iterations, fullText, toolCalls, toolResults);
2499
+ if (toolCalls.length > 0) {
2500
+ messages.push({
2501
+ role: "assistant",
2502
+ content: [
2503
+ ...fullText ? [{ type: "text", text: fullText }] : [],
2504
+ ...toolCalls.map((tc) => ({
2505
+ type: "tool-call",
2506
+ toolCallId: tc.toolCallId,
2507
+ toolName: tc.toolName,
2508
+ args: tc.args
2509
+ }))
2510
+ ]
2511
+ });
2512
+ messages.push({
2513
+ role: "tool",
2514
+ content: toolResults.map((tr) => ({
2515
+ type: "tool-result",
2516
+ toolCallId: tr.toolCallId,
2517
+ toolName: tr.toolName,
2518
+ result: tr.result
2519
+ }))
2520
+ });
2521
+ continue;
2522
+ }
2523
+ config.onStatus?.(id, "Complete");
2524
+ await closeSubagentRollout(roll, "complete", fullText);
1935
2525
  return {
1936
- content: JSON.stringify({
1937
- status: "already_finished",
1938
- agent_id: params.id,
1939
- agent_status: entry.status
1940
- })
2526
+ id,
2527
+ type: "builtin",
2528
+ text: fullText,
2529
+ toolCalls: totalToolCalls,
2530
+ iterations
1941
2531
  };
1942
2532
  }
1943
- entry.abortController.abort();
2533
+ config.onStatus?.(id, "Max iterations reached");
2534
+ await closeSubagentRollout(roll, "max-iterations", "[Built-in agent reached max iterations]");
1944
2535
  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
- })
2536
+ id,
2537
+ type: "builtin",
2538
+ text: "[Built-in agent reached max iterations]",
2539
+ toolCalls: totalToolCalls,
2540
+ iterations
2541
+ };
2542
+ } catch (err) {
2543
+ config.onStatus?.(id, `Error: ${err.message}`);
2544
+ await closeSubagentRollout(roll, "error", "", err.message);
2545
+ return {
2546
+ id,
2547
+ type: "builtin",
2548
+ text: "",
2549
+ toolCalls: totalToolCalls,
2550
+ iterations,
2551
+ error: err.message
1950
2552
  };
1951
2553
  }
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
2554
  }
2089
2555
 
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("|");
2556
+ // src/coordinator/runtime.ts
2557
+ var registry = /* @__PURE__ */ new Map();
2558
+ function registerAgent(entry) {
2559
+ registry.set(entry.agentId, entry);
2102
2560
  }
2103
- function makeKey(sessionId, toolName, args) {
2104
- return `${sessionId}\0${toolName}\0${fingerprintArgs(args)}`;
2561
+ function getAgent(agentId) {
2562
+ return registry.get(agentId);
2105
2563
  }
2106
- function recordInteractiveApproval(sessionId, toolName, args) {
2107
- const key = makeKey(sessionId, toolName, args);
2108
- recentApprovals.set(key, { key, expires: Date.now() + SESSION_RECENT_MS });
2564
+ function pendingCount() {
2565
+ let n = 0;
2566
+ for (const entry of registry.values()) {
2567
+ if (!entry.delivered) n++;
2568
+ }
2569
+ return n;
2109
2570
  }
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;
2571
+ function formatTaskNotification(agentId, toolUseId, status, summary, result) {
2572
+ const MAX_RESULT = 8 * 1024;
2573
+ const truncated = result.length > MAX_RESULT ? result.slice(0, MAX_RESULT) + `
2574
+
2575
+ [truncated ${result.length - MAX_RESULT} chars]` : result;
2576
+ const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2577
+ return `<task-notification>
2578
+ <task-id>${esc(agentId)}</task-id>
2579
+ <tool-use-id>${esc(toolUseId)}</tool-use-id>
2580
+ <status>${status}</status>
2581
+ <summary>${esc(summary)}</summary>
2582
+ <result>${esc(truncated)}</result>
2583
+ </task-notification>`;
2584
+ }
2585
+ async function awaitOneCompletion() {
2586
+ const pending = [];
2587
+ for (const entry of registry.values()) {
2588
+ if (!entry.delivered) pending.push(entry);
2117
2589
  }
2590
+ if (pending.length === 0) return null;
2591
+ const alreadySettled = pending.find((e) => e.result !== void 0);
2592
+ const winner = alreadySettled ? alreadySettled : await Promise.race(
2593
+ pending.map(
2594
+ (entry) => entry.completionPromise.then(() => entry).catch(() => entry)
2595
+ )
2596
+ );
2597
+ winner.delivered = true;
2598
+ const result = winner.result;
2599
+ const status = winner.status;
2600
+ 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})`;
2601
+ const text = result?.text ?? result?.error ?? "";
2602
+ const xml = formatTaskNotification(
2603
+ winner.agentId,
2604
+ winner.toolUseId,
2605
+ status,
2606
+ summary,
2607
+ text
2608
+ );
2609
+ return {
2610
+ agentId: winner.agentId,
2611
+ toolUseId: winner.toolUseId,
2612
+ status,
2613
+ summary,
2614
+ result: text,
2615
+ xml
2616
+ };
2617
+ }
2618
+ function injectCompletionAsUserMessage(messages, completion) {
2619
+ messages.push({
2620
+ role: "user",
2621
+ content: completion.xml
2622
+ });
2623
+ }
2624
+ async function pollPendingAgents(messages) {
2625
+ if (pendingCount() === 0) return false;
2626
+ const completion = await awaitOneCompletion();
2627
+ if (!completion) return false;
2628
+ injectCompletionAsUserMessage(messages, completion);
2118
2629
  return true;
2119
2630
  }
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" };
2631
+
2632
+ // src/coordinator/agent-resolver.ts
2633
+ var ROLE_HINTS = [
2634
+ {
2635
+ name: "archimedes",
2636
+ concepts: ["performance", "speed", "latency", "memory", "cpu", "throughput", "cost"],
2637
+ tasks: ["optimize", "benchmark", "measure", "slow", "faster", "hot path", "profiling"],
2638
+ strong: ["profile", "bench", "o(n", "big-o", "hotspot"]
2639
+ },
2640
+ {
2641
+ name: "euclid",
2642
+ concepts: ["proof", "invariant", "correctness", "theorem", "algorithm"],
2643
+ tasks: ["prove", "verify", "derive", "axiom", "formal"],
2644
+ strong: ["property test", "invariant", "induction", "lemma"]
2645
+ },
2646
+ {
2647
+ name: "hypatia",
2648
+ concepts: ["math", "formula", "statistics", "probability", "linear algebra", "numerical"],
2649
+ tasks: ["compute", "calculate", "regression", "distribution", "matrix"],
2650
+ strong: ["eigenvalue", "gradient", "ODE", "PDE", "markov"]
2651
+ },
2652
+ {
2653
+ name: "kepler",
2654
+ concepts: ["orbits", "pattern", "trend", "time series", "dataset"],
2655
+ tasks: ["analyze", "pattern", "forecast", "anomaly"],
2656
+ strong: ["time-series", "anomaly detection", "seasonality"]
2657
+ },
2658
+ {
2659
+ name: "plato",
2660
+ concepts: ["architecture", "design", "tradeoff", "principle", "philosophy"],
2661
+ tasks: ["design", "refactor", "tradeoff", "pattern", "architect"],
2662
+ strong: ["architectural decision", "adr", "design doc"]
2663
+ },
2664
+ {
2665
+ name: "ptolemy",
2666
+ concepts: ["map", "survey", "inventory", "catalog", "codebase", "dependency"],
2667
+ tasks: ["survey", "map out", "enumerate", "list all", "find every"],
2668
+ strong: ["dependency graph", "module map", "inventory"]
2669
+ },
2670
+ {
2671
+ name: "pythagoras",
2672
+ concepts: ["test", "assertion", "edge case", "spec", "contract"],
2673
+ tasks: ["test", "write tests", "cover", "assert", "spec"],
2674
+ strong: ["unit test", "integration test", "coverage gap"]
2675
+ },
2676
+ {
2677
+ name: "awaiter",
2678
+ concepts: ["wait", "poll", "watch", "monitor", "long-running"],
2679
+ tasks: ["wait for", "watch the", "poll", "until"],
2680
+ strong: ["long running", "deployment watch", "cron"]
2681
+ }
2682
+ ];
2683
+ var MIN_CONFIDENCE = 2;
2684
+ function scoreRole(description, hint) {
2685
+ const lower = description.toLowerCase();
2686
+ let score = 0;
2687
+ const reasons = [];
2688
+ for (const kw of hint.strong) {
2689
+ if (lower.includes(kw)) {
2690
+ score += 3;
2691
+ reasons.push(`strong:"${kw}"`);
2138
2692
  }
2139
- if (baseDecision === "deny") {
2140
- return { outcome: "deny", reason: "denied by permission config" };
2693
+ }
2694
+ for (const kw of hint.tasks) {
2695
+ if (lower.includes(kw)) {
2696
+ score += 2;
2697
+ reasons.push(`task:"${kw}"`);
2141
2698
  }
2142
- if (wasRecentlyApproved(ctx.sessionId, toolName, args)) {
2143
- return {
2144
- outcome: "allow",
2145
- silent: true,
2146
- reason: "session-recent approval (within 30s)"
2147
- };
2699
+ }
2700
+ for (const kw of hint.concepts) {
2701
+ if (lower.includes(kw)) {
2702
+ score += 1;
2703
+ reasons.push(`concept:"${kw}"`);
2148
2704
  }
2149
- return { outcome: "prompt" };
2150
2705
  }
2151
- };
2706
+ return { score, reasons };
2707
+ }
2708
+ function resolveAgent(description) {
2709
+ const agents = listBuiltinAgents();
2710
+ const byName = /* @__PURE__ */ new Map();
2711
+ for (const a of agents) byName.set(a.name, a);
2712
+ const ranked = [];
2713
+ for (const hint of ROLE_HINTS) {
2714
+ const agent = byName.get(hint.name);
2715
+ if (!agent) continue;
2716
+ const { score, reasons } = scoreRole(description, hint);
2717
+ if (score > 0) ranked.push({ agent, score, reasons });
2718
+ }
2719
+ ranked.sort((a, b) => b.score - a.score);
2720
+ const best = ranked[0] ?? null;
2721
+ const confident = best !== null && best.score >= MIN_CONFIDENCE;
2722
+ return { best, ranked, confident };
2723
+ }
2152
2724
 
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" };
2725
+ // src/coordinator/tools.ts
2726
+ var spawnParameters = z6.object({
2727
+ subagent_type: z6.enum(["explore", "plan", "general", "builtin"]).describe(
2728
+ "Worker profile: explore (read-only recon), plan (planning without edits), general (full tool access), builtin (named TOML-defined agent \u2014 provide builtin_name)."
2729
+ ),
2730
+ prompt: z6.string().describe(
2731
+ "Self-contained spec for the worker. Include purpose, file paths, acceptance criteria, and what to report back. Workers cannot see this conversation."
2732
+ ),
2733
+ description: z6.string().describe(
2734
+ "Short human-readable label for this worker, shown in status updates and in <task-notification> summaries."
2735
+ ),
2736
+ builtin_name: z6.string().optional().describe('Required when subagent_type is "builtin" \u2014 the name of the TOML-defined built-in agent to run.')
2737
+ });
2738
+ var agentSpawnTool = {
2739
+ name: "agent_spawn",
2740
+ 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.',
2741
+ parameters: spawnParameters,
2742
+ async execute(params, ctx) {
2743
+ if (!ctx.coordinatorWorkerModel) {
2744
+ return {
2745
+ 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.",
2746
+ isError: true
2747
+ };
2160
2748
  }
2161
- if (ctx.autoConfirm) {
2749
+ const { subagent_type, prompt, description, builtin_name } = params;
2750
+ if (subagent_type === "builtin" && !builtin_name) {
2751
+ const hint = resolveAgent(`${description}
2752
+ ${prompt}`);
2753
+ const suggestion = hint.confident ? ` Suggested: builtin_name="${hint.best.agent.name}" (matched ${hint.best.reasons.slice(0, 2).join(", ")}).` : "";
2162
2754
  return {
2163
- outcome: "allow",
2164
- silent: true,
2165
- reason: "--yes flag set; auto-confirming one-shot"
2755
+ content: `agent_spawn: builtin_name is required when subagent_type is "builtin".${suggestion}`,
2756
+ isError: true
2166
2757
  };
2167
2758
  }
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)`
2759
+ const agentId = subagent_type === "builtin" ? `${(builtin_name ?? "builtin").toLowerCase()}-${nextSubagentId("general").split("-")[1]}` : nextSubagentId(subagent_type);
2760
+ const abortController = new AbortController();
2761
+ const workerCtx = { ...ctx, coordinatorMode: false };
2762
+ const completionPromise = subagent_type === "builtin" ? spawnBuiltinAgent({
2763
+ id: agentId,
2764
+ agentName: builtin_name,
2765
+ prompt,
2766
+ model: ctx.coordinatorWorkerModel,
2767
+ toolContext: workerCtx
2768
+ }) : spawnSubagent({
2769
+ id: agentId,
2770
+ type: subagent_type,
2771
+ prompt,
2772
+ model: ctx.coordinatorWorkerModel,
2773
+ toolContext: workerCtx
2774
+ });
2775
+ const toolUseId = `spawn-${agentId}`;
2776
+ const entry = {
2777
+ agentId,
2778
+ toolUseId,
2779
+ description,
2780
+ completionPromise,
2781
+ abortController,
2782
+ status: "spawned",
2783
+ delivered: false,
2784
+ startedAt: Date.now()
2175
2785
  };
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" };
2786
+ registerAgent(entry);
2787
+ completionPromise.then((result) => {
2788
+ entry.result = result;
2789
+ if (abortController.signal.aborted) {
2790
+ entry.status = "killed";
2791
+ } else if (result.error) {
2792
+ entry.status = "failed";
2793
+ } else {
2794
+ entry.status = "completed";
2194
2795
  }
2195
- return { outcome: "allow", silent: true };
2196
- }
2197
- if (baseDecision === "deny") {
2198
- return { outcome: "deny", reason: "denied by permission config" };
2199
- }
2796
+ }).catch((err) => {
2797
+ entry.result = {
2798
+ id: agentId,
2799
+ type: subagent_type === "builtin" ? "builtin" : subagent_type,
2800
+ text: "",
2801
+ toolCalls: 0,
2802
+ iterations: 0,
2803
+ error: err?.message ?? String(err)
2804
+ };
2805
+ entry.status = abortController.signal.aborted ? "killed" : "failed";
2806
+ });
2200
2807
  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)`
2808
+ content: JSON.stringify({
2809
+ agent_id: agentId,
2810
+ status: "spawned",
2811
+ description,
2812
+ note: "Worker is running. You will receive a <task-notification> when it finishes."
2813
+ })
2205
2814
  };
2206
2815
  }
2207
2816
  };
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" };
2817
+ var sendMessageParameters = z6.object({
2818
+ to: z6.string().describe("The agent_id returned by agent_spawn."),
2819
+ message: z6.string().describe("Follow-up instructions for the worker.")
2820
+ });
2821
+ var agentSendMessageTool = {
2822
+ name: "agent_send_message",
2823
+ 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.",
2824
+ parameters: sendMessageParameters,
2825
+ async execute(params, _ctx) {
2826
+ const { to, message: _message } = params;
2827
+ const entry = getAgent(to);
2828
+ if (!entry) {
2829
+ return {
2830
+ content: `agent_send_message: no agent with id "${to}" in the registry. Check the agent_id you got back from agent_spawn.`,
2831
+ isError: true
2832
+ };
2833
+ }
2834
+ if (entry.status === "completed" || entry.status === "failed" || entry.status === "killed") {
2835
+ return {
2836
+ content: JSON.stringify({
2837
+ status: "unsupported",
2838
+ agent_id: to,
2839
+ agent_status: entry.status,
2840
+ 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."
2841
+ }),
2842
+ isError: true
2843
+ };
2216
2844
  }
2217
- const count = (ctx.denialCounter.get(toolName) ?? 0) + 1;
2218
- ctx.denialCounter.set(toolName, count);
2219
- const id = ctx.subagentId ?? "unknown";
2220
2845
  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)`
2846
+ content: JSON.stringify({
2847
+ status: "unsupported",
2848
+ agent_id: to,
2849
+ agent_status: entry.status,
2850
+ 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."
2851
+ }),
2852
+ isError: true
2225
2853
  };
2226
2854
  }
2227
2855
  };
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) {
2856
+ var stopParameters = z6.object({
2857
+ id: z6.string().describe("The agent_id of the worker to stop.")
2858
+ });
2859
+ var agentStopTool = {
2860
+ name: "agent_stop",
2861
+ 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".',
2862
+ parameters: stopParameters,
2863
+ async execute(params, _ctx) {
2864
+ const entry = getAgent(params.id);
2865
+ if (!entry) {
2268
2866
  return {
2269
- outcome: "deny",
2270
- silent: false,
2271
- reason,
2272
- tailNotification: `auto-mode: refused ${toolName} \u2014 ${reason}`
2867
+ content: `agent_stop: no agent with id "${params.id}" in the registry.`,
2868
+ isError: true
2273
2869
  };
2274
2870
  }
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" };
2871
+ if (entry.status === "completed" || entry.status === "failed" || entry.status === "killed") {
2872
+ return {
2873
+ content: JSON.stringify({
2874
+ status: "already_finished",
2875
+ agent_id: params.id,
2876
+ agent_status: entry.status
2877
+ })
2287
2878
  };
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
2879
  }
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
- });
2880
+ entry.abortController.abort();
2331
2881
  return {
2332
- outcome: "deny",
2333
- silent: true,
2334
- reason: "json mode requires explicit allowlist",
2335
- tailNotification: payload
2882
+ content: JSON.stringify({
2883
+ status: "abort_signaled",
2884
+ agent_id: params.id,
2885
+ note: 'Abort signal sent. The worker will finish its current step and then report back with status="killed".'
2886
+ })
2336
2887
  };
2337
2888
  }
2338
2889
  };
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
- }
2890
+ var COORDINATOR_TOOLS = [
2891
+ agentSpawnTool,
2892
+ agentSendMessageTool,
2893
+ agentStopTool
2894
+ ];
2895
+ var COORDINATOR_TOOL_NAMES = new Set(
2896
+ COORDINATOR_TOOLS.map((t) => t.name)
2897
+ );
2898
+ var COORDINATOR_ALLOWED_TOOL_NAMES = /* @__PURE__ */ new Set([
2899
+ ...COORDINATOR_TOOL_NAMES,
2900
+ "read",
2901
+ "grep",
2902
+ "glob"
2903
+ ]);
2405
2904
 
2406
2905
  // src/tools/index.ts
2407
2906
  var BUILTIN_TOOLS = [
@@ -2419,7 +2918,9 @@ var BUILTIN_TOOLS = [
2419
2918
  notebookTool,
2420
2919
  taskTool,
2421
2920
  codeModeExecTool,
2422
- codeModeWaitTool
2921
+ codeModeWaitTool,
2922
+ skillManageTool,
2923
+ eventsTool
2423
2924
  ];
2424
2925
  var mcpClients = /* @__PURE__ */ new Map();
2425
2926
  var mcpTools = [];
@@ -2590,14 +3091,14 @@ function mcpToolCount() {
2590
3091
  }
2591
3092
 
2592
3093
  export {
3094
+ drainTailNotifications,
3095
+ setCurrentSurface,
2593
3096
  MCPClient,
2594
3097
  parseMCPConfig,
2595
3098
  listBuiltinAgents,
2596
3099
  spawnSubagent,
2597
3100
  nextSubagentId,
2598
3101
  pollPendingAgents,
2599
- drainTailNotifications,
2600
- setCurrentSurface,
2601
3102
  initMCPServers,
2602
3103
  disconnectMCPServers,
2603
3104
  buildToolMap,