@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.
- package/dist/{apply-patch-D5PDUXUC.js → apply-patch-U6K67CMT.js} +1 -0
- package/dist/auth-UAMMP5IJ.js +29 -0
- package/dist/chunk-4HPRBCSY.js +167 -0
- package/dist/chunk-6NKRMZTX.js +198 -0
- package/dist/{chunk-YBYF7L4A.js → chunk-EPSOOCNB.js} +1832 -1331
- package/dist/chunk-FZVPGJJW.js +511 -0
- package/dist/chunk-GFVLHUSS.js +155 -0
- package/dist/chunk-J66N6AFH.js +137 -0
- package/dist/chunk-JXQ4HZ47.js +544 -0
- package/dist/chunk-KCAR5DOB.js +52 -0
- package/dist/chunk-KFQGP6VL.js +33 -0
- package/dist/chunk-O6AKZ4OH.js +0 -0
- package/dist/{chunk-6M6CXXWR.js → chunk-PKZKVOAN.js} +209 -1
- package/dist/{chunk-FIFC4V2R.js → chunk-PPEBWOMJ.js} +91 -7
- package/dist/compression-YJLWEHCC.js +33 -0
- package/dist/config-set-3IWEVZQ4.js +110 -0
- package/dist/{edit-JEFEK43H.js → edit-6QYAXVNU.js} +1 -0
- package/dist/{git-5T5TSQTX.js → git-DNQ5EELH.js} +1 -0
- package/dist/{github-DWRGWX6U.js → github-34T4QQIH.js} +1 -0
- package/dist/{glob-BI3P4C7Q.js → glob-XT43LEJ4.js} +1 -0
- package/dist/{grep-VZ3I5GNW.js → grep-T2CXYNRI.js} +1 -0
- package/dist/index.js +2606 -960
- package/dist/{lsp-UPY6I3L7.js → lsp-JXQVU7NP.js} +1 -0
- package/dist/model-download-3NDKS3VM.js +176 -0
- package/dist/{notebook-FXJBTSPA.js → notebook-MFODW345.js} +1 -0
- package/dist/ollama-bench-5V5CCOCQ.js +194 -0
- package/dist/ollama-launch-P5KBK7AJ.js +22 -0
- package/dist/ollama-usage-3PROM2WC.js +70 -0
- package/dist/{plugins-OG2P75K5.js → plugins-PNGRZLFW.js} +1 -0
- package/dist/{read-OVJG2XKW.js → read-B64XE7N3.js} +1 -0
- package/dist/{server-W7FRCVRZ.js → server-IGOZHW52.js} +17 -15
- package/dist/session-index-7FWEVP6E.js +22 -0
- package/dist/{shell-4X545EVN.js → shell-BOZTHQUT.js} +1 -0
- package/dist/{task-OS3E5F3X.js → task-67G4KLYC.js} +1 -0
- package/dist/{tools-Q7CDHB4K.js → tools-XWKCW4RN.js} +4 -1
- package/dist/{web-fetch-KNIV3Z3W.js → web-fetch-OTNDICGJ.js} +1 -0
- package/dist/{write-NNHLOTYK.js → write-ZOSB7I4J.js} +1 -0
- package/package.json +2 -1
- package/dist/auth-JQX6MHJG.js +0 -16
- 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/
|
|
504
|
+
// src/tools/skill-manage.ts
|
|
505
|
+
init_auth();
|
|
498
506
|
import { z as z2 } from "zod";
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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
|
|
571
|
+
const res = await fetch(url, {
|
|
644
572
|
method: "POST",
|
|
645
|
-
headers:
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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
|
-
|
|
659
|
-
|
|
580
|
+
const text = await res.text();
|
|
581
|
+
let parsed = {};
|
|
582
|
+
try {
|
|
583
|
+
parsed = JSON.parse(text);
|
|
584
|
+
} catch {
|
|
660
585
|
}
|
|
661
|
-
|
|
662
|
-
|
|
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
|
-
|
|
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
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
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
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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
|
-
|
|
719
|
-
|
|
720
|
-
|
|
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
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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
|
-
|
|
792
|
-
|
|
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
|
-
|
|
835
|
-
|
|
836
|
-
|
|
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/
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
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
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
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
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
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
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
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
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1005
|
-
|
|
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
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
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
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
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
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
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
|
-
|
|
1269
|
+
// src/mcp/client.ts
|
|
1270
|
+
import { z as z4 } from "zod";
|
|
1058
1271
|
|
|
1059
|
-
|
|
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
|
-
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
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 =
|
|
1153
|
-
category:
|
|
1154
|
-
severity:
|
|
1155
|
-
justification:
|
|
1156
|
-
recommended_action:
|
|
1157
|
-
specific_concerns:
|
|
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
|
|
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:
|
|
2293
|
+
error: "use spawnBuiltinAgent() for built-in agents, not spawnSubagent()"
|
|
1600
2294
|
};
|
|
1601
2295
|
}
|
|
1602
|
-
|
|
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
|
-
|
|
1614
|
-
|
|
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 = `${
|
|
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 = [
|
|
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
|
|
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
|
|
1698
|
-
text: "[
|
|
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
|
|
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
|
-
|
|
1716
|
-
var registry = /* @__PURE__ */ new Map();
|
|
1717
|
-
function registerAgent(entry) {
|
|
1718
|
-
registry.set(entry.agentId, entry);
|
|
1719
|
-
}
|
|
1720
|
-
function getAgent(agentId) {
|
|
1721
|
-
return registry.get(agentId);
|
|
1722
|
-
}
|
|
1723
|
-
function pendingCount() {
|
|
1724
|
-
let n = 0;
|
|
1725
|
-
for (const entry of registry.values()) {
|
|
1726
|
-
if (!entry.delivered) n++;
|
|
1727
|
-
}
|
|
1728
|
-
return n;
|
|
1729
|
-
}
|
|
1730
|
-
function formatTaskNotification(agentId, toolUseId, status, summary, result) {
|
|
1731
|
-
const MAX_RESULT = 8 * 1024;
|
|
1732
|
-
const truncated = result.length > MAX_RESULT ? result.slice(0, MAX_RESULT) + `
|
|
1733
|
-
|
|
1734
|
-
[truncated ${result.length - MAX_RESULT} chars]` : result;
|
|
1735
|
-
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1736
|
-
return `<task-notification>
|
|
1737
|
-
<task-id>${esc(agentId)}</task-id>
|
|
1738
|
-
<tool-use-id>${esc(toolUseId)}</tool-use-id>
|
|
1739
|
-
<status>${status}</status>
|
|
1740
|
-
<summary>${esc(summary)}</summary>
|
|
1741
|
-
<result>${esc(truncated)}</result>
|
|
1742
|
-
</task-notification>`;
|
|
1743
|
-
}
|
|
1744
|
-
async function awaitOneCompletion() {
|
|
1745
|
-
const pending = [];
|
|
1746
|
-
for (const entry of registry.values()) {
|
|
1747
|
-
if (!entry.delivered) pending.push(entry);
|
|
1748
|
-
}
|
|
1749
|
-
if (pending.length === 0) return null;
|
|
1750
|
-
const alreadySettled = pending.find((e) => e.result !== void 0);
|
|
1751
|
-
const winner = alreadySettled ? alreadySettled : await Promise.race(
|
|
1752
|
-
pending.map(
|
|
1753
|
-
(entry) => entry.completionPromise.then(() => entry).catch(() => entry)
|
|
1754
|
-
)
|
|
1755
|
-
);
|
|
1756
|
-
winner.delivered = true;
|
|
1757
|
-
const result = winner.result;
|
|
1758
|
-
const status = winner.status;
|
|
1759
|
-
const summary = status === "completed" ? `Agent "${winner.description}" completed` : status === "failed" ? `Agent "${winner.description}" failed: ${result?.error ?? "unknown error"}` : status === "killed" ? `Agent "${winner.description}" was stopped` : `Agent "${winner.description}" ended (${status})`;
|
|
1760
|
-
const text = result?.text ?? result?.error ?? "";
|
|
1761
|
-
const xml = formatTaskNotification(
|
|
1762
|
-
winner.agentId,
|
|
1763
|
-
winner.toolUseId,
|
|
1764
|
-
status,
|
|
1765
|
-
summary,
|
|
1766
|
-
text
|
|
1767
|
-
);
|
|
1768
|
-
return {
|
|
1769
|
-
agentId: winner.agentId,
|
|
1770
|
-
toolUseId: winner.toolUseId,
|
|
1771
|
-
status,
|
|
1772
|
-
summary,
|
|
1773
|
-
result: text,
|
|
1774
|
-
xml
|
|
1775
|
-
};
|
|
1776
|
-
}
|
|
1777
|
-
function injectCompletionAsUserMessage(messages, completion) {
|
|
1778
|
-
messages.push({
|
|
1779
|
-
role: "user",
|
|
1780
|
-
content: completion.xml
|
|
1781
|
-
});
|
|
1782
|
-
}
|
|
1783
|
-
async function pollPendingAgents(messages) {
|
|
1784
|
-
if (pendingCount() === 0) return false;
|
|
1785
|
-
const completion = await awaitOneCompletion();
|
|
1786
|
-
if (!completion) return false;
|
|
1787
|
-
injectCompletionAsUserMessage(messages, completion);
|
|
1788
|
-
return true;
|
|
2421
|
+
function nextSubagentId(type) {
|
|
2422
|
+
return `${type}-${++subagentCounter}`;
|
|
1789
2423
|
}
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
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
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
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
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
if (
|
|
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
|
-
|
|
1920
|
-
|
|
1921
|
-
}
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
};
|
|
1933
|
-
|
|
1934
|
-
|
|
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
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
2526
|
+
id,
|
|
2527
|
+
type: "builtin",
|
|
2528
|
+
text: fullText,
|
|
2529
|
+
toolCalls: totalToolCalls,
|
|
2530
|
+
iterations
|
|
1941
2531
|
};
|
|
1942
2532
|
}
|
|
1943
|
-
|
|
2533
|
+
config.onStatus?.(id, "Max iterations reached");
|
|
2534
|
+
await closeSubagentRollout(roll, "max-iterations", "[Built-in agent reached max iterations]");
|
|
1944
2535
|
return {
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
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/
|
|
2091
|
-
var
|
|
2092
|
-
|
|
2093
|
-
|
|
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
|
|
2104
|
-
return
|
|
2561
|
+
function getAgent(agentId) {
|
|
2562
|
+
return registry.get(agentId);
|
|
2105
2563
|
}
|
|
2106
|
-
function
|
|
2107
|
-
|
|
2108
|
-
|
|
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
|
|
2111
|
-
const
|
|
2112
|
-
const
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
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
|
-
|
|
2140
|
-
|
|
2693
|
+
}
|
|
2694
|
+
for (const kw of hint.tasks) {
|
|
2695
|
+
if (lower.includes(kw)) {
|
|
2696
|
+
score += 2;
|
|
2697
|
+
reasons.push(`task:"${kw}"`);
|
|
2141
2698
|
}
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
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/
|
|
2154
|
-
var
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2164
|
-
|
|
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
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
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
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
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
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
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
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
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
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
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
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
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
|
-
|
|
2230
|
-
|
|
2231
|
-
var
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
const
|
|
2237
|
-
|
|
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
|
-
|
|
2270
|
-
|
|
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 (
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
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
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
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,
|