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