@ra3orblade/swarm 0.13.2 → 0.15.0
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/README.md +15 -12
- package/dist/swarm-hook.js +1155 -175
- package/dist/swarm-mcp.js +2 -1
- package/dist/swarm.js +871 -255
- package/dist/swarmd.js +4872 -1431
- package/package.json +1 -1
- package/web/dashboard.css +1 -1
- package/web/dashboard.js +23 -19
- package/web/icons.js +2 -2
- package/web/release-notes.js +1 -1
package/dist/swarm-hook.js
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
|
+
var __esm = (fn, res, err) => () => {
|
|
4
|
+
if (fn)
|
|
5
|
+
try {
|
|
6
|
+
res = fn(fn = 0);
|
|
7
|
+
} catch (e) {
|
|
8
|
+
err = [e];
|
|
9
|
+
}
|
|
10
|
+
if (err)
|
|
11
|
+
throw err[0];
|
|
12
|
+
return res;
|
|
13
|
+
};
|
|
3
14
|
|
|
4
|
-
// packages/
|
|
5
|
-
|
|
6
|
-
import { dirname, join as join2 } from "path";
|
|
15
|
+
// packages/client/src/bins.ts
|
|
16
|
+
var init_bins = () => {};
|
|
7
17
|
|
|
8
18
|
// packages/client/src/daemon.ts
|
|
9
19
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
@@ -12,8 +22,6 @@ import { join } from "path";
|
|
|
12
22
|
function swarmHome() {
|
|
13
23
|
return process.env.SWARM_HOME ?? join(homedir(), ".swarm");
|
|
14
24
|
}
|
|
15
|
-
var DEFAULT_PORT = Number(process.env.SWARM_PORT ?? 7777);
|
|
16
|
-
var infoFile = () => join(swarmHome(), "daemon.json");
|
|
17
25
|
function readDaemonInfo() {
|
|
18
26
|
const file = infoFile();
|
|
19
27
|
if (!existsSync(file))
|
|
@@ -24,7 +32,6 @@ function readDaemonInfo() {
|
|
|
24
32
|
return null;
|
|
25
33
|
}
|
|
26
34
|
}
|
|
27
|
-
var tokenFile = (home = swarmHome()) => join(home, "token");
|
|
28
35
|
function readToken(home = swarmHome()) {
|
|
29
36
|
try {
|
|
30
37
|
const t = readFileSync(tokenFile(home), "utf8").trim();
|
|
@@ -33,153 +40,329 @@ function readToken(home = swarmHome()) {
|
|
|
33
40
|
return null;
|
|
34
41
|
}
|
|
35
42
|
}
|
|
36
|
-
|
|
43
|
+
function resolveBaseUrl(explicit) {
|
|
44
|
+
const raw = explicit ?? process.env.SWARM_URL ?? readDaemonInfo()?.url ?? `http://127.0.0.1:${DEFAULT_PORT}`;
|
|
45
|
+
return raw.replace(/\/$/, "");
|
|
46
|
+
}
|
|
47
|
+
var DEFAULT_PORT, infoFile = () => join(swarmHome(), "daemon.json"), tokenFile = (home = swarmHome()) => join(home, "token"), authedFetch = (input, init) => {
|
|
37
48
|
const h = new Headers(init?.headers);
|
|
38
49
|
const t = readToken();
|
|
39
50
|
if (t && !h.has("authorization"))
|
|
40
51
|
h.set("authorization", `Bearer ${t}`);
|
|
41
52
|
return fetch(input, { ...init, headers: h });
|
|
42
53
|
};
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
54
|
+
var init_daemon = __esm(() => {
|
|
55
|
+
init_bins();
|
|
56
|
+
DEFAULT_PORT = Number(process.env.SWARM_PORT ?? 7777);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// packages/client/src/index.ts
|
|
60
|
+
var init_src = __esm(() => {
|
|
61
|
+
init_daemon();
|
|
62
|
+
init_bins();
|
|
63
|
+
init_daemon();
|
|
64
|
+
});
|
|
47
65
|
// packages/core/src/actor.ts
|
|
48
|
-
var HUMAN_ALIASES
|
|
49
|
-
var
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
"
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
"
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
"
|
|
123
|
-
"
|
|
124
|
-
|
|
125
|
-
"
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
"
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
66
|
+
var HUMAN_ALIASES, DAEMON_ALIASES;
|
|
67
|
+
var init_actor = __esm(() => {
|
|
68
|
+
HUMAN_ALIASES = new Set(["cli", "dashboard", "me", "desktop", "human"]);
|
|
69
|
+
DAEMON_ALIASES = new Set(["daemon", "system", "swarm"]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// packages/core/src/adapters/aider/history.ts
|
|
73
|
+
var init_history = () => {};
|
|
74
|
+
// packages/core/src/adapters/claude-code/adapter.ts
|
|
75
|
+
var init_adapter = () => {};
|
|
76
|
+
|
|
77
|
+
// packages/core/src/adapters/codex/rollout.ts
|
|
78
|
+
var init_rollout = () => {};
|
|
79
|
+
|
|
80
|
+
// packages/core/src/adapters/gemini/chats.ts
|
|
81
|
+
var init_chats = () => {};
|
|
82
|
+
|
|
83
|
+
// packages/core/src/adapters/grok/updates.ts
|
|
84
|
+
var init_updates = () => {};
|
|
85
|
+
|
|
86
|
+
// packages/core/src/adapters/opencode/db.ts
|
|
87
|
+
var init_db = () => {};
|
|
88
|
+
// packages/core/src/adapters/index.ts
|
|
89
|
+
var init_adapters = __esm(() => {
|
|
90
|
+
init_history();
|
|
91
|
+
init_adapter();
|
|
92
|
+
init_rollout();
|
|
93
|
+
init_chats();
|
|
94
|
+
init_updates();
|
|
95
|
+
init_db();
|
|
96
|
+
init_history();
|
|
97
|
+
init_adapter();
|
|
98
|
+
init_rollout();
|
|
99
|
+
init_chats();
|
|
100
|
+
init_updates();
|
|
101
|
+
init_db();
|
|
102
|
+
});
|
|
103
|
+
// packages/core/src/prompt.ts
|
|
104
|
+
var init_prompt = () => {};
|
|
105
|
+
|
|
106
|
+
// packages/core/src/adapters/claude-code/hooks.ts
|
|
107
|
+
var OWN_SUMMARY;
|
|
108
|
+
var init_hooks = __esm(() => {
|
|
109
|
+
init_prompt();
|
|
110
|
+
OWN_SUMMARY = new Set([
|
|
111
|
+
"Bash",
|
|
112
|
+
"Read",
|
|
113
|
+
"Edit",
|
|
114
|
+
"Write",
|
|
115
|
+
"MultiEdit",
|
|
116
|
+
"NotebookEdit",
|
|
117
|
+
"Glob",
|
|
118
|
+
"Grep",
|
|
119
|
+
"Agent",
|
|
120
|
+
"Task",
|
|
121
|
+
"WebFetch",
|
|
122
|
+
"WebSearch",
|
|
123
|
+
"AskUserQuestion"
|
|
124
|
+
]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// packages/core/src/guards.ts
|
|
128
|
+
function expandHome(s, home) {
|
|
129
|
+
return s.replace(/(^|[\s="'`:])(?:~|\$HOME|\$\{HOME\})(?=\/|$|[\s"'`])/g, `$1${home}`);
|
|
130
|
+
}
|
|
131
|
+
function segments(cmd) {
|
|
132
|
+
return cmd.split(/\s*(?:&&|\|\||;|\n)\s*/).map((s) => s.trim()).filter(Boolean);
|
|
133
|
+
}
|
|
134
|
+
function words(seg) {
|
|
135
|
+
return (seg.match(/"[^"]*"|'[^']*'|\S+/g) ?? []).map((w) => w.replace(/^["']|["']$/g, ""));
|
|
136
|
+
}
|
|
137
|
+
function dangerousTarget(target, home, toplevel) {
|
|
138
|
+
if (/^\$\{?\w+\}?(\/|$)/.test(target) && !target.startsWith(home))
|
|
139
|
+
return `starts with ${target.match(/^\$\{?\w+\}?/)?.[0]} \u2014 if that is ever unset, it is /`;
|
|
140
|
+
const t = trimSlash(target.replace(/\/\*$/, "/").replace(/\/\.$/, "/"));
|
|
141
|
+
if (t === "*" || target === "/*")
|
|
142
|
+
return "removes everything the glob matches, from the top";
|
|
143
|
+
if (t === "." || t === ".." || t.startsWith("../"))
|
|
144
|
+
return "removes the working directory or its parent";
|
|
145
|
+
if (t === home || t === trimSlash(home))
|
|
146
|
+
return "removes the home directory";
|
|
147
|
+
if (!t.startsWith("/"))
|
|
148
|
+
return null;
|
|
149
|
+
if (SYSTEM_DIRS.includes(t))
|
|
150
|
+
return `removes ${t}`;
|
|
151
|
+
if (t.split("/").length <= 3 && /^\/(Users|home)\//.test(t))
|
|
152
|
+
return `removes the home directory ${t}`;
|
|
153
|
+
if (SCRATCH.test(t))
|
|
154
|
+
return null;
|
|
155
|
+
if (toplevel && !(t === toplevel || t.startsWith(`${trimSlash(toplevel)}/`)))
|
|
156
|
+
return `removes ${t}, outside this repository`;
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
function destructiveFs(cmd, home, toplevel) {
|
|
160
|
+
const c = expandHome(cmd, home);
|
|
161
|
+
for (const seg of segments(c)) {
|
|
162
|
+
const w = words(seg.replace(/^sudo\s+/, ""));
|
|
163
|
+
if (w[0] === "rm") {
|
|
164
|
+
const flags = w.filter((x) => x.startsWith("-"));
|
|
165
|
+
const recursive = flags.some((f) => /^-[a-zA-Z]*[rR]/.test(f) || f === "--recursive");
|
|
166
|
+
if (!recursive)
|
|
167
|
+
continue;
|
|
168
|
+
for (const target of w.slice(1).filter((x) => !x.startsWith("-"))) {
|
|
169
|
+
const why = dangerousTarget(target, home, toplevel);
|
|
170
|
+
if (why)
|
|
171
|
+
return { what: `\`rm -r ${target}\` ${why}` };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (w[0] === "find" && (w.includes("-delete") || /-exec\s+rm\b/.test(seg))) {
|
|
175
|
+
const root = w[1] ?? ".";
|
|
176
|
+
const why = dangerousTarget(root, home, toplevel);
|
|
177
|
+
if (why && root !== ".")
|
|
178
|
+
return { what: `\`find ${root} \u2026 -delete\` ${why}` };
|
|
179
|
+
}
|
|
180
|
+
if (/^(mkfs(\.\w+)?|newfs\w*|wipefs)$/.test(w[0] ?? ""))
|
|
181
|
+
return { what: `\`${w[0]}\` formats a disk` };
|
|
182
|
+
if (w[0] === "dd" && w.some((x) => /^of=\/dev\//.test(x)))
|
|
183
|
+
return { what: "`dd` writes over a device" };
|
|
184
|
+
if (w[0] === "diskutil" && /^(erase|zero|secureErase|reformat)/i.test(w[1] ?? ""))
|
|
185
|
+
return { what: `\`diskutil ${w[1]}\` erases a disk` };
|
|
186
|
+
if ((w[0] === "chmod" || w[0] === "chown") && w.some((x) => /^-[a-zA-Z]*R/.test(x))) {
|
|
187
|
+
const target = w[w.length - 1] ?? "";
|
|
188
|
+
if (target.startsWith("/") || target === home) {
|
|
189
|
+
const why = dangerousTarget(target, home, toplevel);
|
|
190
|
+
if (why)
|
|
191
|
+
return { what: `\`${w[0]} -R ${target}\` ${why.replace(/^removes/, "changes")}` };
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
function destructiveInfra(cmd) {
|
|
198
|
+
for (const r of INFRA)
|
|
199
|
+
if (r.re.test(cmd) && (!r.needs || r.needs.test(cmd)))
|
|
200
|
+
return { what: r.what };
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
function pipeToShell(cmd) {
|
|
204
|
+
if (new RegExp(`${FETCHER}[^|;&]*\\|\\s*(${INTERP})\\b`).test(cmd))
|
|
205
|
+
return { what: "pipes a downloaded script straight into an interpreter" };
|
|
206
|
+
if (/\b((ba|z)?sh|source|\.)\s+<\(\s*(curl|wget)\b/.test(cmd))
|
|
207
|
+
return { what: "runs a downloaded script via process substitution" };
|
|
208
|
+
if (/\b(ba|z)?sh\s+-c\s+["']?\$\(\s*(curl|wget)\b/.test(cmd))
|
|
209
|
+
return { what: "runs a downloaded script via command substitution" };
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
function secretFileIn(text) {
|
|
213
|
+
return SECRET_FILES.find((s) => s.re.test(text))?.what ?? null;
|
|
214
|
+
}
|
|
215
|
+
function secretCommand(cmd) {
|
|
216
|
+
if (/\bsecurity\s+find-(generic|internet)-password\b[^|;&]*\s-[a-z]*w/.test(cmd))
|
|
217
|
+
return { what: "prints a password from the macOS keychain" };
|
|
218
|
+
if (/\b(printenv|env)\s*(\||>|$)/.test(cmd) && /\|\s*(curl|nc|ncat)\b/.test(cmd))
|
|
219
|
+
return { what: "sends the environment over the network" };
|
|
220
|
+
for (const seg of segments(cmd)) {
|
|
221
|
+
if (!READERS.test(seg))
|
|
222
|
+
continue;
|
|
223
|
+
const w = words(seg);
|
|
224
|
+
const copy = /^(cp|scp|rsync)$/.test(w[0] ?? "");
|
|
225
|
+
const text = copy ? w.slice(1, -1).filter((x) => !x.startsWith("-")).join(" ") : seg;
|
|
226
|
+
const what = secretFileIn(text);
|
|
227
|
+
if (what)
|
|
228
|
+
return { what: `${copy ? "copies" : "reads"} ${what}` };
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
function secretFile(tool, path) {
|
|
233
|
+
const what = secretFileIn(path);
|
|
234
|
+
if (!what)
|
|
235
|
+
return null;
|
|
236
|
+
if (tool === "Read")
|
|
237
|
+
return { what: `reads ${what}` };
|
|
238
|
+
if (/\.env(\.[\w-]+)?$|\.(pem|p12|pfx|key)$/i.test(path))
|
|
239
|
+
return { what: `writes ${what}` };
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
function guardConfigFile(path, home) {
|
|
243
|
+
const p = expandHome(path, home);
|
|
244
|
+
const h = trimSlash(home);
|
|
245
|
+
if (/(^|\/)\.claude\/settings(\.local)?\.json$/.test(p))
|
|
246
|
+
return "Claude Code settings (hooks and permissions)";
|
|
247
|
+
if (p === `${h}/.claude.json`)
|
|
248
|
+
return "Claude Code's MCP registration";
|
|
249
|
+
if (p.startsWith(`${h}/.swarm/`) || p === `${h}/.swarm`)
|
|
250
|
+
return "Swarm's config and ledger";
|
|
251
|
+
if (/(^|\/)\.swarm\.toml$/.test(p))
|
|
252
|
+
return "this repo's Swarm rules";
|
|
253
|
+
if (p === `${h}/.codex/config.toml` || p === `${h}/.gemini/settings.json` || p === `${h}/.cursor/hooks.json`)
|
|
254
|
+
return "another agent's hook config";
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
function configTamperCommand(cmd, home) {
|
|
258
|
+
if (/(^|[\s;&|])swarm\s+uninstall\b/.test(cmd))
|
|
259
|
+
return { what: "uninstalls Swarm's hooks" };
|
|
260
|
+
for (const seg of segments(expandHome(cmd, home))) {
|
|
261
|
+
if (!WRITERS.test(seg))
|
|
262
|
+
continue;
|
|
263
|
+
for (const w of words(seg)) {
|
|
264
|
+
const target = w.replace(/^>+/, "");
|
|
265
|
+
if (!target.includes("/") && !target.startsWith("."))
|
|
266
|
+
continue;
|
|
267
|
+
const what = guardConfigFile(target, home);
|
|
268
|
+
if (what)
|
|
269
|
+
return { what: `changes ${what} (${target})` };
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
var trimSlash = (p) => p.length > 1 ? p.replace(/\/+$/, "") : p, SYSTEM_DIRS, SCRATCH, DB_CLIENT, INFRA, FETCHER, INTERP, SECRET_FILES, READERS, WRITERS;
|
|
275
|
+
var init_guards = __esm(() => {
|
|
276
|
+
SYSTEM_DIRS = [
|
|
277
|
+
"/",
|
|
278
|
+
"/bin",
|
|
279
|
+
"/boot",
|
|
280
|
+
"/dev",
|
|
281
|
+
"/etc",
|
|
282
|
+
"/home",
|
|
283
|
+
"/lib",
|
|
284
|
+
"/opt",
|
|
285
|
+
"/private",
|
|
286
|
+
"/root",
|
|
287
|
+
"/sbin",
|
|
288
|
+
"/System",
|
|
289
|
+
"/Library",
|
|
290
|
+
"/Applications",
|
|
291
|
+
"/Users",
|
|
292
|
+
"/usr",
|
|
293
|
+
"/var"
|
|
294
|
+
];
|
|
295
|
+
SCRATCH = /^(\/tmp|\/private\/tmp|\/var\/folders|\/private\/var\/folders)(\/|$)/;
|
|
296
|
+
DB_CLIENT = /\b(psql|mysql|mariadb|sqlite3|sqlcmd|clickhouse(-client)?|cockroach|mongo(sh)?|duckdb)\b/i;
|
|
297
|
+
INFRA = [
|
|
298
|
+
{
|
|
299
|
+
what: "`terraform destroy` tears the stack down",
|
|
300
|
+
re: /\b(terraform|tofu)\s+(\S+\s+)*destroy\b/
|
|
301
|
+
},
|
|
302
|
+
{ what: "`terraform state rm` forgets live resources", re: /\b(terraform|tofu)\s+state\s+rm\b/ },
|
|
303
|
+
{
|
|
304
|
+
what: "`kubectl delete` of a namespace or everything",
|
|
305
|
+
re: /\bkubectl\s+(\S+\s+)*delete\s+(\S+\s+)*(ns|namespaces?|pvc?|persistentvolume(claim)?s?|--all\b|-A\b|--all-namespaces\b)/
|
|
306
|
+
},
|
|
307
|
+
{ what: "`kubectl drain` evicts a node's workloads", re: /\bkubectl\s+(\S+\s+)*drain\b/ },
|
|
308
|
+
{ what: "`helm uninstall` removes a release", re: /\bhelm\s+(\S+\s+)*(uninstall|delete)\b/ },
|
|
309
|
+
{
|
|
310
|
+
what: "`aws s3 rm --recursive` empties a bucket path",
|
|
311
|
+
re: /\baws\s+s3\s+rm\b[^|;&]*--recursive/
|
|
312
|
+
},
|
|
313
|
+
{ what: "`aws s3 rb` deletes a bucket", re: /\baws\s+s3\s+rb\b/ },
|
|
314
|
+
{
|
|
315
|
+
what: "an AWS delete / terminate call",
|
|
316
|
+
re: /\baws\s+[\w-]+\s+(terminate-instances|delete-(db-instance|db-cluster|stack|table|bucket|cluster|function|user|role|repository))\b/
|
|
317
|
+
},
|
|
318
|
+
{ what: "a `gcloud \u2026 delete`", re: /\bgcloud\s+(\S+\s+)+delete\b/ },
|
|
319
|
+
{ what: "an `az \u2026 delete`", re: /\baz\s+(\S+\s+)+delete\b/ },
|
|
320
|
+
{
|
|
321
|
+
what: "`docker system prune` removes images and volumes",
|
|
322
|
+
re: /\bdocker\s+system\s+prune\b[^|;&]*(-a\b|--all\b|--volumes\b)/
|
|
323
|
+
},
|
|
324
|
+
{ what: "`docker volume rm/prune` deletes data volumes", re: /\bdocker\s+volume\s+(rm|prune)\b/ },
|
|
325
|
+
{ what: "a SQL `DROP`", re: /\bdrop\s+(database|schema|table)\b/i, needs: DB_CLIENT },
|
|
326
|
+
{ what: "a SQL `TRUNCATE`", re: /\btruncate\s+(table\s+)?\w/i, needs: DB_CLIENT },
|
|
327
|
+
{
|
|
328
|
+
what: "an unbounded SQL `DELETE`",
|
|
329
|
+
re: /\bdelete\s+from\s+[\w."]+\s*(;|"|'|$)/i,
|
|
330
|
+
needs: DB_CLIENT
|
|
331
|
+
},
|
|
332
|
+
{ what: "`dropdb` deletes a database", re: /(^|[\s;&|])dropdb\s/ },
|
|
333
|
+
{
|
|
334
|
+
what: "a database reset",
|
|
335
|
+
re: /\b(prisma\s+migrate\s+reset|rails\s+db:(drop|reset)|rake\s+db:(drop|reset)|supabase\s+db\s+reset)\b/
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
what: "`redis-cli FLUSHALL/FLUSHDB` wipes the store",
|
|
339
|
+
re: /\bredis-cli\b[^|;&]*\bflush(all|db)\b/i
|
|
340
|
+
},
|
|
341
|
+
{ what: "`dropDatabase()` in the Mongo shell", re: /\bdropDatabase\s*\(/, needs: DB_CLIENT }
|
|
342
|
+
];
|
|
343
|
+
FETCHER = String.raw`\b(curl|wget|fetch|http|xh)\b`;
|
|
344
|
+
INTERP = String.raw`(sudo\s+(-\S+\s+)*)?(env\s+(\w+=\S+\s+)*)?(ba|z|k|da|fi)?sh|python[0-9.]*|node|perl|ruby|php|iex`;
|
|
345
|
+
SECRET_FILES = [
|
|
346
|
+
{
|
|
347
|
+
what: "a .env file",
|
|
348
|
+
re: /(^|[/\s"'`=@])\.env(\.(?!example\b|sample\b|template\b|dist\b|defaults?\b)[\w-]+)?(?=$|[\s"'`;|&)])/
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
what: "an SSH private key",
|
|
352
|
+
re: /(^|[/\s"'`])(id_rsa|id_ed25519|id_ecdsa|id_dsa)(?=$|[\s"'`;|&)])/
|
|
353
|
+
},
|
|
354
|
+
{ what: "AWS credentials", re: /\.aws\/credentials\b/ },
|
|
355
|
+
{ what: "an npm token file", re: /(^|[/\s"'`])\.npmrc\b/ },
|
|
356
|
+
{ what: "a kubeconfig", re: /\.kube\/config\b/ },
|
|
357
|
+
{ what: "Google Cloud credentials", re: /gcloud\/[\w-]*credential/i },
|
|
358
|
+
{ what: "a private key file", re: /[\w-]\.(pem|p12|pfx|key)(?=$|[\s"'`;|&)])/i },
|
|
359
|
+
{ what: "a netrc", re: /(^|[/\s"'`])\.netrc\b/ }
|
|
360
|
+
];
|
|
361
|
+
READERS = /(^|[\s;&|(])(cat|less|more|head|tail|bat|strings|base64|xxd|od|hexdump|grep|egrep|rg|ag|awk|nl|tac|cp|scp|rsync|openssl|gpg|pbcopy)\b|curl\b[^|;&]*(-d|--data[\w-]*|-F|--form|-T|--upload-file)\s*@/;
|
|
362
|
+
WRITERS = /(^|[\s;&|(])(>>?|tee|rm|mv|cp|truncate|ln|chmod|chown|unlink|shred)\b|>>?\s*\S|\bsed\s+(-[a-zA-Z]*\s+)*-i|\bperl\s+-[a-zA-Z]*i|\bsqlite3\b[^|;&]*\b(delete|update|drop|insert)\b/i;
|
|
363
|
+
});
|
|
364
|
+
|
|
181
365
|
// packages/core/src/rules.ts
|
|
182
|
-
var LIVE_WINDOW_MS = 10 * 60000;
|
|
183
366
|
function otherLiveInSameTree(current, sessions, now, withinMs = LIVE_WINDOW_MS) {
|
|
184
367
|
if (!current.toplevel)
|
|
185
368
|
return null;
|
|
@@ -196,7 +379,6 @@ function otherLiveInSameTree(current, sessions, now, withinMs = LIVE_WINDOW_MS)
|
|
|
196
379
|
}
|
|
197
380
|
return null;
|
|
198
381
|
}
|
|
199
|
-
var GIT_GLOBAL_OPTS = "(?:\\s+(?:-C\\s+\\S+|-c\\s+\\S+|--git-dir(?:=|\\s+)\\S+|--work-tree(?:=|\\s+)\\S+|--namespace(?:=|\\s+)\\S+|--no-pager|--paginate|--bare|--literal-pathspecs|--exec-path(?:=\\S+)?))*";
|
|
200
382
|
function gitVerb(verb, tail) {
|
|
201
383
|
return new RegExp(`\\bgit${GIT_GLOBAL_OPTS}\\s+${verb}${tail}`);
|
|
202
384
|
}
|
|
@@ -230,21 +412,149 @@ function killedPorts(cmd) {
|
|
|
230
412
|
ports.add(Number(m[1]));
|
|
231
413
|
return [...ports];
|
|
232
414
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
415
|
+
function familyHitsBash(cmd, home, toplevel) {
|
|
416
|
+
const out = [];
|
|
417
|
+
const add = (rule, h) => h && out.push({ rule, what: h.what });
|
|
418
|
+
add("destructive_fs", destructiveFs(cmd, home, toplevel));
|
|
419
|
+
add("destructive_infra", destructiveInfra(cmd));
|
|
420
|
+
add("pipe_to_shell", pipeToShell(cmd));
|
|
421
|
+
add("secrets", secretCommand(cmd));
|
|
422
|
+
add("config_tamper", configTamperCommand(cmd, home));
|
|
423
|
+
return out;
|
|
424
|
+
}
|
|
425
|
+
function familyHitsFile(tool, path, home) {
|
|
426
|
+
const out = [];
|
|
427
|
+
const secret = secretFile(tool, path);
|
|
428
|
+
if (secret)
|
|
429
|
+
out.push({ rule: "secrets", what: secret.what });
|
|
430
|
+
if (WRITE_TOOLS.has(tool)) {
|
|
431
|
+
const what = guardConfigFile(path, home);
|
|
432
|
+
if (what)
|
|
433
|
+
out.push({ rule: "config_tamper", what: `changes ${what} (${path})` });
|
|
434
|
+
}
|
|
435
|
+
return out;
|
|
436
|
+
}
|
|
437
|
+
function familyDecision(hits, modes) {
|
|
438
|
+
for (const h of hits) {
|
|
439
|
+
const mode = modes[h.rule] ?? "off";
|
|
440
|
+
if (mode !== "off")
|
|
441
|
+
return { action: mode, rule: h.rule, reason: FAMILY_REASON[h.rule](h.what) };
|
|
442
|
+
}
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
function guardFile(tool, path, modes = DEFAULT_MODES, home = process.env.HOME ?? "") {
|
|
446
|
+
if ((modes.secrets ?? "off") === "off" && (modes.config_tamper ?? "off") === "off")
|
|
447
|
+
return { action: "allow" };
|
|
448
|
+
return familyDecision(familyHitsFile(tool, path, home), modes) ?? { action: "allow" };
|
|
449
|
+
}
|
|
450
|
+
function isSingleCommand(cmd) {
|
|
451
|
+
return !/[&|;<>`\n]/.test(cmd) && !cmd.includes("$(");
|
|
452
|
+
}
|
|
453
|
+
function stripFlagsOutsideQuotes(cmd, flags) {
|
|
454
|
+
let out = "";
|
|
455
|
+
let quote = null;
|
|
456
|
+
let i = 0;
|
|
457
|
+
while (i < cmd.length) {
|
|
458
|
+
const ch = cmd[i];
|
|
459
|
+
if (quote) {
|
|
460
|
+
out += ch;
|
|
461
|
+
if (ch === quote && cmd[i - 1] !== "\\")
|
|
462
|
+
quote = null;
|
|
463
|
+
i++;
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
if (ch === '"' || ch === "'") {
|
|
467
|
+
quote = ch;
|
|
468
|
+
out += ch;
|
|
469
|
+
i++;
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
const rest = cmd.slice(i);
|
|
473
|
+
const hit = flags.find((f) => rest.startsWith(f) && /^(\s|$)/.test(rest.slice(f.length)));
|
|
474
|
+
if (hit) {
|
|
475
|
+
out = out.replace(/[ \t]+$/, "");
|
|
476
|
+
i += hit.length;
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
out += ch;
|
|
480
|
+
i++;
|
|
481
|
+
}
|
|
482
|
+
return out;
|
|
483
|
+
}
|
|
484
|
+
function rewriteNoVerify(cmd) {
|
|
485
|
+
if (!isSingleCommand(cmd) || !/^\s*git\s/.test(cmd))
|
|
486
|
+
return null;
|
|
487
|
+
if (!/--no-(verify|gpg-sign)\b/.test(cmd))
|
|
488
|
+
return null;
|
|
489
|
+
const out = stripFlagsOutsideQuotes(cmd, ["--no-verify", "--no-gpg-sign"]);
|
|
490
|
+
return out === cmd ? null : out;
|
|
491
|
+
}
|
|
492
|
+
function rewriteDryRun(cmd) {
|
|
493
|
+
if (!isSingleCommand(cmd))
|
|
494
|
+
return null;
|
|
495
|
+
const key = cmd.replace(/\s+/g, " ").trim();
|
|
496
|
+
if (/^\s*terraform\s+apply\b/.test(cmd)) {
|
|
497
|
+
const tail = cmd.replace(/^\s*terraform\s+apply\b/, "").trim();
|
|
498
|
+
if (tail.length > 0 && tail.split(/\s+/).some((a) => !a.startsWith("-")))
|
|
499
|
+
return null;
|
|
500
|
+
return {
|
|
501
|
+
key,
|
|
502
|
+
command: cmd.replace(/^(\s*)terraform\s+apply\b/, "$1terraform plan").replace(/\s+-auto-approve\b/g, "").replace(/ {2,}/g, " ").trimEnd()
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
if (/^\s*kubectl\s+delete\b/.test(cmd) && !/--dry-run\b/.test(cmd))
|
|
506
|
+
return { key, command: `${cmd.trimEnd()} --dry-run=client` };
|
|
507
|
+
if (/^\s*helm\s+(uninstall|delete)\b/.test(cmd) && !/--dry-run\b/.test(cmd))
|
|
508
|
+
return { key, command: `${cmd.trimEnd()} --dry-run` };
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
function compiled(rule) {
|
|
512
|
+
const hit = customRe.get(rule);
|
|
513
|
+
if (hit !== undefined)
|
|
514
|
+
return hit;
|
|
515
|
+
let re = null;
|
|
516
|
+
try {
|
|
517
|
+
re = new RegExp(rule.match, rule.action === "rewrite" ? "g" : "");
|
|
518
|
+
} catch {
|
|
519
|
+
re = null;
|
|
520
|
+
}
|
|
521
|
+
customRe.set(rule, re);
|
|
522
|
+
return re;
|
|
523
|
+
}
|
|
524
|
+
function applyCustomRule(rule, cmd) {
|
|
525
|
+
const re = compiled(rule);
|
|
526
|
+
if (!re)
|
|
527
|
+
return null;
|
|
528
|
+
if (rule.action === "off" || !re.test(cmd))
|
|
529
|
+
return null;
|
|
530
|
+
const id = `custom:${rule.name}`;
|
|
531
|
+
const reason = rule.reason ?? `matches custom rule "${rule.name}" (${rule.match})`;
|
|
532
|
+
if (rule.action !== "rewrite")
|
|
533
|
+
return { action: rule.action, rule: id, reason };
|
|
534
|
+
if (!isSingleCommand(cmd))
|
|
535
|
+
return {
|
|
536
|
+
action: "ask",
|
|
537
|
+
rule: id,
|
|
538
|
+
reason: `${reason} \u2014 not rewritten: the command chains or redirects`
|
|
539
|
+
};
|
|
540
|
+
re.lastIndex = 0;
|
|
541
|
+
const out = cmd.replace(re, rule.replace ?? "").trim();
|
|
542
|
+
return out === cmd ? null : { action: "rewrite", rule: id, reason, command: out };
|
|
543
|
+
}
|
|
544
|
+
function guardBash(cmd, current, sessions, now, modes = DEFAULT_MODES, ctx = {}) {
|
|
243
545
|
const other = () => otherLiveInSameTree(current, sessions, now);
|
|
244
546
|
const hit = (rule, reason) => {
|
|
245
547
|
const mode = modes[rule];
|
|
246
548
|
return mode === "off" ? { action: "allow" } : { action: mode, rule, reason };
|
|
247
549
|
};
|
|
550
|
+
const fix = (rule, reason, command, key) => {
|
|
551
|
+
const mode = modes[rule] ?? "off";
|
|
552
|
+
if (mode === "off")
|
|
553
|
+
return { action: "allow" };
|
|
554
|
+
if (mode === "rewrite")
|
|
555
|
+
return { action: "rewrite", rule, reason, command, ...key ? { key } : {} };
|
|
556
|
+
return { action: mode, rule, reason };
|
|
557
|
+
};
|
|
248
558
|
if (modes.protected_ports !== "off" && modes.protected.ports.length) {
|
|
249
559
|
const target = killedPorts(cmd).filter((p) => modes.protected.ports.includes(p));
|
|
250
560
|
if (target.length) {
|
|
@@ -261,7 +571,7 @@ function guardBash(cmd, current, sessions, now, modes = DEFAULT_MODES) {
|
|
|
261
571
|
if (modes.shared_tree !== "off" && isBroadStage(cmd)) {
|
|
262
572
|
const o = other();
|
|
263
573
|
if (o) {
|
|
264
|
-
const d = hit("shared_tree",
|
|
574
|
+
const d = hit("shared_tree", sharedTreeReason(o.id));
|
|
265
575
|
if (d.action !== "allow")
|
|
266
576
|
return d;
|
|
267
577
|
}
|
|
@@ -274,6 +584,28 @@ function guardBash(cmd, current, sessions, now, modes = DEFAULT_MODES) {
|
|
|
274
584
|
return d;
|
|
275
585
|
}
|
|
276
586
|
}
|
|
587
|
+
if (FAMILY_RULES.some((r) => (modes[r] ?? "off") !== "off")) {
|
|
588
|
+
const d = familyDecision(familyHitsBash(cmd, ctx.home ?? process.env.HOME ?? "", current.toplevel), modes);
|
|
589
|
+
if (d)
|
|
590
|
+
return d;
|
|
591
|
+
}
|
|
592
|
+
for (const rule of modes.custom ?? []) {
|
|
593
|
+
const d = applyCustomRule(rule, cmd);
|
|
594
|
+
if (d)
|
|
595
|
+
return d;
|
|
596
|
+
}
|
|
597
|
+
const nv = rewriteNoVerify(cmd);
|
|
598
|
+
if (nv) {
|
|
599
|
+
const d = fix("no_verify", NO_VERIFY_REASON, nv);
|
|
600
|
+
if (d.action !== "allow")
|
|
601
|
+
return d;
|
|
602
|
+
}
|
|
603
|
+
const dr = rewriteDryRun(cmd);
|
|
604
|
+
if (dr && !ctx.rewritesDone?.has(dr.key)) {
|
|
605
|
+
const d = fix("dry_run_first", `The first \`${dr.key}\` in a session runs as a dry run so you can read what it would change; run the real one next.`, dr.command, dr.key);
|
|
606
|
+
if (d.action !== "allow")
|
|
607
|
+
return d;
|
|
608
|
+
}
|
|
277
609
|
return { action: "allow" };
|
|
278
610
|
}
|
|
279
611
|
function norm(p) {
|
|
@@ -302,7 +634,6 @@ function absolutePath(path, cwd) {
|
|
|
302
634
|
return path;
|
|
303
635
|
return `${cwd.replace(/\/+$/, "")}/${path}`;
|
|
304
636
|
}
|
|
305
|
-
var WRITE_TOOLS = new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
|
|
306
637
|
function guardWrite(target, current, claims, modes = DEFAULT_MODES, kind = "file") {
|
|
307
638
|
const hit = (rule, reason) => {
|
|
308
639
|
const mode = modes[rule];
|
|
@@ -328,22 +659,47 @@ function guardWrite(target, current, claims, modes = DEFAULT_MODES, kind = "file
|
|
|
328
659
|
}
|
|
329
660
|
return { action: "allow" };
|
|
330
661
|
}
|
|
331
|
-
|
|
332
|
-
var
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
662
|
+
var LIVE_WINDOW_MS, GIT_GLOBAL_OPTS = "(?:\\s+(?:-C\\s+\\S+|-c\\s+\\S+|--git-dir(?:=|\\s+)\\S+|--work-tree(?:=|\\s+)\\S+|--namespace(?:=|\\s+)\\S+|--no-pager|--paginate|--bare|--literal-pathspecs|--exec-path(?:=\\S+)?))*", DEFAULT_MODES, FAMILY_RULES, FAMILY_REASON, customRe, sharedTreeReason = (sessionId) => `Another session (${sessionId.slice(0, 8)}) is active in this same checkout. \`git add -A\` / \`git commit -a\` will sweep its uncommitted changes into your commit. Stage explicit paths (\`git add <path>\`), or give each session its own git worktree.`, NO_VERIFY_REASON = "Hooks and signing exist for a reason: `--no-verify` / `--no-gpg-sign` was dropped. If a hook is wrong, fix the hook.", WRITE_TOOLS;
|
|
663
|
+
var init_rules = __esm(() => {
|
|
664
|
+
init_guards();
|
|
665
|
+
LIVE_WINDOW_MS = 10 * 60000;
|
|
666
|
+
DEFAULT_MODES = {
|
|
667
|
+
shared_tree: "ask",
|
|
668
|
+
destructive_git: "ask",
|
|
669
|
+
pattern_kill: "ask",
|
|
670
|
+
protected_ports: "ask",
|
|
671
|
+
no_foreign_worktree: "ask",
|
|
672
|
+
claim_required_to_write: "off",
|
|
673
|
+
no_verify: "off",
|
|
674
|
+
dry_run_first: "off",
|
|
675
|
+
custom: [],
|
|
676
|
+
destructive_fs: "off",
|
|
677
|
+
destructive_infra: "off",
|
|
678
|
+
pipe_to_shell: "off",
|
|
679
|
+
secrets: "off",
|
|
680
|
+
config_tamper: "off",
|
|
681
|
+
protected: { ports: [] }
|
|
682
|
+
};
|
|
683
|
+
FAMILY_RULES = [
|
|
684
|
+
"destructive_fs",
|
|
685
|
+
"destructive_infra",
|
|
686
|
+
"pipe_to_shell",
|
|
687
|
+
"secrets",
|
|
688
|
+
"config_tamper"
|
|
689
|
+
];
|
|
690
|
+
FAMILY_REASON = {
|
|
691
|
+
destructive_fs: (w) => `This ${w}. Nothing gets that back \u2014 name the exact paths you mean, inside this repository.`,
|
|
692
|
+
destructive_infra: (w) => `This is ${w} \u2014 outside this checkout, and not something git can undo. Confirm with the owner first.`,
|
|
693
|
+
pipe_to_shell: (w) => `This ${w}. Download it to a file, read it, then run it.`,
|
|
694
|
+
secrets: (w) => `This ${w}. Credentials stay out of the transcript: use the variable by name, or ask the owner.`,
|
|
695
|
+
config_tamper: (w) => `This ${w} \u2014 the settings that decide what agents may do. Ask the owner to change them.`
|
|
696
|
+
};
|
|
697
|
+
customRe = new WeakMap;
|
|
698
|
+
WRITE_TOOLS = new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
|
|
699
|
+
});
|
|
700
|
+
|
|
342
701
|
// packages/core/src/policy.ts
|
|
343
702
|
import { createHash } from "crypto";
|
|
344
|
-
var POLICY_CACHE_VERSION = 1;
|
|
345
|
-
var POLICY_CACHE_FILE = "policy.cache.json";
|
|
346
|
-
var digest = (body) => createHash("sha256").update(JSON.stringify(body)).digest("hex");
|
|
347
703
|
function verifyPolicyCache(raw) {
|
|
348
704
|
if (!raw || typeof raw !== "object")
|
|
349
705
|
return null;
|
|
@@ -362,10 +718,15 @@ function evaluateOffline(cache, raw, toplevel, now = Date.now()) {
|
|
|
362
718
|
const cwd = typeof raw.cwd === "string" ? raw.cwd : "";
|
|
363
719
|
const isWrite = WRITE_TOOLS.has(tool) && typeof input.file_path === "string";
|
|
364
720
|
const cmd = tool === "Bash" && typeof input.command === "string" ? input.command : null;
|
|
721
|
+
const modes = cache.modes;
|
|
722
|
+
if (typeof input.file_path === "string" && (isWrite || tool === "Read")) {
|
|
723
|
+
const f = guardFile(tool, absolutePath(input.file_path, cwd), modes);
|
|
724
|
+
if (f.action !== "allow")
|
|
725
|
+
return f;
|
|
726
|
+
}
|
|
365
727
|
if (!isWrite && !cmd)
|
|
366
728
|
return { action: "allow" };
|
|
367
729
|
const current = { id, cwd, toplevel: toplevel(cwd) };
|
|
368
|
-
const modes = cache.modes;
|
|
369
730
|
if (modes.no_foreign_worktree !== "off" || modes.claim_required_to_write !== "off") {
|
|
370
731
|
const target = isWrite ? absolutePath(input.file_path, cwd) : cwd;
|
|
371
732
|
const w = guardWrite(target, current, cache.worktrees, modes, isWrite ? "file" : "bash");
|
|
@@ -376,10 +737,617 @@ function evaluateOffline(cache, raw, toplevel, now = Date.now()) {
|
|
|
376
737
|
return { action: "allow" };
|
|
377
738
|
return guardBash(cmd, current, cache.sessions, now, modes);
|
|
378
739
|
}
|
|
740
|
+
var POLICY_CACHE_VERSION = 1, POLICY_CACHE_FILE = "policy.cache.json", digest = (body) => createHash("sha256").update(JSON.stringify(body)).digest("hex");
|
|
741
|
+
var init_policy = __esm(() => {
|
|
742
|
+
init_hooks();
|
|
743
|
+
init_rules();
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
// packages/core/src/agenthooks.ts
|
|
747
|
+
function detectAgent(raw) {
|
|
748
|
+
if (typeof raw.cursor_version === "string" || typeof raw.conversation_id === "string")
|
|
749
|
+
return "cursor";
|
|
750
|
+
if (raw.hook_event_name === "BeforeTool")
|
|
751
|
+
return "gemini";
|
|
752
|
+
if (typeof raw.turn_id === "string")
|
|
753
|
+
return "codex";
|
|
754
|
+
return "claude-code";
|
|
755
|
+
}
|
|
756
|
+
function patchPaths(patch) {
|
|
757
|
+
const out = [];
|
|
758
|
+
for (const m of patch.matchAll(/^\*\*\* (?:Add File|Update File|Delete File|Move to): (.+)$/gm)) {
|
|
759
|
+
const p = m[1]?.trim();
|
|
760
|
+
if (p && !out.includes(p))
|
|
761
|
+
out.push(p);
|
|
762
|
+
}
|
|
763
|
+
return out;
|
|
764
|
+
}
|
|
765
|
+
function toToolRequests(agent, raw) {
|
|
766
|
+
const input = raw.tool_input && typeof raw.tool_input === "object" ? raw.tool_input : {};
|
|
767
|
+
const tool = str(raw.tool_name) ?? "";
|
|
768
|
+
const cwd = str(raw.cwd) ?? "";
|
|
769
|
+
const sessionId = str(raw.session_id) ?? str(raw.conversation_id) ?? "";
|
|
770
|
+
const req = (t, i, at = cwd) => ({
|
|
771
|
+
tool: t,
|
|
772
|
+
input: i,
|
|
773
|
+
sessionId,
|
|
774
|
+
cwd: at
|
|
775
|
+
});
|
|
776
|
+
switch (agent) {
|
|
777
|
+
case "claude-code": {
|
|
778
|
+
const command = str(input.command);
|
|
779
|
+
const file_path = str(input.file_path);
|
|
780
|
+
return [req(tool, { ...command ? { command } : {}, ...file_path ? { file_path } : {} })];
|
|
781
|
+
}
|
|
782
|
+
case "codex": {
|
|
783
|
+
const command = str(input.command);
|
|
784
|
+
if (tool === "Bash" && command)
|
|
785
|
+
return [req("Bash", { command })];
|
|
786
|
+
if (tool === "apply_patch" && command)
|
|
787
|
+
return patchPaths(command).map((p) => req("Write", { file_path: p }));
|
|
788
|
+
return [];
|
|
789
|
+
}
|
|
790
|
+
case "gemini": {
|
|
791
|
+
const shell = str(input.command);
|
|
792
|
+
if (tool === "run_shell_command" && shell)
|
|
793
|
+
return [req("Bash", { command: shell }, str(input.dir_path) ?? cwd)];
|
|
794
|
+
const fp = str(input.file_path);
|
|
795
|
+
if (!fp)
|
|
796
|
+
return [];
|
|
797
|
+
if (tool === "read_file")
|
|
798
|
+
return [req("Read", { file_path: fp })];
|
|
799
|
+
if (tool === "write_file" || tool === "replace")
|
|
800
|
+
return [req("Write", { file_path: fp })];
|
|
801
|
+
return [];
|
|
802
|
+
}
|
|
803
|
+
case "cursor": {
|
|
804
|
+
const shell = str(input.command);
|
|
805
|
+
if (tool === "Shell" && shell)
|
|
806
|
+
return [req("Bash", { command: shell }, str(input.working_directory) ?? cwd)];
|
|
807
|
+
const fp = str(input.file_path) ?? str(input.path) ?? str(input.target_file);
|
|
808
|
+
if (!fp)
|
|
809
|
+
return [];
|
|
810
|
+
if (tool === "Read")
|
|
811
|
+
return [req("Read", { file_path: fp })];
|
|
812
|
+
if (tool === "Write" || tool === "Delete")
|
|
813
|
+
return [req("Write", { file_path: fp })];
|
|
814
|
+
return [];
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
function effectiveDecision(agent, d) {
|
|
819
|
+
if (d.action === "ask" && !CAN_ASK.has(agent))
|
|
820
|
+
return {
|
|
821
|
+
...d,
|
|
822
|
+
action: "deny",
|
|
823
|
+
reason: `${d.reason ?? ""} (Swarm would ask the user here; ${AGENT_LABEL[agent]}'s hooks cannot ask, so the call is refused \u2014 ask the user to run it, or to turn the rule off.)`.trim()
|
|
824
|
+
};
|
|
825
|
+
if (d.action === "rewrite" && agent !== "claude-code")
|
|
826
|
+
return {
|
|
827
|
+
...d,
|
|
828
|
+
action: "deny",
|
|
829
|
+
reason: `${d.reason ?? ""} Run this instead: ${d.command ?? ""}`.trim()
|
|
830
|
+
};
|
|
831
|
+
return d;
|
|
832
|
+
}
|
|
833
|
+
function renderAgentDecision(agent, d) {
|
|
834
|
+
const e = effectiveDecision(agent, d);
|
|
835
|
+
if (e.action === "allow")
|
|
836
|
+
return "{}";
|
|
837
|
+
const reason = `[swarm] ${e.reason ?? ""}`.trim();
|
|
838
|
+
if (agent === "gemini")
|
|
839
|
+
return JSON.stringify({ decision: "deny", reason });
|
|
840
|
+
return JSON.stringify({
|
|
841
|
+
hookSpecificOutput: {
|
|
842
|
+
hookEventName: "PreToolUse",
|
|
843
|
+
permissionDecision: e.action === "ask" ? "ask" : "deny",
|
|
844
|
+
permissionDecisionReason: reason
|
|
845
|
+
}
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
var AGENT_LABEL, CAN_ASK, str = (v) => typeof v === "string" && v ? v : undefined;
|
|
849
|
+
var init_agenthooks = __esm(() => {
|
|
850
|
+
init_policy();
|
|
851
|
+
AGENT_LABEL = {
|
|
852
|
+
"claude-code": "Claude Code",
|
|
853
|
+
codex: "Codex",
|
|
854
|
+
gemini: "Gemini CLI",
|
|
855
|
+
cursor: "Cursor"
|
|
856
|
+
};
|
|
857
|
+
CAN_ASK = new Set(["claude-code"]);
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
// packages/core/src/art.ts
|
|
861
|
+
var ROBOT, HEAD;
|
|
862
|
+
var init_art = __esm(() => {
|
|
863
|
+
ROBOT = [
|
|
864
|
+
" MM MM ",
|
|
865
|
+
" LL LL ",
|
|
866
|
+
" LL LL ",
|
|
867
|
+
" LM MM ",
|
|
868
|
+
" MD MS ",
|
|
869
|
+
" LE LE ",
|
|
870
|
+
" ME ME ",
|
|
871
|
+
" MS MS ",
|
|
872
|
+
" KK KK ",
|
|
873
|
+
" SSDDDSDDDDDDDDDDDDDDDDDDDDDDDDDDDDS ",
|
|
874
|
+
" SSDSDDDDDDDDDDDDDDDDDDDDDDDDDDDDDSS ",
|
|
875
|
+
" SSDOOOOOOOOOOOOOOOOOOOOOOOOOOOOOKSS ",
|
|
876
|
+
" SSOMMMMMMMMMMMMMMMMMMMMMMMMMMMMMOSS ",
|
|
877
|
+
" DDKMMLLLMLLMMMMMMMMMMMMMMMMMMMMMODD ",
|
|
878
|
+
" DDOMMLMMMMMMMMMMMMMMMMMMMMMMMMMMODD ",
|
|
879
|
+
" SSSSODDOMMLMMMMMMMMMMMMMMMMMMMMMMMMMMODDOSSSS ",
|
|
880
|
+
" SSSSODDOMMMMMMMMMMMMMMMMMMMMMMMMMMMMMODDOSSSS ",
|
|
881
|
+
" SMMMMODDOMMLMMMMMMMMMMMMMMMMMMMMMMMMMMODDOSMMMS ",
|
|
882
|
+
" DMSSSODDOMMMMOOOOOOMMMMMMMMMOOOOOOMMMMODDOSSSED ",
|
|
883
|
+
" DSSSSODDOMMMMODDDDDMMMMMMMMMODDDDDMMMMODDOSSSSK ",
|
|
884
|
+
" DSMMSODDOMMMMDDDDDDMMMMMMMMMKDDDDDMMMMODDOSMMSK ",
|
|
885
|
+
" DSSSSODDOMMMMDDDDDDMMMMMMMMMDDDDDDMMMMODDOSSSSK ",
|
|
886
|
+
" DSSSSODDOMMMMDDDDDDMMMMMMMMMDDDDDDMMMMODDODSSSK ",
|
|
887
|
+
" DSSSSODDOMMMMDDDDDDMMMMMMMMMDDDDDDMMMMODDODSSSK ",
|
|
888
|
+
" DSSSKODDOMMMMDDDDDDMMMMMMMMMDDDDDDMMMMODDODSSSD ",
|
|
889
|
+
" OKKKKODDOMMMMMMMMMMMMMMMMMMMMMMMMMMMMMODDOKKKKO ",
|
|
890
|
+
" KKKKODDOMMMMMMMMMMMMMMMMMMMMMMMMMMMMMODDOOKKK ",
|
|
891
|
+
" KKKOODDOMMMMMMMMMMMMMMMMMMMMMMMMMMMMMODDOOKKK ",
|
|
892
|
+
" DDOMMMMDDDDDDDDDDDDDDDDDDDDDMMMMODD ",
|
|
893
|
+
" DDOMMMMDDDDDDDDDDDDDDDDDDDDDMMMMODD ",
|
|
894
|
+
" DDOMMMMDDDDDDDDDDDDDDDDDDDDDMMMMODD ",
|
|
895
|
+
" DDOMMMMMMMMMMMMMMMMMMMMMMMMMMMMMODD ",
|
|
896
|
+
" DDOMMMMMMMMMMMMMMMMMMMMMMMMMMMMMODD ",
|
|
897
|
+
" DDOMMMMMMMMMMMMMMMMMMMMMMMMMMMMMODD ",
|
|
898
|
+
" DDDOOOOOOOOOOOOOOOOOOOOOOOOOOOOOKDD ",
|
|
899
|
+
" DDDDDDDDDDDDDDDDDDDDDDDDDDDDDKDDDDD ",
|
|
900
|
+
" DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD ",
|
|
901
|
+
" OOOOOOOOOOO ",
|
|
902
|
+
" OODDDDDKKOO ",
|
|
903
|
+
" DSMMMMMMSDD ",
|
|
904
|
+
" OOOOOOOOOOO ",
|
|
905
|
+
" KDSMMMMMESSKK ",
|
|
906
|
+
" MMMMMMSSKOOKDSMMMMMSSSDKOODSEMMMMMM ",
|
|
907
|
+
" SSSSDO MMMMMMSSKKOOOOOOOOOOOOOOOOOKSSSMMMMMM ODSSSS ",
|
|
908
|
+
" EEESSSSOOLLLLMMMMMESSSSSSSSSSSSSSSSSSSSMMMMMLLLLOOSSSSMME ",
|
|
909
|
+
" SMMSSSSKOEELLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLEEOOSSSSEMS ",
|
|
910
|
+
" DSSSSSSSOOSEEKSMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMSOEEDOOSSSSSSSK ",
|
|
911
|
+
" DSSSSSSDOOSEOSKEMMMMMMMMMMMMMMMMMMMMMMMMMMMMMEOSDEDOODSSSSSSK ",
|
|
912
|
+
" KSSSSSSKOODSSOSEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEESOEEDOOKSSSSSSK ",
|
|
913
|
+
" DKSSSSKKOODSMEEMLLLLLLLLLLLLLLLLLLLLLLLLLLLLLMSEMSDOOKKSSSSDK ",
|
|
914
|
+
" OKDDKDKKOOKSMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMSKOOKKKKKKKO ",
|
|
915
|
+
" OOOOOKKKOOKSMMMMESSSSSSSSSSSSSMMMMEEEEEESSSSEMMMMSKOOKKKOOOOO ",
|
|
916
|
+
" OOOOOOKKOOKSMMMSSKKKKKKKKKKKKKKMMMDKOOOOOOOODDMMMSKOOKKOOOOOO ",
|
|
917
|
+
" OKSSKOOOOOKSMMMKKMLLLLLLLLLLLMKMMMDDSSSSSSSSDDMMMSKOOOOODSSKO ",
|
|
918
|
+
" SDOKKK KSMMMKDLLLLLLLLLLLLLKMMMKDKKKDDKKKDKMMMSK KKKKDS ",
|
|
919
|
+
" DMLLMKO KSMMMKDLLLLLLLLLLLLLKMMMDDDDDDDDDSKDMMMSK ODMLLMD ",
|
|
920
|
+
" DELLMSD KSMMMKKMLLLLLLLLLLLLKMMMDKKKKKKKKKDDMMMSK DSMLLEO ",
|
|
921
|
+
" KSKKKKDO KSMMMEDDDDKKDDDKKKDKSMMMDDDDDDDDDSDDMMMSK ODDKKKSK ",
|
|
922
|
+
" EMLLMDO KSMMMMLLLLLLLLLLLLLLLMMMDOOOOOOOOODDMMMSK ODMLLME ",
|
|
923
|
+
" KMLLMSD KSMMMMMMMMMMMMMMMMMMMMMMDDSSDDSSSSDDMMMSK SSMLLEK ",
|
|
924
|
+
" KSKKKKSO KSMMMMMOOMMOOMKOEMOOMMMMDOOOOOOOOODDMMMSK OSKKKKSO ",
|
|
925
|
+
" SMLLMDO KSMMMMMKKMMKKMDKEMKKMMMMKDDDDSSSSDDDMMMSK ODMLLMS ",
|
|
926
|
+
" SMMMMSD KSMMMMMEEMMEEMEEMMEEMMMMDSOOOOOOOODDMMSSK DSMMMMD ",
|
|
927
|
+
" OOOOOOO KSSMMMMMMMMMMMMMMMMMMMMMDDDDDDDDDDDDMSSSK OOOOOOO ",
|
|
928
|
+
" KSMLLLMMSKO KSSSSMMMMMMMMMMMMMMMMMMMSOOOOOOOOOOSSSSSK OKSMLLLLMSO ",
|
|
929
|
+
" DSMMMEEESKK KSSKSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSOSSK KDSMMMMMESD ",
|
|
930
|
+
" DSMMMMMESKK KSOOKSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSOOKSK KKSMMMMMMSD ",
|
|
931
|
+
" DSMMMMMESKK OKSKSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSDSKO KDSMMMMMMSD ",
|
|
932
|
+
" KDDDDDDDDKK KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK KKDDDDDDDDO ",
|
|
933
|
+
" OOOOOOOOOOO OOOOOOOOO OOOOOOOOO OOOOOOOOOOO ",
|
|
934
|
+
"DSDO OSSO ODKKKKKKK OKKKKKKKO KEDO ODEK",
|
|
935
|
+
"SMDO OSMD KDSSSSSSK KDSSSSSKO SMDO ODMS",
|
|
936
|
+
"DEKO OOED OOKKKOOO OOOKKOOO SSOO OKMS",
|
|
937
|
+
"DEKO OOED SEMMMMSK OSMMMMED SSKO OKES",
|
|
938
|
+
"DSSO OESK SEMMMMSK OSMMMMED KSSO OSSK",
|
|
939
|
+
"KSMS EEDO OOKKKKKO OOKKKKKO OSEE EMSO",
|
|
940
|
+
" DSS SSK OSEEESDO ODSEEESO KSS SSD ",
|
|
941
|
+
" OOO OO SEMMMMSK OSMMMMED OO OOO ",
|
|
942
|
+
" KSSSSSDO OKSSESSK ",
|
|
943
|
+
" DKKDDDDDDKKDK KDKDDDDDDDKDK ",
|
|
944
|
+
" SSMLLMMMMMMLMSK DSSMMMMMMMLLMSS ",
|
|
945
|
+
" SMLMMMMMMMMLLED SEMMMMMMMMMMLMS ",
|
|
946
|
+
" DMMMMMMMMMMMMMMD SMMMMMMMMMMMMMSK ",
|
|
947
|
+
" ODSSSSSSSSSSSSSSDO ODSSSSSSSSSSSSSSKO ",
|
|
948
|
+
" OOOOOOOOOOOOOOOOO OOOOOOOOOOOOOOOOO ",
|
|
949
|
+
" OKSSSSSSSSSSSSSSK KSSSSSSSSSSSSSSKO ",
|
|
950
|
+
" OKDDDDDDDDDDDDDDK KDDDDDDDDDDDDDDOO "
|
|
951
|
+
];
|
|
952
|
+
HEAD = ROBOT.slice(0, 37);
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
// packages/core/src/audit.ts
|
|
956
|
+
var AUDIT_TYPES, AUDIT_TYPES_SQL;
|
|
957
|
+
var init_audit = __esm(() => {
|
|
958
|
+
AUDIT_TYPES = new Set([
|
|
959
|
+
"session.started",
|
|
960
|
+
"session.ended",
|
|
961
|
+
"tool.denied",
|
|
962
|
+
"claim.acquired",
|
|
963
|
+
"claim.renewed",
|
|
964
|
+
"claim.released",
|
|
965
|
+
"claim.expired",
|
|
966
|
+
"claim.orphaned",
|
|
967
|
+
"claim.denied",
|
|
968
|
+
"rules.changed",
|
|
969
|
+
"worktree.reclaimed",
|
|
970
|
+
"worktree.created",
|
|
971
|
+
"worktree.removed",
|
|
972
|
+
"worktree.bootstrapped",
|
|
973
|
+
"pr.opened",
|
|
974
|
+
"question.asked",
|
|
975
|
+
"question.answered",
|
|
976
|
+
"dispatch.queued",
|
|
977
|
+
"dispatch.started",
|
|
978
|
+
"dispatch.finished",
|
|
979
|
+
"resource.acquired",
|
|
980
|
+
"resource.released",
|
|
981
|
+
"resource.reaped",
|
|
982
|
+
"process.started",
|
|
983
|
+
"process.exited",
|
|
984
|
+
"gate.recorded",
|
|
985
|
+
"gate.blocked",
|
|
986
|
+
"codify.applied",
|
|
987
|
+
"handoff.recorded",
|
|
988
|
+
"permission.requested",
|
|
989
|
+
"permission.resolved",
|
|
990
|
+
"incident.opened",
|
|
991
|
+
"incident.acked",
|
|
992
|
+
"run.result",
|
|
993
|
+
"workflow.started",
|
|
994
|
+
"workflow.finished"
|
|
995
|
+
]);
|
|
996
|
+
AUDIT_TYPES_SQL = [...AUDIT_TYPES].map((t) => `'${t}'`).join(", ");
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
// packages/core/src/budget.ts
|
|
1000
|
+
var BUDGET_ASK_TOOLS;
|
|
1001
|
+
var init_budget = __esm(() => {
|
|
1002
|
+
BUDGET_ASK_TOOLS = new Set(["Bash", "Edit", "Write", "MultiEdit", "NotebookEdit"]);
|
|
1003
|
+
});
|
|
1004
|
+
// packages/core/src/workflows.ts
|
|
1005
|
+
var init_workflows = () => {};
|
|
1006
|
+
|
|
1007
|
+
// packages/core/src/config.ts
|
|
1008
|
+
var init_config = __esm(() => {
|
|
1009
|
+
init_audit();
|
|
1010
|
+
init_workflows();
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
// packages/core/src/context.ts
|
|
1014
|
+
var init_context = () => {};
|
|
1015
|
+
|
|
1016
|
+
// packages/core/src/dag.ts
|
|
1017
|
+
var init_dag = () => {};
|
|
1018
|
+
// packages/core/src/dryrun.ts
|
|
1019
|
+
var init_dryrun = __esm(() => {
|
|
1020
|
+
init_rules();
|
|
1021
|
+
});
|
|
1022
|
+
// packages/core/src/gatehealth.ts
|
|
1023
|
+
var init_gatehealth = () => {};
|
|
1024
|
+
|
|
1025
|
+
// packages/core/src/gates.ts
|
|
1026
|
+
var init_gates = () => {};
|
|
1027
|
+
|
|
1028
|
+
// packages/core/src/graphs.ts
|
|
1029
|
+
var init_graphs = __esm(() => {
|
|
1030
|
+
init_rules();
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
// packages/core/src/heat.ts
|
|
1034
|
+
var init_heat = __esm(() => {
|
|
1035
|
+
init_rules();
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
// packages/core/src/worktree.ts
|
|
1039
|
+
var init_worktree = __esm(() => {
|
|
1040
|
+
init_config();
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
// packages/core/src/hygiene.ts
|
|
1044
|
+
var HYGIENE_DEFAULTS;
|
|
1045
|
+
var init_hygiene = __esm(() => {
|
|
1046
|
+
init_worktree();
|
|
1047
|
+
HYGIENE_DEFAULTS = {
|
|
1048
|
+
staleDays: 2,
|
|
1049
|
+
abandonedDays: 30,
|
|
1050
|
+
hungryRssKb: 1024 * 1024,
|
|
1051
|
+
heavyKb: 2 * 1024 * 1024
|
|
1052
|
+
};
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
// packages/core/src/ledger.ts
|
|
1056
|
+
var EDIT_TOOLS;
|
|
1057
|
+
var init_ledger = __esm(() => {
|
|
1058
|
+
init_prompt();
|
|
1059
|
+
EDIT_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
|
|
1060
|
+
});
|
|
1061
|
+
// packages/core/src/lessons-apply.ts
|
|
1062
|
+
var init_lessons_apply = () => {};
|
|
1063
|
+
|
|
1064
|
+
// packages/core/src/lineage.ts
|
|
1065
|
+
var init_lineage = __esm(() => {
|
|
1066
|
+
init_dag();
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
// packages/core/src/mcphealth.ts
|
|
1070
|
+
var init_mcphealth = __esm(() => {
|
|
1071
|
+
init_gatehealth();
|
|
1072
|
+
});
|
|
1073
|
+
// packages/core/src/memory.ts
|
|
1074
|
+
var init_memory = () => {};
|
|
1075
|
+
// packages/core/src/otel.ts
|
|
1076
|
+
var init_otel = () => {};
|
|
1077
|
+
|
|
1078
|
+
// packages/core/src/outcomes.ts
|
|
1079
|
+
var DEFAULT_BRANCHES;
|
|
1080
|
+
var init_outcomes = __esm(() => {
|
|
1081
|
+
DEFAULT_BRANCHES = new Set(["main", "master", "develop", "trunk"]);
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
// packages/core/src/pricing.ts
|
|
1085
|
+
var init_pricing = () => {};
|
|
1086
|
+
// packages/core/src/provenance.ts
|
|
1087
|
+
var init_provenance = () => {};
|
|
1088
|
+
// packages/core/src/quota.ts
|
|
1089
|
+
var BURN_MIN_SPAN_MS, BURN_LOOKBACK_MS;
|
|
1090
|
+
var init_quota = __esm(() => {
|
|
1091
|
+
BURN_MIN_SPAN_MS = 10 * 60000;
|
|
1092
|
+
BURN_LOOKBACK_MS = 60 * 60000;
|
|
1093
|
+
});
|
|
1094
|
+
// packages/core/src/security.ts
|
|
1095
|
+
var init_security = __esm(() => {
|
|
1096
|
+
init_rules();
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
// packages/core/src/stall.ts
|
|
1100
|
+
var init_stall = () => {};
|
|
1101
|
+
|
|
1102
|
+
// packages/core/src/statusline.ts
|
|
1103
|
+
function parseStatuslinePayload(text) {
|
|
1104
|
+
try {
|
|
1105
|
+
const v = JSON.parse(text);
|
|
1106
|
+
return v && typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
1107
|
+
} catch {
|
|
1108
|
+
return null;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
function pct(color, label, value) {
|
|
1112
|
+
const s = `${label} ${Math.round(value)}%`;
|
|
1113
|
+
if (value >= 90)
|
|
1114
|
+
return red(color, s);
|
|
1115
|
+
if (value >= 70)
|
|
1116
|
+
return yellow(color, s);
|
|
1117
|
+
return s;
|
|
1118
|
+
}
|
|
1119
|
+
function money(usd) {
|
|
1120
|
+
return usd >= 10 ? `$${usd.toFixed(0)}` : `$${usd.toFixed(2)}`;
|
|
1121
|
+
}
|
|
1122
|
+
function renderStatusline(payload, state, opts = {}) {
|
|
1123
|
+
const color = opts.color ?? false;
|
|
1124
|
+
const local = [];
|
|
1125
|
+
const model = payload.model?.display_name ?? payload.model?.id;
|
|
1126
|
+
if (model)
|
|
1127
|
+
local.push(bold(color, model));
|
|
1128
|
+
const ctx = payload.context_window?.used_percentage;
|
|
1129
|
+
if (typeof ctx === "number")
|
|
1130
|
+
local.push(pct(color, "ctx", ctx));
|
|
1131
|
+
const cost = payload.cost?.total_cost_usd;
|
|
1132
|
+
if (typeof cost === "number")
|
|
1133
|
+
local.push(money(cost));
|
|
1134
|
+
const five = payload.rate_limits?.five_hour?.used_percentage;
|
|
1135
|
+
if (typeof five === "number")
|
|
1136
|
+
local.push(pct(color, "5h", five));
|
|
1137
|
+
const week = payload.rate_limits?.seven_day?.used_percentage;
|
|
1138
|
+
if (typeof week === "number")
|
|
1139
|
+
local.push(pct(color, "7d", week));
|
|
1140
|
+
const swarm = [];
|
|
1141
|
+
if (state) {
|
|
1142
|
+
if (state.task)
|
|
1143
|
+
swarm.push(`${state.task.id} ${dim(color, `${state.task.leftMin}m`)}`);
|
|
1144
|
+
if (state.budget && state.budget.level !== "ok")
|
|
1145
|
+
swarm.push((state.budget.level === "exceeded" ? red : yellow)(color, `budget ${state.budget.level} ${Math.round(state.budget.pct * 100)}%`));
|
|
1146
|
+
if (state.incidents > 0)
|
|
1147
|
+
swarm.push(yellow(color, `${state.incidents} incident${state.incidents === 1 ? "" : "s"}`));
|
|
1148
|
+
if (state.waitingOn > 0)
|
|
1149
|
+
swarm.push(red(color, `waiting on you${state.waitingOn > 1 ? ` \xD7${state.waitingOn}` : ""}`));
|
|
1150
|
+
if (state.inbox > 0)
|
|
1151
|
+
swarm.push(`inbox ${state.inbox}`);
|
|
1152
|
+
if (state.quota) {
|
|
1153
|
+
const short = { five_hour: "5h", seven_day: "7d", spend_limit: "spend" }[state.quota.window];
|
|
1154
|
+
const h = state.quota.hoursToLimit;
|
|
1155
|
+
if (h == null)
|
|
1156
|
+
swarm.push(red(color, `${short} limit reached`));
|
|
1157
|
+
else {
|
|
1158
|
+
const s = `${short} limit in ${h < 1 ? `${Math.max(1, Math.round(h * 60))}m` : `${Math.round(h)}h`}`;
|
|
1159
|
+
swarm.push(h < 1 ? red(color, s) : h < 3 ? yellow(color, s) : s);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
const sep = dim(color, " \xB7 ");
|
|
1164
|
+
const bar = dim(color, " \u2502 ");
|
|
1165
|
+
const left = local.join(sep);
|
|
1166
|
+
if (!swarm.length)
|
|
1167
|
+
return left;
|
|
1168
|
+
const right = swarm.join(sep);
|
|
1169
|
+
return left ? `${left}${bar}${right}` : right;
|
|
1170
|
+
}
|
|
1171
|
+
var ESC = "\x1B[", paint = (color, code, s) => color ? `${ESC}${code}m${s}${ESC}0m` : s, dim = (c, s) => paint(c, "2", s), yellow = (c, s) => paint(c, "33", s), red = (c, s) => paint(c, "31", s), bold = (c, s) => paint(c, "1", s);
|
|
1172
|
+
|
|
1173
|
+
// packages/core/src/tasks.ts
|
|
1174
|
+
var init_tasks = () => {};
|
|
1175
|
+
|
|
1176
|
+
// packages/core/src/team.ts
|
|
1177
|
+
var init_team = () => {};
|
|
1178
|
+
|
|
1179
|
+
// packages/core/src/teamsetup.ts
|
|
1180
|
+
var init_teamsetup = () => {};
|
|
1181
|
+
|
|
1182
|
+
// packages/core/src/transitions.ts
|
|
1183
|
+
var init_transitions = () => {};
|
|
1184
|
+
|
|
1185
|
+
// packages/core/src/types.ts
|
|
1186
|
+
var init_types = () => {};
|
|
1187
|
+
|
|
1188
|
+
// packages/core/src/waiting.ts
|
|
1189
|
+
var init_waiting = () => {};
|
|
1190
|
+
|
|
1191
|
+
// packages/core/src/index.ts
|
|
1192
|
+
var init_src2 = __esm(() => {
|
|
1193
|
+
init_actor();
|
|
1194
|
+
init_adapters();
|
|
1195
|
+
init_hooks();
|
|
1196
|
+
init_agenthooks();
|
|
1197
|
+
init_art();
|
|
1198
|
+
init_audit();
|
|
1199
|
+
init_budget();
|
|
1200
|
+
init_config();
|
|
1201
|
+
init_context();
|
|
1202
|
+
init_dag();
|
|
1203
|
+
init_dryrun();
|
|
1204
|
+
init_gatehealth();
|
|
1205
|
+
init_gates();
|
|
1206
|
+
init_graphs();
|
|
1207
|
+
init_heat();
|
|
1208
|
+
init_hygiene();
|
|
1209
|
+
init_ledger();
|
|
1210
|
+
init_lessons_apply();
|
|
1211
|
+
init_lineage();
|
|
1212
|
+
init_mcphealth();
|
|
1213
|
+
init_memory();
|
|
1214
|
+
init_otel();
|
|
1215
|
+
init_outcomes();
|
|
1216
|
+
init_policy();
|
|
1217
|
+
init_pricing();
|
|
1218
|
+
init_prompt();
|
|
1219
|
+
init_provenance();
|
|
1220
|
+
init_quota();
|
|
1221
|
+
init_rules();
|
|
1222
|
+
init_security();
|
|
1223
|
+
init_stall();
|
|
1224
|
+
init_tasks();
|
|
1225
|
+
init_team();
|
|
1226
|
+
init_teamsetup();
|
|
1227
|
+
init_transitions();
|
|
1228
|
+
init_types();
|
|
1229
|
+
init_waiting();
|
|
1230
|
+
init_workflows();
|
|
1231
|
+
init_worktree();
|
|
1232
|
+
});
|
|
1233
|
+
|
|
1234
|
+
// packages/hook/src/wait.ts
|
|
1235
|
+
async function waitForWake(sessionId, opts = {}) {
|
|
1236
|
+
const maxMs = opts.maxMs ?? 55 * 60000;
|
|
1237
|
+
const pollMs = opts.pollMs ?? 11 * 60000;
|
|
1238
|
+
const started = Date.now();
|
|
1239
|
+
let base = opts.baseUrl ?? resolveBaseUrl();
|
|
1240
|
+
const fallback = `http://127.0.0.1:${DEFAULT_PORT}`;
|
|
1241
|
+
while (Date.now() - started < maxMs) {
|
|
1242
|
+
let body = null;
|
|
1243
|
+
try {
|
|
1244
|
+
const r = await authedFetch(`${base}/v1/wake`, {
|
|
1245
|
+
method: "POST",
|
|
1246
|
+
headers: { "content-type": "application/json" },
|
|
1247
|
+
body: JSON.stringify({ session_id: sessionId }),
|
|
1248
|
+
signal: AbortSignal.timeout(pollMs)
|
|
1249
|
+
});
|
|
1250
|
+
if (r.ok)
|
|
1251
|
+
body = await r.json();
|
|
1252
|
+
} catch {
|
|
1253
|
+
if (base !== fallback && !opts.baseUrl) {
|
|
1254
|
+
base = fallback;
|
|
1255
|
+
continue;
|
|
1256
|
+
}
|
|
1257
|
+
return { exit: 0, reason: "daemon unreachable" };
|
|
1258
|
+
}
|
|
1259
|
+
if (!body)
|
|
1260
|
+
return { exit: 0, reason: "daemon refused" };
|
|
1261
|
+
if (body.wake && body.text)
|
|
1262
|
+
return { exit: 2, text: body.text };
|
|
1263
|
+
if (body.reason && body.reason !== "timeout")
|
|
1264
|
+
return { exit: 0, reason: body.reason };
|
|
1265
|
+
}
|
|
1266
|
+
return { exit: 0, reason: "gave up" };
|
|
1267
|
+
}
|
|
1268
|
+
var init_wait = __esm(() => {
|
|
1269
|
+
init_src();
|
|
1270
|
+
});
|
|
1271
|
+
|
|
1272
|
+
// packages/hook/src/statusline.ts
|
|
1273
|
+
async function fetchStatuslineState(payload, opts = {}) {
|
|
1274
|
+
const timeout = opts.timeoutMs ?? Number(process.env.SWARM_HOOK_TIMEOUT_MS ?? 400);
|
|
1275
|
+
const post = async (base) => {
|
|
1276
|
+
const r = await authedFetch(`${base}/v1/statusline`, {
|
|
1277
|
+
method: "POST",
|
|
1278
|
+
headers: { "content-type": "application/json" },
|
|
1279
|
+
body: JSON.stringify(payload),
|
|
1280
|
+
signal: AbortSignal.timeout(timeout)
|
|
1281
|
+
});
|
|
1282
|
+
return r.ok ? await r.json() : null;
|
|
1283
|
+
};
|
|
1284
|
+
const base = opts.baseUrl ?? resolveBaseUrl();
|
|
1285
|
+
const fallback = `http://127.0.0.1:${DEFAULT_PORT}`;
|
|
1286
|
+
try {
|
|
1287
|
+
const s = await post(base);
|
|
1288
|
+
if (s)
|
|
1289
|
+
return s;
|
|
1290
|
+
} catch {}
|
|
1291
|
+
if (base !== fallback && !opts.baseUrl) {
|
|
1292
|
+
try {
|
|
1293
|
+
return await post(fallback);
|
|
1294
|
+
} catch {}
|
|
1295
|
+
}
|
|
1296
|
+
return null;
|
|
1297
|
+
}
|
|
1298
|
+
async function runStatusline(input, opts = {}) {
|
|
1299
|
+
const payload = parseStatuslinePayload(input) ?? {};
|
|
1300
|
+
const state = await fetchStatuslineState(payload, opts);
|
|
1301
|
+
const color = opts.color ?? !process.env.NO_COLOR;
|
|
1302
|
+
return renderStatusline(payload, state, { color });
|
|
1303
|
+
}
|
|
1304
|
+
var init_statusline = __esm(() => {
|
|
1305
|
+
init_src();
|
|
1306
|
+
init_src2();
|
|
1307
|
+
});
|
|
1308
|
+
|
|
379
1309
|
// packages/hook/src/bin.ts
|
|
1310
|
+
init_src();
|
|
1311
|
+
init_src2();
|
|
1312
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
1313
|
+
import { dirname, join as join2 } from "path";
|
|
380
1314
|
var event = process.argv[2] ?? "Unknown";
|
|
381
1315
|
var input = await Bun.stdin.text();
|
|
382
|
-
|
|
1316
|
+
if (event === "wait") {
|
|
1317
|
+
await Promise.resolve().then(() => init_wait());
|
|
1318
|
+
let sid = "";
|
|
1319
|
+
try {
|
|
1320
|
+
sid = String(JSON.parse(input || "{}").session_id ?? "");
|
|
1321
|
+
} catch {}
|
|
1322
|
+
if (!sid)
|
|
1323
|
+
process.exit(0);
|
|
1324
|
+
const out = await waitForWake(sid);
|
|
1325
|
+
if (out.exit === 2)
|
|
1326
|
+
process.stderr.write(`${out.text}
|
|
1327
|
+
`);
|
|
1328
|
+
process.exit(out.exit);
|
|
1329
|
+
}
|
|
1330
|
+
if (event === "statusline") {
|
|
1331
|
+
await Promise.resolve().then(() => init_statusline());
|
|
1332
|
+
process.stdout.write(`${await runStatusline(input)}
|
|
1333
|
+
`);
|
|
1334
|
+
process.exit(0);
|
|
1335
|
+
}
|
|
1336
|
+
var fast = Number(process.env.SWARM_HOOK_TIMEOUT_MS ?? 400);
|
|
1337
|
+
async function repairArmed() {
|
|
1338
|
+
try {
|
|
1339
|
+
const cwd = String(JSON.parse(input || "{}").cwd ?? "");
|
|
1340
|
+
if (!cwd)
|
|
1341
|
+
return false;
|
|
1342
|
+
const r = await authedFetch(`${resolveBaseUrl()}/v1/repair?cwd=${encodeURIComponent(cwd)}`, {
|
|
1343
|
+
signal: AbortSignal.timeout(fast)
|
|
1344
|
+
});
|
|
1345
|
+
return r.ok && (await r.json()).block === true;
|
|
1346
|
+
} catch {
|
|
1347
|
+
return false;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
var timeout = event === "Stop" ? await repairArmed() ? Number(process.env.SWARM_STOP_TIMEOUT_MS ?? 330000) : fast : event === "PermissionRequest" ? Number(process.env.SWARM_PERMISSION_TIMEOUT_MS ?? 130000) : fast;
|
|
383
1351
|
var post = async (base) => {
|
|
384
1352
|
const r = await authedFetch(`${base}/v1/hook/${event}`, {
|
|
385
1353
|
method: "POST",
|
|
@@ -400,7 +1368,7 @@ if (out === null && base !== fallback) {
|
|
|
400
1368
|
out = await post(fallback);
|
|
401
1369
|
} catch {}
|
|
402
1370
|
}
|
|
403
|
-
if (out === null && event === "PreToolUse")
|
|
1371
|
+
if (out === null && (event === "PreToolUse" || event === "BeforeTool"))
|
|
404
1372
|
out = offline();
|
|
405
1373
|
process.stdout.write(`${out ?? "{}"}
|
|
406
1374
|
`);
|
|
@@ -413,6 +1381,18 @@ function offline() {
|
|
|
413
1381
|
if (!cache)
|
|
414
1382
|
return null;
|
|
415
1383
|
const raw = input ? JSON.parse(input) : {};
|
|
1384
|
+
const agent = detectAgent(raw);
|
|
1385
|
+
if (agent !== "claude-code") {
|
|
1386
|
+
for (const r of toToolRequests(agent, raw)) {
|
|
1387
|
+
const d = evaluateOffline(cache, { tool_name: r.tool, tool_input: r.input, session_id: r.sessionId, cwd: r.cwd }, gitToplevel);
|
|
1388
|
+
if (d.action !== "allow")
|
|
1389
|
+
return renderAgentDecision(agent, {
|
|
1390
|
+
...d,
|
|
1391
|
+
reason: `(daemon unreachable, policy-locked rule) ${d.reason}`
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
return null;
|
|
1395
|
+
}
|
|
416
1396
|
const d = evaluateOffline(cache, raw, gitToplevel);
|
|
417
1397
|
if (d.action === "allow")
|
|
418
1398
|
return null;
|