@ra3orblade/swarm 0.7.0 → 0.8.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 +63 -10
- package/dist/swarm-hook.js +281 -1
- package/dist/swarm-mcp.js +18 -2
- package/dist/swarm.js +430 -48
- package/dist/swarmd.js +956 -68
- package/package.json +1 -1
- package/web/app.js +388 -127
- package/web/index.html +63 -3
- package/web/menus.js +1 -1
- package/web/release-notes.js +1 -1
package/dist/swarmd.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
4
|
// packages/client/src/daemon.ts
|
|
5
|
+
import { randomBytes } from "crypto";
|
|
5
6
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
6
7
|
import { homedir } from "os";
|
|
7
8
|
import { join } from "path";
|
|
@@ -18,9 +19,65 @@ function writeDaemonInfo(info) {
|
|
|
18
19
|
`);
|
|
19
20
|
return full;
|
|
20
21
|
}
|
|
22
|
+
var tokenFile = (home = swarmHome()) => join(home, "token");
|
|
23
|
+
function readToken(home = swarmHome()) {
|
|
24
|
+
try {
|
|
25
|
+
const t = readFileSync(tokenFile(home), "utf8").trim();
|
|
26
|
+
return /^[a-f0-9]{64}$/.test(t) ? t : null;
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function ensureToken(home = swarmHome()) {
|
|
32
|
+
const cur = readToken(home);
|
|
33
|
+
if (cur)
|
|
34
|
+
return cur;
|
|
35
|
+
mkdirSync(home, { recursive: true });
|
|
36
|
+
const t = randomBytes(32).toString("hex");
|
|
37
|
+
writeFileSync(tokenFile(home), `${t}
|
|
38
|
+
`, { mode: 384 });
|
|
39
|
+
return t;
|
|
40
|
+
}
|
|
21
41
|
function clearDaemonInfo() {
|
|
22
42
|
rmSync(infoFile(), { force: true });
|
|
23
43
|
}
|
|
44
|
+
// packages/core/src/actor.ts
|
|
45
|
+
var HUMAN_ALIASES = new Set(["cli", "dashboard", "me", "desktop", "human"]);
|
|
46
|
+
var DAEMON_ALIASES = new Set(["daemon", "system", "swarm"]);
|
|
47
|
+
function actorFrom(owner, sessionId, opts = {}) {
|
|
48
|
+
const o = (owner ?? "").trim();
|
|
49
|
+
const sid = sessionId?.trim() || undefined;
|
|
50
|
+
if (opts.runId)
|
|
51
|
+
return { kind: "run", id: opts.runId, session: sid };
|
|
52
|
+
if (o.startsWith("auto:"))
|
|
53
|
+
return { kind: "daemon", id: "daemon", session: sid };
|
|
54
|
+
if (DAEMON_ALIASES.has(o))
|
|
55
|
+
return { kind: "daemon", id: "daemon" };
|
|
56
|
+
if (o === "agent" || o.startsWith("agent:") || o.startsWith("session:"))
|
|
57
|
+
return {
|
|
58
|
+
kind: "agent",
|
|
59
|
+
id: sid ?? o.replace(/^(agent|session):/, "") ?? "agent",
|
|
60
|
+
session: sid
|
|
61
|
+
};
|
|
62
|
+
if (!o || HUMAN_ALIASES.has(o)) {
|
|
63
|
+
if (!o && sid)
|
|
64
|
+
return { kind: "agent", id: sid, session: sid };
|
|
65
|
+
return { kind: "human", id: opts.user?.trim() || "me" };
|
|
66
|
+
}
|
|
67
|
+
if (sid && o === sid)
|
|
68
|
+
return { kind: "agent", id: sid, session: sid };
|
|
69
|
+
return { kind: "human", id: o };
|
|
70
|
+
}
|
|
71
|
+
function actorFromColumns(kind, id, session) {
|
|
72
|
+
if (!kind || !id)
|
|
73
|
+
return null;
|
|
74
|
+
if (!["human", "agent", "run", "daemon"].includes(kind))
|
|
75
|
+
return null;
|
|
76
|
+
const a = { kind, id };
|
|
77
|
+
if (session && kind !== "human" && kind !== "daemon")
|
|
78
|
+
a.session = session;
|
|
79
|
+
return a;
|
|
80
|
+
}
|
|
24
81
|
// packages/core/src/adapters/claude-code/transcript.ts
|
|
25
82
|
function parseTranscriptChunk(chunk) {
|
|
26
83
|
const out = {
|
|
@@ -229,6 +286,18 @@ function parseGrokUpdates(chunk) {
|
|
|
229
286
|
return out;
|
|
230
287
|
}
|
|
231
288
|
// packages/core/src/adapters/claude-code/hooks.ts
|
|
289
|
+
var HOOK_EVENTS = [
|
|
290
|
+
"SessionStart",
|
|
291
|
+
"UserPromptSubmit",
|
|
292
|
+
"PreToolUse",
|
|
293
|
+
"PostToolUse",
|
|
294
|
+
"SubagentStart",
|
|
295
|
+
"SubagentStop",
|
|
296
|
+
"Stop",
|
|
297
|
+
"SessionEnd",
|
|
298
|
+
"Notification",
|
|
299
|
+
"PreCompact"
|
|
300
|
+
];
|
|
232
301
|
var MAP = {
|
|
233
302
|
SessionStart: "session.started",
|
|
234
303
|
UserPromptSubmit: "prompt.submitted",
|
|
@@ -322,6 +391,155 @@ function normalizeHook(event, raw, projectId, ts = new Date().toISOString()) {
|
|
|
322
391
|
payload.prompt = raw.prompt;
|
|
323
392
|
return { ts, type, projectId, sessionId: raw.session_id ?? null, payload, raw };
|
|
324
393
|
}
|
|
394
|
+
// packages/core/src/audit.ts
|
|
395
|
+
var AUDIT_TYPES = new Set([
|
|
396
|
+
"session.started",
|
|
397
|
+
"session.ended",
|
|
398
|
+
"tool.denied",
|
|
399
|
+
"claim.acquired",
|
|
400
|
+
"claim.renewed",
|
|
401
|
+
"claim.released",
|
|
402
|
+
"claim.expired",
|
|
403
|
+
"claim.orphaned",
|
|
404
|
+
"worktree.created",
|
|
405
|
+
"worktree.removed",
|
|
406
|
+
"worktree.bootstrapped",
|
|
407
|
+
"pr.opened",
|
|
408
|
+
"question.asked",
|
|
409
|
+
"question.answered",
|
|
410
|
+
"dispatch.queued",
|
|
411
|
+
"dispatch.started",
|
|
412
|
+
"dispatch.finished",
|
|
413
|
+
"resource.acquired",
|
|
414
|
+
"resource.released",
|
|
415
|
+
"resource.reaped",
|
|
416
|
+
"process.started",
|
|
417
|
+
"process.exited",
|
|
418
|
+
"gate.recorded",
|
|
419
|
+
"handoff.recorded",
|
|
420
|
+
"permission.requested",
|
|
421
|
+
"permission.resolved",
|
|
422
|
+
"incident.opened",
|
|
423
|
+
"incident.acked",
|
|
424
|
+
"run.result"
|
|
425
|
+
]);
|
|
426
|
+
var isAuditType = (t) => AUDIT_TYPES.has(t);
|
|
427
|
+
var AUDIT_TYPES_SQL = [...AUDIT_TYPES].map((t) => `'${t}'`).join(", ");
|
|
428
|
+
var DEFAULT_PRIVACY = {
|
|
429
|
+
store_prompts: true,
|
|
430
|
+
store_reasoning: true,
|
|
431
|
+
redact: []
|
|
432
|
+
};
|
|
433
|
+
var BUILTIN_REDACT = [
|
|
434
|
+
/\b(sk|pk|rk|ghp|gho|ghu|ghs|xoxb|xoxp|AKIA)[A-Za-z0-9_-]{16,}\b/g,
|
|
435
|
+
/\bBearer\s+[A-Za-z0-9._-]{20,}/g
|
|
436
|
+
];
|
|
437
|
+
function compileRedactions(patterns) {
|
|
438
|
+
const out = [...BUILTIN_REDACT];
|
|
439
|
+
for (const p of patterns) {
|
|
440
|
+
try {
|
|
441
|
+
out.push(new RegExp(p, "g"));
|
|
442
|
+
} catch {}
|
|
443
|
+
}
|
|
444
|
+
return out;
|
|
445
|
+
}
|
|
446
|
+
function redactValue(v, res) {
|
|
447
|
+
if (!res.length)
|
|
448
|
+
return v;
|
|
449
|
+
if (typeof v === "string")
|
|
450
|
+
return redactString(v, res);
|
|
451
|
+
if (Array.isArray(v)) {
|
|
452
|
+
let changed = false;
|
|
453
|
+
const out = v.map((x) => {
|
|
454
|
+
const r = redactValue(x, res);
|
|
455
|
+
if (r !== x)
|
|
456
|
+
changed = true;
|
|
457
|
+
return r;
|
|
458
|
+
});
|
|
459
|
+
return changed ? out : v;
|
|
460
|
+
}
|
|
461
|
+
if (v && typeof v === "object") {
|
|
462
|
+
let changed = false;
|
|
463
|
+
const out = {};
|
|
464
|
+
for (const [k, x] of Object.entries(v)) {
|
|
465
|
+
const r = redactValue(x, res);
|
|
466
|
+
if (r !== x)
|
|
467
|
+
changed = true;
|
|
468
|
+
out[k] = r;
|
|
469
|
+
}
|
|
470
|
+
return changed ? out : v;
|
|
471
|
+
}
|
|
472
|
+
return v;
|
|
473
|
+
}
|
|
474
|
+
function redactString(s, res) {
|
|
475
|
+
let out = s;
|
|
476
|
+
for (const re of res) {
|
|
477
|
+
re.lastIndex = 0;
|
|
478
|
+
if (re.test(out)) {
|
|
479
|
+
re.lastIndex = 0;
|
|
480
|
+
out = out.replace(re, "[redacted]");
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return out;
|
|
484
|
+
}
|
|
485
|
+
var AUDIT_COLUMNS = [
|
|
486
|
+
"seq",
|
|
487
|
+
"ts",
|
|
488
|
+
"type",
|
|
489
|
+
"projectId",
|
|
490
|
+
"sessionId",
|
|
491
|
+
"actorKind",
|
|
492
|
+
"actorId",
|
|
493
|
+
"summary",
|
|
494
|
+
"payload"
|
|
495
|
+
];
|
|
496
|
+
function auditRow(e) {
|
|
497
|
+
const p = e.payload ?? {};
|
|
498
|
+
const summary = typeof p.summary === "string" ? p.summary : typeof p.reason === "string" ? p.reason : typeof p.command === "string" ? p.command : typeof p.task === "string" ? String(p.task) : "";
|
|
499
|
+
return {
|
|
500
|
+
seq: e.seq ?? 0,
|
|
501
|
+
ts: e.ts,
|
|
502
|
+
type: e.type,
|
|
503
|
+
projectId: e.projectId,
|
|
504
|
+
sessionId: e.sessionId,
|
|
505
|
+
actorKind: e.actor?.kind ?? null,
|
|
506
|
+
actorId: e.actor?.id ?? null,
|
|
507
|
+
summary: summary.slice(0, 400),
|
|
508
|
+
payload: e.payload ?? null
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
var csvCell = (v) => {
|
|
512
|
+
const s = v == null ? "" : typeof v === "string" ? v : JSON.stringify(v);
|
|
513
|
+
return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
|
|
514
|
+
};
|
|
515
|
+
function formatAudit(rows, format) {
|
|
516
|
+
if (format === "json")
|
|
517
|
+
return JSON.stringify(rows);
|
|
518
|
+
if (format === "csv") {
|
|
519
|
+
const lines = [
|
|
520
|
+
AUDIT_COLUMNS.join(","),
|
|
521
|
+
...rows.map((r) => AUDIT_COLUMNS.map((c) => csvCell(r[c])).join(","))
|
|
522
|
+
];
|
|
523
|
+
return `${lines.join(`
|
|
524
|
+
`)}
|
|
525
|
+
`;
|
|
526
|
+
}
|
|
527
|
+
return `${rows.map((r) => JSON.stringify(r)).join(`
|
|
528
|
+
`)}${rows.length ? `
|
|
529
|
+
` : ""}`;
|
|
530
|
+
}
|
|
531
|
+
function sinceToIso(since, now = Date.now()) {
|
|
532
|
+
if (!since)
|
|
533
|
+
return null;
|
|
534
|
+
const m = /^(\d+)([dhm])$/.exec(since.trim());
|
|
535
|
+
if (m) {
|
|
536
|
+
const n = Number(m[1]);
|
|
537
|
+
const ms = m[2] === "d" ? 86400000 : m[2] === "h" ? 3600000 : 60000;
|
|
538
|
+
return new Date(now - n * ms).toISOString();
|
|
539
|
+
}
|
|
540
|
+
const t = Date.parse(since);
|
|
541
|
+
return Number.isNaN(t) ? null : new Date(t).toISOString();
|
|
542
|
+
}
|
|
325
543
|
// packages/core/src/budget.ts
|
|
326
544
|
function budgetStatus(spent, cfg) {
|
|
327
545
|
const part = (s, l) => ({
|
|
@@ -385,24 +603,33 @@ function parseGateDefs(gates) {
|
|
|
385
603
|
if (!isRecord(gates))
|
|
386
604
|
return out;
|
|
387
605
|
for (const [name, v] of Object.entries(gates)) {
|
|
388
|
-
if (!isRecord(v)
|
|
606
|
+
if (!isRecord(v))
|
|
607
|
+
continue;
|
|
608
|
+
const builtin = v.builtin === "review" ? "review" : null;
|
|
609
|
+
const cmd = typeof v.cmd === "string" ? v.cmd.trim() : "";
|
|
610
|
+
if (!cmd && !builtin)
|
|
389
611
|
continue;
|
|
390
612
|
if (!/^[a-z0-9][a-z0-9_.-]{0,39}$/i.test(name))
|
|
391
613
|
continue;
|
|
392
614
|
const t = Number(v.timeout);
|
|
393
615
|
out[name] = {
|
|
394
|
-
cmd:
|
|
395
|
-
timeout: Number.isFinite(t) && t > 0 ? Math.min(t, 86400) : DEFAULT_GATE_TIMEOUT_S,
|
|
396
|
-
cwd: isRepoRelative(v.cwd) ? v.cwd.trim() : null
|
|
616
|
+
cmd: builtin ? "" : cmd,
|
|
617
|
+
timeout: Number.isFinite(t) && t > 0 ? Math.min(t, 86400) : builtin ? 600 : DEFAULT_GATE_TIMEOUT_S,
|
|
618
|
+
cwd: isRepoRelative(v.cwd) ? v.cwd.trim() : null,
|
|
619
|
+
builtin,
|
|
620
|
+
model: typeof v.model === "string" && v.model.trim() ? v.model.trim() : null
|
|
397
621
|
};
|
|
398
622
|
}
|
|
399
623
|
return out;
|
|
400
624
|
}
|
|
401
625
|
var DEFAULT_CONFIG = {
|
|
402
|
-
daemon: { port: 7777 },
|
|
626
|
+
daemon: { port: 7777, auth: "loopback-optional" },
|
|
403
627
|
tasks: { source: null, labels: [], team: null },
|
|
404
628
|
gates: { required: [], auto: "session-end", defs: {} },
|
|
405
629
|
budget: { daily: null, weekly: null, warn_at: 0.8, on_exceed: "warn" },
|
|
630
|
+
events: { retain_days: 30 },
|
|
631
|
+
audit: { retain_days: 0 },
|
|
632
|
+
privacy: DEFAULT_PRIVACY,
|
|
406
633
|
dispatch: {
|
|
407
634
|
max_parallel: 2,
|
|
408
635
|
permission_mode: null,
|
|
@@ -450,6 +677,10 @@ function isRepoRelative(f) {
|
|
|
450
677
|
return false;
|
|
451
678
|
return !t.split(/[/\\]/).some((seg) => seg === "..");
|
|
452
679
|
}
|
|
680
|
+
var days = (v, fallback) => {
|
|
681
|
+
const n = Number(v);
|
|
682
|
+
return Number.isInteger(n) && n >= 0 ? Math.min(n, 3650) : fallback;
|
|
683
|
+
};
|
|
453
684
|
function validate(c) {
|
|
454
685
|
const mode = (v, fallback) => MODES.includes(v) ? v : fallback;
|
|
455
686
|
const port = Number(c.daemon?.port);
|
|
@@ -470,7 +701,10 @@ function validate(c) {
|
|
|
470
701
|
const auto = rawGates?.auto;
|
|
471
702
|
return {
|
|
472
703
|
...c,
|
|
473
|
-
daemon: {
|
|
704
|
+
daemon: {
|
|
705
|
+
port: Number.isInteger(port) && port > 0 && port < 65536 ? port : 7777,
|
|
706
|
+
auth: c.daemon?.auth === "required" ? "required" : "loopback-optional"
|
|
707
|
+
},
|
|
474
708
|
tasks: {
|
|
475
709
|
source: typeof source === "string" && source.trim() && !source.startsWith("/") ? source.trim() : null,
|
|
476
710
|
labels: Array.isArray(c.tasks?.labels) ? c.tasks.labels.filter((l) => typeof l === "string" && l.trim() !== "") : [],
|
|
@@ -487,6 +721,17 @@ function validate(c) {
|
|
|
487
721
|
warn_at: Number.isFinite(warnAt) && warnAt > 0 && warnAt < 1 ? warnAt : 0.8,
|
|
488
722
|
on_exceed: b.on_exceed === "ask" || b.on_exceed === "stop" ? b.on_exceed : "warn"
|
|
489
723
|
},
|
|
724
|
+
events: {
|
|
725
|
+
retain_days: days(c.events?.retain_days, 30)
|
|
726
|
+
},
|
|
727
|
+
audit: {
|
|
728
|
+
retain_days: days(c.audit?.retain_days, 0)
|
|
729
|
+
},
|
|
730
|
+
privacy: {
|
|
731
|
+
store_prompts: c.privacy?.store_prompts !== false,
|
|
732
|
+
store_reasoning: c.privacy?.store_reasoning !== false,
|
|
733
|
+
redact: Array.isArray(c.privacy?.redact) ? c.privacy.redact.filter((r) => typeof r === "string" && r.length > 0) : []
|
|
734
|
+
},
|
|
490
735
|
dispatch: {
|
|
491
736
|
max_parallel: Number.isInteger(mp) && mp > 0 ? Math.min(mp, 16) : 2,
|
|
492
737
|
permission_mode: str(d.permission_mode),
|
|
@@ -514,18 +759,84 @@ function validate(c) {
|
|
|
514
759
|
}
|
|
515
760
|
};
|
|
516
761
|
}
|
|
517
|
-
function
|
|
762
|
+
function leafPaths(v, prefix = "") {
|
|
763
|
+
if (!isRecord(v))
|
|
764
|
+
return prefix ? [prefix] : [];
|
|
765
|
+
const keys = Object.keys(v);
|
|
766
|
+
if (keys.length === 0)
|
|
767
|
+
return prefix ? [prefix] : [];
|
|
768
|
+
return keys.flatMap((k) => leafPaths(v[k], prefix ? `${prefix}.${k}` : k));
|
|
769
|
+
}
|
|
770
|
+
function getPath(v, path) {
|
|
771
|
+
let cur = v;
|
|
772
|
+
for (const seg of path.split(".")) {
|
|
773
|
+
if (!isRecord(cur))
|
|
774
|
+
return;
|
|
775
|
+
cur = cur[seg];
|
|
776
|
+
}
|
|
777
|
+
return cur;
|
|
778
|
+
}
|
|
779
|
+
function setPath(obj, path, value) {
|
|
780
|
+
const segs = path.split(".");
|
|
781
|
+
let cur = obj;
|
|
782
|
+
for (const seg of segs.slice(0, -1)) {
|
|
783
|
+
if (!isRecord(cur[seg]))
|
|
784
|
+
cur[seg] = {};
|
|
785
|
+
cur = cur[seg];
|
|
786
|
+
}
|
|
787
|
+
cur[segs[segs.length - 1]] = value;
|
|
788
|
+
}
|
|
789
|
+
var isLockedBy = (path, lock) => path === lock || path.startsWith(`${lock}.`);
|
|
790
|
+
function readLayer(path) {
|
|
791
|
+
return existsSync2(path) ? parseToml(readFileSync2(path, "utf8"), path) : null;
|
|
792
|
+
}
|
|
793
|
+
function loadConfigDetailed(opts = {}) {
|
|
518
794
|
const home = opts.home ?? process.env.SWARM_HOME ?? join2(process.env.HOME ?? "", ".swarm");
|
|
795
|
+
const policyPath = opts.policy ?? process.env.SWARM_POLICY ?? join2(home, "policy.toml");
|
|
796
|
+
const policyRaw = readLayer(policyPath);
|
|
797
|
+
const locked = Array.isArray(policyRaw?.locked) ? policyRaw.locked.filter((k) => typeof k === "string" && /^[a-z0-9_.-]+$/i.test(k)) : [];
|
|
798
|
+
const policy = { ...policyRaw ?? {} };
|
|
799
|
+
delete policy.locked;
|
|
800
|
+
const layers = [
|
|
801
|
+
["policy", policyRaw ? policy : null],
|
|
802
|
+
["global", readLayer(join2(home, "config.toml"))],
|
|
803
|
+
["repo", opts.repoRoot ? readLayer(join2(opts.repoRoot, ".swarm.toml")) : null]
|
|
804
|
+
];
|
|
805
|
+
const provenance = {};
|
|
806
|
+
for (const p of leafPaths(DEFAULT_CONFIG))
|
|
807
|
+
provenance[p] = "default";
|
|
808
|
+
const overridden = [];
|
|
519
809
|
let cfg = DEFAULT_CONFIG;
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
810
|
+
for (const [layer, raw] of layers) {
|
|
811
|
+
if (!raw)
|
|
812
|
+
continue;
|
|
813
|
+
for (const p of leafPaths(raw)) {
|
|
814
|
+
const lock = layer !== "policy" && locked.find((l) => isLockedBy(p, l));
|
|
815
|
+
if (lock)
|
|
816
|
+
overridden.push({ key: p, layer, attempted: getPath(raw, p) });
|
|
817
|
+
else
|
|
818
|
+
provenance[p] = layer;
|
|
819
|
+
}
|
|
820
|
+
cfg = merge(cfg, raw);
|
|
821
|
+
}
|
|
822
|
+
if (overridden.length) {
|
|
823
|
+
const out = structuredClone(cfg);
|
|
824
|
+
for (const { key } of overridden) {
|
|
825
|
+
const fromPolicy = getPath(policy, key);
|
|
826
|
+
setPath(out, key, fromPolicy === undefined ? getPath(DEFAULT_CONFIG, key) : fromPolicy);
|
|
827
|
+
provenance[key] = fromPolicy === undefined ? "default" : "policy";
|
|
828
|
+
}
|
|
829
|
+
cfg = out;
|
|
527
830
|
}
|
|
528
|
-
return
|
|
831
|
+
return {
|
|
832
|
+
config: validate(cfg),
|
|
833
|
+
provenance,
|
|
834
|
+
overridden,
|
|
835
|
+
policy: { path: policyRaw ? policyPath : null, locked }
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
function loadConfig(opts = {}) {
|
|
839
|
+
return loadConfigDetailed(opts).config;
|
|
529
840
|
}
|
|
530
841
|
// packages/core/src/dispatch.ts
|
|
531
842
|
function planDispatch(tasks, wanted, opts) {
|
|
@@ -1328,6 +1639,92 @@ function parseMemoryQuery(q) {
|
|
|
1328
1639
|
}
|
|
1329
1640
|
return { match: terms.join(" "), kind, task };
|
|
1330
1641
|
}
|
|
1642
|
+
// packages/core/src/policy.ts
|
|
1643
|
+
import { createHash } from "crypto";
|
|
1644
|
+
var HOOK_MARK = "swarm-hook";
|
|
1645
|
+
var hookIsOurs = (h) => typeof h.command === "string" && (h.command.includes(HOOK_MARK) || h.command.includes("/packages/hook/src/bin.ts"));
|
|
1646
|
+
var MIN_HOOK_TIMEOUT_S = 5;
|
|
1647
|
+
function hookCoverage(settings) {
|
|
1648
|
+
const hooks = settings && typeof settings === "object" && !Array.isArray(settings) ? settings.hooks : undefined;
|
|
1649
|
+
const missing = [];
|
|
1650
|
+
const short = [];
|
|
1651
|
+
for (const ev of HOOK_EVENTS) {
|
|
1652
|
+
const groups = Array.isArray(hooks?.[ev]) ? hooks?.[ev] : [];
|
|
1653
|
+
const ours = groups.flatMap((g) => {
|
|
1654
|
+
const list = g?.hooks;
|
|
1655
|
+
return Array.isArray(list) ? list.filter((h) => hookIsOurs(h)) : [];
|
|
1656
|
+
});
|
|
1657
|
+
if (!ours.length)
|
|
1658
|
+
missing.push(ev);
|
|
1659
|
+
else if (ours.every((h) => typeof h.timeout === "number" && h.timeout < MIN_HOOK_TIMEOUT_S))
|
|
1660
|
+
short.push(ev);
|
|
1661
|
+
}
|
|
1662
|
+
return { missing, short, complete: !missing.length && !short.length };
|
|
1663
|
+
}
|
|
1664
|
+
var hasLockedRules = (loaded) => loaded.policy.locked.some((k) => k === "rules" || k.startsWith("rules."));
|
|
1665
|
+
function policyFindings(input) {
|
|
1666
|
+
const out = [];
|
|
1667
|
+
const repo = input.repoRoot ?? "";
|
|
1668
|
+
for (const o of input.loaded.overridden)
|
|
1669
|
+
out.push({
|
|
1670
|
+
key: `override:${repo}:${o.layer}:${o.key}`,
|
|
1671
|
+
subject: `${o.layer === "repo" ? ".swarm.toml" : "config.toml"} ${o.key}`,
|
|
1672
|
+
reason: `locked by policy; ${o.layer} config tried to set ${JSON.stringify(o.attempted)}`
|
|
1673
|
+
});
|
|
1674
|
+
const cov = input.coverage;
|
|
1675
|
+
if (cov && !cov.complete) {
|
|
1676
|
+
if (cov.missing.length)
|
|
1677
|
+
out.push({
|
|
1678
|
+
key: `hooks:missing:${cov.missing.join(",")}`,
|
|
1679
|
+
subject: `hooks ${cov.missing.join(", ")}`,
|
|
1680
|
+
reason: "swarm hook entry removed from settings.json \u2014 run: swarm install"
|
|
1681
|
+
});
|
|
1682
|
+
if (cov.short.length)
|
|
1683
|
+
out.push({
|
|
1684
|
+
key: `hooks:short:${cov.short.join(",")}`,
|
|
1685
|
+
subject: `hooks ${cov.short.join(", ")}`,
|
|
1686
|
+
reason: `hook timeout below ${MIN_HOOK_TIMEOUT_S}s \u2014 run: swarm install`
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
if (input.guardOff && hasLockedRules(input.loaded))
|
|
1690
|
+
out.push({
|
|
1691
|
+
key: "guard:off",
|
|
1692
|
+
subject: "SWARM_GUARD=off",
|
|
1693
|
+
reason: "policy locks rules; SWARM_GUARD=off is ignored for locked rules"
|
|
1694
|
+
});
|
|
1695
|
+
return out;
|
|
1696
|
+
}
|
|
1697
|
+
var POLICY_CACHE_VERSION = 1;
|
|
1698
|
+
var POLICY_CACHE_FILE = "policy.cache.json";
|
|
1699
|
+
var RULE_KEYS = [
|
|
1700
|
+
"shared_tree",
|
|
1701
|
+
"destructive_git",
|
|
1702
|
+
"pattern_kill",
|
|
1703
|
+
"protected_ports",
|
|
1704
|
+
"no_foreign_worktree",
|
|
1705
|
+
"claim_required_to_write"
|
|
1706
|
+
];
|
|
1707
|
+
var lockedKey = (locked, key) => locked.some((l) => l === "rules" || key === l || key.startsWith(`${l}.`));
|
|
1708
|
+
function offlineModes(loaded) {
|
|
1709
|
+
const locked = loaded.policy.locked;
|
|
1710
|
+
const out = { ...DEFAULT_MODES, protected: { ports: [] } };
|
|
1711
|
+
for (const k of RULE_KEYS)
|
|
1712
|
+
out[k] = lockedKey(locked, `rules.${k}`) ? loaded.config.rules[k] : "off";
|
|
1713
|
+
if (lockedKey(locked, "rules.protected.ports"))
|
|
1714
|
+
out.protected = { ports: [...loaded.config.rules.protected.ports] };
|
|
1715
|
+
return out;
|
|
1716
|
+
}
|
|
1717
|
+
var digest = (body) => createHash("sha256").update(JSON.stringify(body)).digest("hex");
|
|
1718
|
+
function buildPolicyCache(loaded, sessions, worktrees, now = new Date) {
|
|
1719
|
+
const body = {
|
|
1720
|
+
version: POLICY_CACHE_VERSION,
|
|
1721
|
+
writtenAt: now.toISOString(),
|
|
1722
|
+
modes: offlineModes(loaded),
|
|
1723
|
+
sessions,
|
|
1724
|
+
worktrees
|
|
1725
|
+
};
|
|
1726
|
+
return { ...body, sha256: digest(body) };
|
|
1727
|
+
}
|
|
1331
1728
|
// packages/core/src/pricing.ts
|
|
1332
1729
|
var PRICES = {
|
|
1333
1730
|
"claude-opus-4": { input: 15, output: 75, cacheWrite: 18.75, cacheWrite1h: 30, cacheRead: 1.5 },
|
|
@@ -1474,6 +1871,114 @@ function acquireRefusalMessage(holder) {
|
|
|
1474
1871
|
const via = isTrackedPid(holder.pid) ? `pid ${holder.pid}` : holder.expiresAt ? `lease until ${holder.expiresAt}` : "unbounded";
|
|
1475
1872
|
return `Resource "${holder.name}" is held by ${holder.owner} (${via}).` + ` Pick another name, coordinate with the holder, or wait for release/reap.`;
|
|
1476
1873
|
}
|
|
1874
|
+
// packages/core/src/review.ts
|
|
1875
|
+
var REVIEW_RUBRIC = "review: no blocker/major findings \u2014 correctness bugs, data loss, security, broken invariants (never kill by pattern, never touch a worktree you don't hold, repo-agnostic), missing tests for changed behaviour";
|
|
1876
|
+
var REVIEW_PATCH_MAX = 120000;
|
|
1877
|
+
function reviewPrompt(input) {
|
|
1878
|
+
const patch = input.patch.length > REVIEW_PATCH_MAX ? `${input.patch.slice(0, REVIEW_PATCH_MAX)}
|
|
1879
|
+
|
|
1880
|
+
[\u2026 patch truncated at ${REVIEW_PATCH_MAX} chars; read the files for the rest]` : input.patch;
|
|
1881
|
+
return [
|
|
1882
|
+
`You are the review gate for task ${input.task}${input.title ? ` \u2014 ${input.title}` : ""}${input.branch ? ` (branch ${input.branch})` : ""}.`,
|
|
1883
|
+
"You are read-only: you may Read, Grep and Glob files in this worktree to understand context. Do not edit anything.",
|
|
1884
|
+
"",
|
|
1885
|
+
"Judge the diff below against this rubric and nothing else:",
|
|
1886
|
+
`- ${REVIEW_RUBRIC}`,
|
|
1887
|
+
"- A finding is a concrete defect with a file and, when possible, a line \u2014 not style, not preference.",
|
|
1888
|
+
"- Severity: blocker (must not merge), major (should not merge), minor (worth fixing), nit.",
|
|
1889
|
+
'- verdict is "fail" if and only if there is at least one blocker or major finding.',
|
|
1890
|
+
"",
|
|
1891
|
+
"Respond with ONLY a JSON object, no prose, no code fence:",
|
|
1892
|
+
'{"verdict":"pass"|"fail","summary":"one sentence","findings":[{"file":"path","line":123,"severity":"blocker|major|minor|nit","summary":"what is wrong and why"}]}',
|
|
1893
|
+
"",
|
|
1894
|
+
"Files changed:",
|
|
1895
|
+
input.stat.trim() || "(no stat)",
|
|
1896
|
+
"",
|
|
1897
|
+
"Diff:",
|
|
1898
|
+
patch.trim() || "(empty diff)"
|
|
1899
|
+
].join(`
|
|
1900
|
+
`);
|
|
1901
|
+
}
|
|
1902
|
+
function reviewArgs(prompt, opts = {}) {
|
|
1903
|
+
const args = [
|
|
1904
|
+
"-p",
|
|
1905
|
+
prompt,
|
|
1906
|
+
"--output-format",
|
|
1907
|
+
"json",
|
|
1908
|
+
"--allowedTools",
|
|
1909
|
+
"Read",
|
|
1910
|
+
"Grep",
|
|
1911
|
+
"Glob",
|
|
1912
|
+
"LS",
|
|
1913
|
+
"--disallowedTools",
|
|
1914
|
+
"Edit",
|
|
1915
|
+
"Write",
|
|
1916
|
+
"MultiEdit",
|
|
1917
|
+
"NotebookEdit",
|
|
1918
|
+
"Bash",
|
|
1919
|
+
"WebFetch",
|
|
1920
|
+
"WebSearch",
|
|
1921
|
+
"--permission-mode",
|
|
1922
|
+
"dontAsk"
|
|
1923
|
+
];
|
|
1924
|
+
if (opts.model)
|
|
1925
|
+
args.push("--model", opts.model);
|
|
1926
|
+
return args;
|
|
1927
|
+
}
|
|
1928
|
+
function parseReviewVerdict(stdout) {
|
|
1929
|
+
let text = stdout.trim();
|
|
1930
|
+
try {
|
|
1931
|
+
const env = JSON.parse(text);
|
|
1932
|
+
if (env && typeof env === "object" && typeof env.result === "string")
|
|
1933
|
+
text = env.result.trim();
|
|
1934
|
+
} catch {}
|
|
1935
|
+
text = text.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "");
|
|
1936
|
+
const start = text.indexOf("{");
|
|
1937
|
+
const end = text.lastIndexOf("}");
|
|
1938
|
+
if (start < 0 || end <= start)
|
|
1939
|
+
return null;
|
|
1940
|
+
let obj;
|
|
1941
|
+
try {
|
|
1942
|
+
obj = JSON.parse(text.slice(start, end + 1));
|
|
1943
|
+
} catch {
|
|
1944
|
+
return null;
|
|
1945
|
+
}
|
|
1946
|
+
if (!obj || typeof obj !== "object")
|
|
1947
|
+
return null;
|
|
1948
|
+
const o = obj;
|
|
1949
|
+
const findings = Array.isArray(o.findings) ? o.findings.filter((f) => !!f && typeof f === "object").map((f) => ({
|
|
1950
|
+
file: String(f.file ?? "").slice(0, 300),
|
|
1951
|
+
line: Number.isInteger(f.line) ? f.line : null,
|
|
1952
|
+
severity: ["blocker", "major", "minor", "nit"].includes(String(f.severity)) ? String(f.severity) : "minor",
|
|
1953
|
+
summary: String(f.summary ?? "").slice(0, 600)
|
|
1954
|
+
})).filter((f) => f.summary) : [];
|
|
1955
|
+
const serious = findings.some((f) => f.severity === "blocker" || f.severity === "major");
|
|
1956
|
+
const verdict = serious ? "fail" : o.verdict === "fail" ? "fail" : "pass";
|
|
1957
|
+
return { verdict, summary: String(o.summary ?? "").slice(0, 400), findings };
|
|
1958
|
+
}
|
|
1959
|
+
function reviewGateInput(task, gate, outcome) {
|
|
1960
|
+
if (outcome.kind === "error")
|
|
1961
|
+
return {
|
|
1962
|
+
task,
|
|
1963
|
+
gate,
|
|
1964
|
+
verdict: "fail",
|
|
1965
|
+
rubric: REVIEW_RUBRIC,
|
|
1966
|
+
evidence: `reviewer did not answer: ${outcome.reason}${outcome.output ? `
|
|
1967
|
+
${outcome.output.slice(-1500)}` : ""}`
|
|
1968
|
+
};
|
|
1969
|
+
const v = outcome.verdict;
|
|
1970
|
+
const lines = v.findings.map((f) => `- [${f.severity}] ${f.file}${f.line ? `:${f.line}` : ""} \u2014 ${f.summary}`);
|
|
1971
|
+
const secs = (outcome.durationMs / 1000).toFixed(0);
|
|
1972
|
+
return {
|
|
1973
|
+
task,
|
|
1974
|
+
gate,
|
|
1975
|
+
verdict: v.verdict,
|
|
1976
|
+
rubric: REVIEW_RUBRIC,
|
|
1977
|
+
evidence: `${v.summary || (v.verdict === "pass" ? "no blocking findings" : "blocking findings")} (${v.findings.length} finding${v.findings.length === 1 ? "" : "s"}, ${secs}s)${lines.length ? `
|
|
1978
|
+
${lines.join(`
|
|
1979
|
+
`)}` : ""}`
|
|
1980
|
+
};
|
|
1981
|
+
}
|
|
1477
1982
|
// packages/core/src/tasks.ts
|
|
1478
1983
|
var ID_RE = /^[A-Za-z][A-Za-z0-9_-]*\d[\w.-]*$/;
|
|
1479
1984
|
var DEP_RE = /[A-Za-z][A-Za-z0-9_-]*\d[\w.]*/g;
|
|
@@ -1918,7 +2423,7 @@ var tryDecode = (str, decoder) => {
|
|
|
1918
2423
|
}
|
|
1919
2424
|
};
|
|
1920
2425
|
var tryDecodeURI = (str) => tryDecode(str, decodeURI);
|
|
1921
|
-
var
|
|
2426
|
+
var getPath2 = (request) => {
|
|
1922
2427
|
const url = request.url;
|
|
1923
2428
|
const start = url.indexOf("/", url.indexOf(":") + 4);
|
|
1924
2429
|
let i = start;
|
|
@@ -1937,7 +2442,7 @@ var getPath = (request) => {
|
|
|
1937
2442
|
return url.slice(start, i);
|
|
1938
2443
|
};
|
|
1939
2444
|
var getPathNoStrict = (request) => {
|
|
1940
|
-
const result =
|
|
2445
|
+
const result = getPath2(request);
|
|
1941
2446
|
return result.length > 1 && result.at(-1) === "/" ? result.slice(0, -1) : result;
|
|
1942
2447
|
};
|
|
1943
2448
|
var mergePath = (base, sub, ...rest) => {
|
|
@@ -2459,7 +2964,7 @@ var Hono = class _Hono {
|
|
|
2459
2964
|
};
|
|
2460
2965
|
const { strict, ...optionsWithoutStrict } = options;
|
|
2461
2966
|
Object.assign(this, optionsWithoutStrict);
|
|
2462
|
-
this.getPath = strict ?? true ? options.getPath ??
|
|
2967
|
+
this.getPath = strict ?? true ? options.getPath ?? getPath2 : getPathNoStrict;
|
|
2463
2968
|
}
|
|
2464
2969
|
#clone() {
|
|
2465
2970
|
const clone = new _Hono({
|
|
@@ -4309,9 +4814,10 @@ import {
|
|
|
4309
4814
|
realpathSync as realpathSync2,
|
|
4310
4815
|
renameSync,
|
|
4311
4816
|
statSync,
|
|
4817
|
+
unlinkSync,
|
|
4312
4818
|
writeFileSync as writeFileSync2
|
|
4313
4819
|
} from "fs";
|
|
4314
|
-
import { homedir as homedir3 } from "os";
|
|
4820
|
+
import { homedir as homedir3, tmpdir, userInfo } from "os";
|
|
4315
4821
|
import { basename, dirname as dirname2, join as join8 } from "path";
|
|
4316
4822
|
|
|
4317
4823
|
// packages/daemon/src/bootstrap.ts
|
|
@@ -4519,6 +5025,9 @@ class Store {
|
|
|
4519
5025
|
this.db.exec(SCHEMA);
|
|
4520
5026
|
this.ensureColumn("sessions", "agent", "TEXT DEFAULT 'claude-code'");
|
|
4521
5027
|
this.ensureColumn("projects", "sort_order", "INTEGER");
|
|
5028
|
+
this.ensureColumn("projects", "icon", "TEXT");
|
|
5029
|
+
this.ensureColumn("projects", "color", "TEXT");
|
|
5030
|
+
this.migrate();
|
|
4522
5031
|
this.migrateProjectsJson(join8(home, "projects.json"));
|
|
4523
5032
|
this.reconcileMovedProjects();
|
|
4524
5033
|
this.slimExistingEvents();
|
|
@@ -4601,6 +5110,57 @@ class Store {
|
|
|
4601
5110
|
this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${col} ${decl}`);
|
|
4602
5111
|
}
|
|
4603
5112
|
}
|
|
5113
|
+
static SCHEMA_VERSION = 1;
|
|
5114
|
+
schemaVersion() {
|
|
5115
|
+
return Number(this.meta("schema_version") ?? 0);
|
|
5116
|
+
}
|
|
5117
|
+
migrate() {
|
|
5118
|
+
const steps = [
|
|
5119
|
+
(db) => {
|
|
5120
|
+
for (const t of [
|
|
5121
|
+
"events",
|
|
5122
|
+
"claims",
|
|
5123
|
+
"resources",
|
|
5124
|
+
"processes",
|
|
5125
|
+
"handoffs",
|
|
5126
|
+
"gates",
|
|
5127
|
+
"incident_acks",
|
|
5128
|
+
"sessions"
|
|
5129
|
+
]) {
|
|
5130
|
+
this.ensureColumn(t, "actor_kind", "TEXT");
|
|
5131
|
+
this.ensureColumn(t, "actor_id", "TEXT");
|
|
5132
|
+
}
|
|
5133
|
+
const user = osUser();
|
|
5134
|
+
const fill = (table, ownerCol, sessionCol, key) => {
|
|
5135
|
+
const rows = db.query(`SELECT rowid AS rid, ${ownerCol ?? "NULL"} AS owner, ${sessionCol ?? "NULL"} AS sid FROM ${table} WHERE actor_kind IS NULL`).all();
|
|
5136
|
+
const upd = db.query(`UPDATE ${table} SET actor_kind = ?, actor_id = ? WHERE rowid = ?`);
|
|
5137
|
+
for (const r of rows) {
|
|
5138
|
+
const a = actorFrom(r.owner, r.sid, { user });
|
|
5139
|
+
upd.run(a.kind, a.id, r.rid);
|
|
5140
|
+
}
|
|
5141
|
+
return `${key}:${rows.length}`;
|
|
5142
|
+
};
|
|
5143
|
+
fill("claims", "owner", null, "claims");
|
|
5144
|
+
fill("resources", "owner", "session_id", "resources");
|
|
5145
|
+
fill("processes", "owner", "session_id", "processes");
|
|
5146
|
+
fill("handoffs", "by", "session_id", "handoffs");
|
|
5147
|
+
fill("gates", "NULL", "session_id", "gates");
|
|
5148
|
+
fill("incident_acks", "'dashboard'", null, "acks");
|
|
5149
|
+
fill("sessions", "NULL", "id", "sessions");
|
|
5150
|
+
fill("events", "COALESCE(json_extract(payload, '$.owner'), json_extract(payload, '$.by'))", "session_id", "events");
|
|
5151
|
+
}
|
|
5152
|
+
];
|
|
5153
|
+
for (let v = this.schemaVersion();v < steps.length; v++) {
|
|
5154
|
+
const step = steps[v];
|
|
5155
|
+
this.db.transaction(() => {
|
|
5156
|
+
step(this.db);
|
|
5157
|
+
this.setMeta("schema_version", String(v + 1));
|
|
5158
|
+
})();
|
|
5159
|
+
}
|
|
5160
|
+
}
|
|
5161
|
+
actorFor(owner, sessionId, runId) {
|
|
5162
|
+
return actorFrom(owner, sessionId, { user: osUser(), runId });
|
|
5163
|
+
}
|
|
4604
5164
|
migrateProjectsJson(file) {
|
|
4605
5165
|
if (!existsSync5(file))
|
|
4606
5166
|
return;
|
|
@@ -4621,7 +5181,8 @@ class Store {
|
|
|
4621
5181
|
this.topCache.set(cwd, { v, t: Date.now() });
|
|
4622
5182
|
return v;
|
|
4623
5183
|
}
|
|
4624
|
-
|
|
5184
|
+
policyCache = new Map;
|
|
5185
|
+
policySeen = new Set;
|
|
4625
5186
|
preregisterSpawnedSession(id, projectId, cwd, task) {
|
|
4626
5187
|
const now = new Date().toISOString();
|
|
4627
5188
|
this.db.query(`INSERT INTO sessions (id, project_id, kind, cwd, started_at, last_seen_at, last, last_type, state, title)
|
|
@@ -4650,8 +5211,8 @@ class Store {
|
|
|
4650
5211
|
createdAt: new Date().toISOString()
|
|
4651
5212
|
};
|
|
4652
5213
|
const sessionId = this.knownSession(h.sessionId);
|
|
4653
|
-
const ins = this.db.query(`INSERT INTO handoffs (project_id, task, done, remaining, files, verify, by, session_id, created_at)
|
|
4654
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(projectId, handoff.task, handoff.done, handoff.remaining, JSON.stringify(handoff.files), handoff.verify, handoff.by, sessionId, handoff.createdAt);
|
|
5214
|
+
const ins = this.db.query(`INSERT INTO handoffs (project_id, task, done, remaining, files, verify, by, session_id, created_at, actor_kind, actor_id)
|
|
5215
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(projectId, handoff.task, handoff.done, handoff.remaining, JSON.stringify(handoff.files), handoff.verify, handoff.by, sessionId, handoff.createdAt, ...actorCols(this.actorFor(handoff.by, sessionId)));
|
|
4655
5216
|
this.remember(handoffDoc(projectId, Number(ins.lastInsertRowid), handoff, sessionId));
|
|
4656
5217
|
this.append({
|
|
4657
5218
|
ts: handoff.createdAt,
|
|
@@ -4701,8 +5262,8 @@ class Store {
|
|
|
4701
5262
|
if (!h)
|
|
4702
5263
|
return null;
|
|
4703
5264
|
this.db.query("DELETE FROM handoffs WHERE project_id = ? AND task = ? AND session_id = ? AND by LIKE 'auto%'").run(held.projectId, held.task, sessionId);
|
|
4704
|
-
const ins = this.db.query(`INSERT INTO handoffs (project_id, task, done, remaining, files, verify, by, session_id, created_at)
|
|
4705
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(held.projectId, held.task, h.done, h.remaining, JSON.stringify(h.files), h.verify, h.by, sessionId, h.createdAt);
|
|
5265
|
+
const ins = this.db.query(`INSERT INTO handoffs (project_id, task, done, remaining, files, verify, by, session_id, created_at, actor_kind, actor_id)
|
|
5266
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(held.projectId, held.task, h.done, h.remaining, JSON.stringify(h.files), h.verify, h.by, sessionId, h.createdAt, ...actorCols(this.actorFor(h.by, sessionId)));
|
|
4706
5267
|
this.remember(handoffDoc(held.projectId, Number(ins.lastInsertRowid), h, sessionId));
|
|
4707
5268
|
this.touch();
|
|
4708
5269
|
return h;
|
|
@@ -5054,6 +5615,8 @@ class Store {
|
|
|
5054
5615
|
const logDir = join8(this.home, "logs", projectId);
|
|
5055
5616
|
mkdirSync4(logDir, { recursive: true });
|
|
5056
5617
|
const log = join8(logDir, `gate-${slug(task)}-${slug(gate)}.log`);
|
|
5618
|
+
if (def.builtin === "review")
|
|
5619
|
+
return this.runReviewGate(projectId, task, gate, def, { worktree: worktree2, cwd, key, log }, opts);
|
|
5057
5620
|
writeFileSync2(log, `$ ${def.cmd}
|
|
5058
5621
|
# cwd ${cwd} \xB7 ${new Date().toISOString()}
|
|
5059
5622
|
`);
|
|
@@ -5133,6 +5696,134 @@ class Store {
|
|
|
5133
5696
|
this.gateJobs.set(key, done);
|
|
5134
5697
|
return { ok: true, pid: proc.pid, log, done };
|
|
5135
5698
|
}
|
|
5699
|
+
runReviewGate(projectId, task, gate, def, where, opts) {
|
|
5700
|
+
const bin = findBin("claude");
|
|
5701
|
+
if (!bin)
|
|
5702
|
+
return { ok: false, reason: "claude CLI not found \u2014 the review gate needs Claude Code" };
|
|
5703
|
+
const p = this.project(projectId);
|
|
5704
|
+
if (!p)
|
|
5705
|
+
return { ok: false, reason: "unknown project" };
|
|
5706
|
+
const started = Date.now();
|
|
5707
|
+
const record = (input) => {
|
|
5708
|
+
const run2 = this.recordGate(projectId, { ...input, sessionId: opts.sessionId ?? null });
|
|
5709
|
+
return run2.ok ? run2.run : null;
|
|
5710
|
+
};
|
|
5711
|
+
const done = (async () => {
|
|
5712
|
+
let diffText = "";
|
|
5713
|
+
let stat = "";
|
|
5714
|
+
try {
|
|
5715
|
+
const diff = await worktreeDiff(p.root, where.worktree);
|
|
5716
|
+
stat = diff.files.map((f) => `${f.status ?? "M"} ${f.path} (+${f.added} -${f.deleted})`).join(`
|
|
5717
|
+
`);
|
|
5718
|
+
diffText = await worktreePatch(where.worktree, diff.base);
|
|
5719
|
+
} catch (e) {
|
|
5720
|
+
return record(reviewGateInput(task, gate, {
|
|
5721
|
+
kind: "error",
|
|
5722
|
+
reason: `diff failed: ${e.message}`,
|
|
5723
|
+
durationMs: Date.now() - started
|
|
5724
|
+
}));
|
|
5725
|
+
}
|
|
5726
|
+
if (!diffText.trim())
|
|
5727
|
+
return record(reviewGateInput(task, gate, {
|
|
5728
|
+
kind: "verdict",
|
|
5729
|
+
durationMs: Date.now() - started,
|
|
5730
|
+
verdict: { verdict: "pass", summary: "nothing to review \u2014 empty diff", findings: [] }
|
|
5731
|
+
}));
|
|
5732
|
+
const taskRow = this.tasks(projectId)?.tasks.find((t) => t.id === task) ?? null;
|
|
5733
|
+
const w = this.findWorktree(projectId, where.worktree);
|
|
5734
|
+
const prompt = reviewPrompt({
|
|
5735
|
+
task,
|
|
5736
|
+
title: taskRow?.title ?? null,
|
|
5737
|
+
branch: w?.branch ?? null,
|
|
5738
|
+
stat,
|
|
5739
|
+
patch: diffText
|
|
5740
|
+
});
|
|
5741
|
+
writeFileSync2(where.log, `$ claude -p <review prompt, ${prompt.length} chars> --output-format json (read-only)
|
|
5742
|
+
# cwd ${where.cwd} \xB7 ${new Date().toISOString()}
|
|
5743
|
+
`);
|
|
5744
|
+
let proc;
|
|
5745
|
+
try {
|
|
5746
|
+
proc = Bun.spawn([bin, ...reviewArgs(prompt, { model: def.model })], {
|
|
5747
|
+
cwd: where.cwd,
|
|
5748
|
+
stdin: "ignore",
|
|
5749
|
+
stdout: "pipe",
|
|
5750
|
+
stderr: "pipe",
|
|
5751
|
+
env: {
|
|
5752
|
+
...process.env,
|
|
5753
|
+
SWARM_WORKTREE: where.worktree,
|
|
5754
|
+
SWARM_TASK: task,
|
|
5755
|
+
SWARM_GATE: gate,
|
|
5756
|
+
CLAUDE_CODE_DISABLE_AUTOUPDATE: "1"
|
|
5757
|
+
}
|
|
5758
|
+
});
|
|
5759
|
+
} catch (e) {
|
|
5760
|
+
return record(reviewGateInput(task, gate, {
|
|
5761
|
+
kind: "error",
|
|
5762
|
+
reason: e.message,
|
|
5763
|
+
durationMs: Date.now() - started
|
|
5764
|
+
}));
|
|
5765
|
+
}
|
|
5766
|
+
const reg = this.registerProcess({
|
|
5767
|
+
pid: proc.pid,
|
|
5768
|
+
projectId,
|
|
5769
|
+
sessionId: opts.sessionId ?? null,
|
|
5770
|
+
kind: "gate",
|
|
5771
|
+
name: `gate:${task}:${gate}`,
|
|
5772
|
+
cwd: where.cwd,
|
|
5773
|
+
cmd: "claude -p (review)",
|
|
5774
|
+
owner: opts.owner ?? "daemon",
|
|
5775
|
+
log: where.log
|
|
5776
|
+
});
|
|
5777
|
+
let timedOut = false;
|
|
5778
|
+
const timer = setTimeout(() => {
|
|
5779
|
+
timedOut = true;
|
|
5780
|
+
try {
|
|
5781
|
+
proc.kill("SIGTERM");
|
|
5782
|
+
setTimeout(() => {
|
|
5783
|
+
try {
|
|
5784
|
+
proc.kill("SIGKILL");
|
|
5785
|
+
} catch {}
|
|
5786
|
+
}, 5000).unref();
|
|
5787
|
+
} catch {}
|
|
5788
|
+
}, def.timeout * 1000);
|
|
5789
|
+
const [out, err] = await Promise.all([
|
|
5790
|
+
new Response(proc.stdout).text(),
|
|
5791
|
+
new Response(proc.stderr).text()
|
|
5792
|
+
]);
|
|
5793
|
+
const code = await proc.exited;
|
|
5794
|
+
clearTimeout(timer);
|
|
5795
|
+
try {
|
|
5796
|
+
writeFileSync2(where.log, `${readFileSync3(where.log, "utf8")}${out}
|
|
5797
|
+
${err}
|
|
5798
|
+
# exit ${timedOut ? "timeout" : code} \xB7 ${((Date.now() - started) / 1000).toFixed(0)}s
|
|
5799
|
+
`);
|
|
5800
|
+
} catch {}
|
|
5801
|
+
if (reg.ok)
|
|
5802
|
+
this.processes(projectId);
|
|
5803
|
+
const durationMs = Date.now() - started;
|
|
5804
|
+
if (timedOut)
|
|
5805
|
+
return record(reviewGateInput(task, gate, {
|
|
5806
|
+
kind: "error",
|
|
5807
|
+
reason: `timed out after ${def.timeout}s`,
|
|
5808
|
+
durationMs,
|
|
5809
|
+
output: err
|
|
5810
|
+
}));
|
|
5811
|
+
const verdict = parseReviewVerdict(out);
|
|
5812
|
+
if (!verdict)
|
|
5813
|
+
return record(reviewGateInput(task, gate, {
|
|
5814
|
+
kind: "error",
|
|
5815
|
+
reason: code === 0 ? "no JSON verdict in the reply" : `claude exited ${code}`,
|
|
5816
|
+
durationMs,
|
|
5817
|
+
output: err || out
|
|
5818
|
+
}));
|
|
5819
|
+
return record(reviewGateInput(task, gate, { kind: "verdict", verdict, durationMs }));
|
|
5820
|
+
})().finally(() => {
|
|
5821
|
+
this.gateJobs.delete(where.key);
|
|
5822
|
+
this.touch();
|
|
5823
|
+
});
|
|
5824
|
+
this.gateJobs.set(where.key, done);
|
|
5825
|
+
return { ok: true, pid: 0, log: where.log, done };
|
|
5826
|
+
}
|
|
5136
5827
|
async runGates(projectId, task, gates2, opts = {}) {
|
|
5137
5828
|
const cfg = this.gateDefs(projectId);
|
|
5138
5829
|
const names = gates2?.length ? gates2 : (cfg?.required ?? []).filter((g) => cfg?.defs[g]);
|
|
@@ -5202,8 +5893,8 @@ class Store {
|
|
|
5202
5893
|
return v;
|
|
5203
5894
|
const createdAt = new Date().toISOString();
|
|
5204
5895
|
const sessionId = this.knownSession(input.sessionId);
|
|
5205
|
-
const r = this.db.query(`INSERT INTO gates (project_id, task, gate, verdict, rubric, evidence, session_id, created_at)
|
|
5206
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(projectId, input.task.trim(), input.gate, input.verdict, input.rubric.trim(), input.evidence?.trim() || null, sessionId, createdAt);
|
|
5896
|
+
const r = this.db.query(`INSERT INTO gates (project_id, task, gate, verdict, rubric, evidence, session_id, created_at, actor_kind, actor_id)
|
|
5897
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(projectId, input.task.trim(), input.gate, input.verdict, input.rubric.trim(), input.evidence?.trim() || null, sessionId, createdAt, ...actorCols(this.actorFor(input.sessionId ? null : "daemon", sessionId)));
|
|
5207
5898
|
const run2 = this.rowToGate(this.db.query("SELECT * FROM gates WHERE id = ?").get(Number(r.lastInsertRowid)));
|
|
5208
5899
|
this.remember(gateDoc(projectId, run2.id, run2, sessionId));
|
|
5209
5900
|
this.append({
|
|
@@ -5289,13 +5980,67 @@ class Store {
|
|
|
5289
5980
|
return { source, required, tasks: board, error };
|
|
5290
5981
|
}
|
|
5291
5982
|
rulesFor(repoRoot) {
|
|
5983
|
+
return this.policyFor(repoRoot).config.rules;
|
|
5984
|
+
}
|
|
5985
|
+
policyFor(repoRoot) {
|
|
5292
5986
|
const key = repoRoot ?? "";
|
|
5293
|
-
const hit = this.
|
|
5987
|
+
const hit = this.policyCache.get(key);
|
|
5294
5988
|
if (hit && Date.now() - hit.at < 30000)
|
|
5295
|
-
return hit.
|
|
5296
|
-
const
|
|
5297
|
-
this.
|
|
5298
|
-
|
|
5989
|
+
return hit.loaded;
|
|
5990
|
+
const loaded = loadConfigDetailed({ repoRoot, home: this.home });
|
|
5991
|
+
this.policyCache.set(key, { at: Date.now(), loaded });
|
|
5992
|
+
this.writePolicyCache(loaded);
|
|
5993
|
+
return loaded;
|
|
5994
|
+
}
|
|
5995
|
+
writePolicyCache(loaded) {
|
|
5996
|
+
const file = join8(this.home, POLICY_CACHE_FILE);
|
|
5997
|
+
try {
|
|
5998
|
+
if (!hasLockedRules(loaded)) {
|
|
5999
|
+
if (existsSync5(file))
|
|
6000
|
+
unlinkSync(file);
|
|
6001
|
+
return;
|
|
6002
|
+
}
|
|
6003
|
+
const cache = buildPolicyCache(loaded, this.liveSessions(), this.heldWorktrees());
|
|
6004
|
+
writeFileSync2(file, JSON.stringify(cache), { mode: 384 });
|
|
6005
|
+
} catch (e) {
|
|
6006
|
+
console.error(`swarm: policy cache: ${e.message}`);
|
|
6007
|
+
}
|
|
6008
|
+
}
|
|
6009
|
+
guardDisabled(repoRoot) {
|
|
6010
|
+
return process.env.SWARM_GUARD === "off" && !hasLockedRules(this.policyFor(repoRoot));
|
|
6011
|
+
}
|
|
6012
|
+
claudeSettings() {
|
|
6013
|
+
const p = process.env.CLAUDE_SETTINGS ?? join8(homedir3(), ".claude", "settings.json");
|
|
6014
|
+
try {
|
|
6015
|
+
return existsSync5(p) ? JSON.parse(readFileSync3(p, "utf8")) : null;
|
|
6016
|
+
} catch {
|
|
6017
|
+
return null;
|
|
6018
|
+
}
|
|
6019
|
+
}
|
|
6020
|
+
checkPolicy(cwd, sessionId) {
|
|
6021
|
+
const project = existsSync5(cwd) ? this.resolveProject(cwd) : null;
|
|
6022
|
+
const repoRoot = project?.root ?? null;
|
|
6023
|
+
const loaded = this.policyFor(repoRoot);
|
|
6024
|
+
const settings = this.claudeSettings();
|
|
6025
|
+
const findings = policyFindings({
|
|
6026
|
+
loaded,
|
|
6027
|
+
coverage: settings === null ? null : hookCoverage(settings),
|
|
6028
|
+
guardOff: process.env.SWARM_GUARD === "off",
|
|
6029
|
+
repoRoot
|
|
6030
|
+
});
|
|
6031
|
+
for (const f of findings) {
|
|
6032
|
+
if (this.policySeen.has(f.key))
|
|
6033
|
+
continue;
|
|
6034
|
+
this.policySeen.add(f.key);
|
|
6035
|
+
this.append({
|
|
6036
|
+
ts: new Date().toISOString(),
|
|
6037
|
+
type: "incident.opened",
|
|
6038
|
+
projectId: project?.id ?? "p_unknown",
|
|
6039
|
+
sessionId,
|
|
6040
|
+
payload: { rule: "policy", action: "tampered", command: f.subject, reason: f.reason }
|
|
6041
|
+
});
|
|
6042
|
+
}
|
|
6043
|
+
return findings;
|
|
5299
6044
|
}
|
|
5300
6045
|
evaluateTool(tool, input, sessionId, cwd, recordIncident = true) {
|
|
5301
6046
|
if (BUDGET_ASK_TOOLS.has(tool) && cwd && existsSync5(cwd)) {
|
|
@@ -5498,6 +6243,8 @@ class Store {
|
|
|
5498
6243
|
name: r.name,
|
|
5499
6244
|
discovered: Boolean(r.discovered),
|
|
5500
6245
|
order: typeof r.sort_order === "number" ? r.sort_order : null,
|
|
6246
|
+
icon: r.icon ?? null,
|
|
6247
|
+
color: r.color ?? null,
|
|
5501
6248
|
createdAt: r.created_at
|
|
5502
6249
|
}));
|
|
5503
6250
|
}
|
|
@@ -5512,6 +6259,8 @@ class Store {
|
|
|
5512
6259
|
name: r.name,
|
|
5513
6260
|
discovered: Boolean(r.discovered),
|
|
5514
6261
|
order: typeof r.sort_order === "number" ? r.sort_order : null,
|
|
6262
|
+
icon: r.icon ?? null,
|
|
6263
|
+
color: r.color ?? null,
|
|
5515
6264
|
createdAt: r.created_at
|
|
5516
6265
|
};
|
|
5517
6266
|
}
|
|
@@ -5549,6 +6298,8 @@ class Store {
|
|
|
5549
6298
|
...ident,
|
|
5550
6299
|
discovered: !explicit,
|
|
5551
6300
|
order: null,
|
|
6301
|
+
icon: null,
|
|
6302
|
+
color: null,
|
|
5552
6303
|
createdAt: new Date().toISOString()
|
|
5553
6304
|
};
|
|
5554
6305
|
if (name)
|
|
@@ -5569,8 +6320,24 @@ class Store {
|
|
|
5569
6320
|
return;
|
|
5570
6321
|
if (patch.pinned !== undefined)
|
|
5571
6322
|
this.db.query("UPDATE projects SET discovered = ? WHERE id = ?").run(patch.pinned ? 0 : 1, id);
|
|
5572
|
-
if (patch.name)
|
|
5573
|
-
this.db.query("UPDATE projects SET name = ? WHERE id = ?").run(patch.name, id);
|
|
6323
|
+
if (patch.name?.trim())
|
|
6324
|
+
this.db.query("UPDATE projects SET name = ? WHERE id = ?").run(patch.name.trim(), id);
|
|
6325
|
+
if (patch.icon !== undefined) {
|
|
6326
|
+
const icon = (patch.icon ?? "").trim();
|
|
6327
|
+
const isImage = /^data:image\/(png|jpeg|webp);base64,[A-Za-z0-9+/=]+$/.test(icon);
|
|
6328
|
+
if (!isImage && [...icon].length > 4)
|
|
6329
|
+
return;
|
|
6330
|
+
if (isImage && icon.length > 24000)
|
|
6331
|
+
return;
|
|
6332
|
+
this.db.query("UPDATE projects SET icon = ? WHERE id = ?").run(icon || null, id);
|
|
6333
|
+
}
|
|
6334
|
+
if (patch.color !== undefined) {
|
|
6335
|
+
const color = (patch.color ?? "").trim();
|
|
6336
|
+
if (color && !/^c[1-7]$/.test(color))
|
|
6337
|
+
return;
|
|
6338
|
+
this.db.query("UPDATE projects SET color = ? WHERE id = ?").run(color || null, id);
|
|
6339
|
+
}
|
|
6340
|
+
this.touch();
|
|
5574
6341
|
return this.project(id);
|
|
5575
6342
|
}
|
|
5576
6343
|
reorderProjects(ids) {
|
|
@@ -5589,10 +6356,29 @@ class Store {
|
|
|
5589
6356
|
this.touch();
|
|
5590
6357
|
return this.db.query("DELETE FROM projects WHERE id = ?").run(id).changes > 0;
|
|
5591
6358
|
}
|
|
5592
|
-
|
|
6359
|
+
redactions() {
|
|
6360
|
+
const cfg = this.policyFor(null).config.privacy;
|
|
6361
|
+
const key = cfg.redact.join("\x00");
|
|
6362
|
+
if (this.redactCache?.key !== key)
|
|
6363
|
+
this.redactCache = { key, res: compileRedactions(cfg.redact) };
|
|
6364
|
+
return this.redactCache.res;
|
|
6365
|
+
}
|
|
6366
|
+
redactCache = null;
|
|
6367
|
+
append(e0) {
|
|
6368
|
+
const privacy = this.policyFor(null).config.privacy;
|
|
6369
|
+
let e = e0;
|
|
6370
|
+
if (!privacy.store_prompts && e.type === "prompt.submitted" && e.payload && typeof e.payload === "object") {
|
|
6371
|
+
const { prompt: _p, ...rest } = e.payload;
|
|
6372
|
+
e = { ...e, payload: { ...rest, prompt: "[not stored]" } };
|
|
6373
|
+
}
|
|
6374
|
+
const res = this.redactions();
|
|
6375
|
+
if (res.length)
|
|
6376
|
+
e = { ...e, payload: redactValue(e.payload, res), raw: redactValue(e.raw, res) };
|
|
5593
6377
|
const slim = slimForStorage(e);
|
|
5594
|
-
const
|
|
5595
|
-
const
|
|
6378
|
+
const p = e.payload ?? {};
|
|
6379
|
+
const actor2 = e.actor ?? this.actorFor(typeof p.owner === "string" ? p.owner : typeof p.by === "string" ? p.by : null, e.sessionId);
|
|
6380
|
+
const r = this.db.query("INSERT INTO events (ts, type, project_id, session_id, payload, raw, actor_kind, actor_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)").run(e.ts, e.type, e.projectId, e.sessionId, JSON.stringify(slim.payload ?? null), slim.raw === undefined ? null : JSON.stringify(slim.raw), actor2.kind, actor2.id);
|
|
6381
|
+
const stored = { ...e, actor: actor2, seq: Number(r.lastInsertRowid) };
|
|
5596
6382
|
if (stored.type === "incident.opened")
|
|
5597
6383
|
this.remember(incidentDoc(stored.projectId, stored.seq, stored.payload, stored.ts, stored.sessionId));
|
|
5598
6384
|
this.projectSession(stored);
|
|
@@ -5602,9 +6388,34 @@ class Store {
|
|
|
5602
6388
|
l(wire);
|
|
5603
6389
|
return stored;
|
|
5604
6390
|
}
|
|
5605
|
-
|
|
5606
|
-
const
|
|
5607
|
-
const
|
|
6391
|
+
audit(opts = {}) {
|
|
6392
|
+
const where = [`type IN (${AUDIT_TYPES_SQL})`];
|
|
6393
|
+
const args = [];
|
|
6394
|
+
if (opts.since) {
|
|
6395
|
+
where.push("ts >= ?");
|
|
6396
|
+
args.push(opts.since);
|
|
6397
|
+
}
|
|
6398
|
+
if (opts.projectId) {
|
|
6399
|
+
where.push("project_id = ?");
|
|
6400
|
+
args.push(opts.projectId);
|
|
6401
|
+
}
|
|
6402
|
+
if (opts.type && isAuditType(opts.type)) {
|
|
6403
|
+
where.push("type = ?");
|
|
6404
|
+
args.push(opts.type);
|
|
6405
|
+
}
|
|
6406
|
+
const limit = Math.min(Math.max(opts.limit ?? 1e4, 1), 1e5);
|
|
6407
|
+
const rows = this.db.query(`SELECT * FROM (SELECT ${WIRE_COLS} FROM events WHERE ${where.join(" AND ")} ORDER BY seq DESC LIMIT ?) ORDER BY seq`).all(...args, limit);
|
|
6408
|
+
return rows.map((r) => auditRow(wireRowToEvent(r)));
|
|
6409
|
+
}
|
|
6410
|
+
prune(days2) {
|
|
6411
|
+
const cfg = this.policyFor(null).config;
|
|
6412
|
+
const chatter = days2 ?? cfg.events.retain_days;
|
|
6413
|
+
const cutoff = new Date(Date.now() - chatter * 86400000).toISOString();
|
|
6414
|
+
let n = this.db.query(`DELETE FROM events WHERE ts < ? AND type NOT IN (${AUDIT_TYPES_SQL})`).run(cutoff).changes;
|
|
6415
|
+
if (cfg.audit.retain_days > 0) {
|
|
6416
|
+
const acut = new Date(Date.now() - cfg.audit.retain_days * 86400000).toISOString();
|
|
6417
|
+
n += this.db.query(`DELETE FROM events WHERE ts < ? AND type IN (${AUDIT_TYPES_SQL}) AND type != 'incident.opened'`).run(acut).changes;
|
|
6418
|
+
}
|
|
5608
6419
|
const old = new Date(Date.now() - 7 * 86400000).toISOString();
|
|
5609
6420
|
this.db.query("UPDATE events SET raw = NULL WHERE ts < ? AND raw IS NOT NULL").run(old);
|
|
5610
6421
|
if (n > 0)
|
|
@@ -5702,13 +6513,15 @@ class Store {
|
|
|
5702
6513
|
}
|
|
5703
6514
|
}
|
|
5704
6515
|
persistTurns(sessionId, agentId, turns) {
|
|
6516
|
+
const privacy = this.policyFor(null).config.privacy;
|
|
6517
|
+
const res = this.redactions();
|
|
5705
6518
|
const up = this.db.query(`INSERT INTO turns (id, session_id, agent_id, ts, model, effort, sidechain, input, output, cache_write, cache_write_1h, cache_read, thinking, cost_usd, text, tools)
|
|
5706
6519
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
5707
6520
|
ON CONFLICT(id) DO UPDATE SET input=excluded.input, output=excluded.output, cache_write=excluded.cache_write, cache_write_1h=excluded.cache_write_1h,
|
|
5708
6521
|
cache_read=excluded.cache_read, thinking=excluded.thinking, cost_usd=excluded.cost_usd, text=CASE WHEN excluded.text != '' THEN excluded.text ELSE turns.text END, tools=excluded.tools`);
|
|
5709
6522
|
const tx = this.db.transaction((ts) => {
|
|
5710
6523
|
for (const t of ts) {
|
|
5711
|
-
up.run(t.id, sessionId, agentId, t.ts, t.model, t.effort, t.sidechain ? 1 : 0, t.usage.input, t.usage.output, t.usage.cacheWrite, t.usage.cacheWrite1h ?? 0, t.usage.cacheRead, t.usage.thinking, costUsd(t.model, t.usage, this.prices), t.text, JSON.stringify(t.tools));
|
|
6524
|
+
up.run(t.id, sessionId, agentId, t.ts, t.model, t.effort, t.sidechain ? 1 : 0, t.usage.input, t.usage.output, t.usage.cacheWrite, t.usage.cacheWrite1h ?? 0, t.usage.cacheRead, t.usage.thinking, costUsd(t.model, t.usage, this.prices), privacy.store_reasoning ? redactValue(t.text, res) : "", JSON.stringify(t.tools));
|
|
5712
6525
|
}
|
|
5713
6526
|
});
|
|
5714
6527
|
if (turns.length)
|
|
@@ -5939,7 +6752,7 @@ class Store {
|
|
|
5939
6752
|
const p = this.project(projectId);
|
|
5940
6753
|
return join8(this.home, "worktrees", slug(p?.name ?? projectId), slug(task));
|
|
5941
6754
|
}
|
|
5942
|
-
claim(projectId, task, owner, baseRef = "HEAD") {
|
|
6755
|
+
claim(projectId, task, owner, baseRef = "HEAD", sessionId = null) {
|
|
5943
6756
|
const p = this.project(projectId);
|
|
5944
6757
|
if (!p)
|
|
5945
6758
|
return { ok: false, error: "unknown project" };
|
|
@@ -5958,15 +6771,17 @@ class Store {
|
|
|
5958
6771
|
this.invalidateWorktrees(projectId);
|
|
5959
6772
|
const expiresAt = nextExpiry(now);
|
|
5960
6773
|
const acquiredAt = new Date(now).toISOString();
|
|
5961
|
-
this.db.query(`INSERT INTO claims (project_id, task, owner, worktree, branch, acquired_at, expires_at, released_at, state)
|
|
5962
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, NULL, 'held')
|
|
6774
|
+
this.db.query(`INSERT INTO claims (project_id, task, owner, worktree, branch, acquired_at, expires_at, released_at, state, actor_kind, actor_id)
|
|
6775
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, NULL, 'held', ?, ?)
|
|
5963
6776
|
ON CONFLICT(project_id, task) DO UPDATE SET owner=excluded.owner, worktree=excluded.worktree, branch=excluded.branch,
|
|
5964
|
-
acquired_at=excluded.acquired_at, expires_at=excluded.expires_at, released_at=NULL, state='held'
|
|
6777
|
+
acquired_at=excluded.acquired_at, expires_at=excluded.expires_at, released_at=NULL, state='held',
|
|
6778
|
+
actor_kind=excluded.actor_kind, actor_id=excluded.actor_id`).run(projectId, task, owner, created, branch, acquiredAt, expiresAt, ...actorCols(this.actorFor(owner, sessionId)));
|
|
5965
6779
|
this.append({
|
|
5966
6780
|
ts: acquiredAt,
|
|
5967
6781
|
type: "claim.acquired",
|
|
5968
6782
|
projectId,
|
|
5969
|
-
sessionId
|
|
6783
|
+
sessionId,
|
|
6784
|
+
actor: this.actorFor(owner, sessionId),
|
|
5970
6785
|
payload: { task, owner, worktree: created, branch, summary: `claim ${task} by ${owner}` }
|
|
5971
6786
|
});
|
|
5972
6787
|
const bootstrap = this.bootstrapWorktree(projectId, task, p.root, created);
|
|
@@ -6642,19 +7457,19 @@ class Store {
|
|
|
6642
7457
|
WHERE e.type = 'incident.opened' AND a.seq IS NULL${projectId ? " AND e.project_id = ?" : ""}`).get(...projectId ? [projectId] : []);
|
|
6643
7458
|
return r.n;
|
|
6644
7459
|
}
|
|
6645
|
-
ackIncident(seq) {
|
|
7460
|
+
ackIncident(seq, by) {
|
|
6646
7461
|
const row = this.db.query("SELECT seq FROM events WHERE seq = ? AND type = 'incident.opened'").get(seq);
|
|
6647
7462
|
if (!row)
|
|
6648
7463
|
return false;
|
|
6649
|
-
this.db.query("INSERT OR IGNORE INTO incident_acks (seq, acked_at) VALUES (?, ?)").run(seq, new Date().toISOString());
|
|
7464
|
+
this.db.query("INSERT OR IGNORE INTO incident_acks (seq, acked_at, actor_kind, actor_id) VALUES (?, ?, ?, ?)").run(seq, new Date().toISOString(), ...actorCols(this.actorFor(by ?? "dashboard")));
|
|
6650
7465
|
this.touch();
|
|
6651
7466
|
return true;
|
|
6652
7467
|
}
|
|
6653
|
-
ackAllIncidents(projectId) {
|
|
7468
|
+
ackAllIncidents(projectId, by) {
|
|
6654
7469
|
const at = new Date().toISOString();
|
|
6655
|
-
const r = this.db.query(`INSERT OR IGNORE INTO incident_acks (seq, acked_at)
|
|
6656
|
-
SELECT e.seq, ? FROM events e LEFT JOIN incident_acks a ON a.seq = e.seq
|
|
6657
|
-
WHERE e.type = 'incident.opened' AND a.seq IS NULL${projectId ? " AND e.project_id = ?" : ""}`).run(at, ...projectId ? [projectId] : []);
|
|
7470
|
+
const r = this.db.query(`INSERT OR IGNORE INTO incident_acks (seq, acked_at, actor_kind, actor_id)
|
|
7471
|
+
SELECT e.seq, ?, ?, ? FROM events e LEFT JOIN incident_acks a ON a.seq = e.seq
|
|
7472
|
+
WHERE e.type = 'incident.opened' AND a.seq IS NULL${projectId ? " AND e.project_id = ?" : ""}`).run(at, ...actorCols(this.actorFor(by ?? "dashboard")), ...projectId ? [projectId] : []);
|
|
6658
7473
|
this.touch();
|
|
6659
7474
|
return Number(r.changes);
|
|
6660
7475
|
}
|
|
@@ -6821,8 +7636,8 @@ class Store {
|
|
|
6821
7636
|
endedAt: null
|
|
6822
7637
|
};
|
|
6823
7638
|
this.db.query("UPDATE processes SET ended_at = ? WHERE ended_at IS NULL AND project_id = ? AND name = ?").run(p.startedAt, p.projectId, p.name);
|
|
6824
|
-
this.db.query(`INSERT INTO processes (pid, start_time, project_id, session_id, kind, name, port, cwd, cmd, owner, log, started_at, ended_at)
|
|
6825
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)`).run(p.pid, p.startTime, p.projectId, p.sessionId, p.kind, p.name, p.port, p.cwd, p.cmd, p.owner, p.log, p.startedAt);
|
|
7639
|
+
this.db.query(`INSERT INTO processes (pid, start_time, project_id, session_id, kind, name, port, cwd, cmd, owner, log, started_at, ended_at, actor_kind, actor_id)
|
|
7640
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?)`).run(p.pid, p.startTime, p.projectId, p.sessionId, p.kind, p.name, p.port, p.cwd, p.cmd, p.owner, p.log, p.startedAt, ...actorCols(this.actorFor(p.owner, p.sessionId)));
|
|
6826
7641
|
this.append({
|
|
6827
7642
|
ts: p.startedAt,
|
|
6828
7643
|
type: "process.started",
|
|
@@ -6886,11 +7701,12 @@ class Store {
|
|
|
6886
7701
|
expiresAt,
|
|
6887
7702
|
released: false
|
|
6888
7703
|
};
|
|
6889
|
-
this.db.query(`INSERT INTO resources (name, project_id, kind, owner, session_id, pid, port, acquired_at, expires_at, released)
|
|
6890
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0)
|
|
7704
|
+
this.db.query(`INSERT INTO resources (name, project_id, kind, owner, session_id, pid, port, acquired_at, expires_at, released, actor_kind, actor_id)
|
|
7705
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?)
|
|
6891
7706
|
ON CONFLICT(name, project_id) DO UPDATE SET
|
|
6892
7707
|
kind=excluded.kind, owner=excluded.owner, session_id=excluded.session_id, pid=excluded.pid,
|
|
6893
|
-
port=excluded.port, acquired_at=excluded.acquired_at, expires_at=excluded.expires_at, released=0
|
|
7708
|
+
port=excluded.port, acquired_at=excluded.acquired_at, expires_at=excluded.expires_at, released=0,
|
|
7709
|
+
actor_kind=excluded.actor_kind, actor_id=excluded.actor_id`).run(resource.name, key, resource.kind, resource.owner, resource.sessionId, resource.pid, resource.port, resource.acquiredAt, resource.expiresAt, ...actorCols(this.actorFor(resource.owner, resource.sessionId)));
|
|
6894
7710
|
this.append({
|
|
6895
7711
|
ts: resource.acquiredAt,
|
|
6896
7712
|
type: "resource.acquired",
|
|
@@ -6937,7 +7753,7 @@ class Store {
|
|
|
6937
7753
|
}
|
|
6938
7754
|
snapshot() {
|
|
6939
7755
|
const worktrees = {};
|
|
6940
|
-
const projects = this.projects();
|
|
7756
|
+
const projects = this.projects().filter((p) => !(p.discovered && isScratchRoot(p.root)));
|
|
6941
7757
|
for (const p of projects)
|
|
6942
7758
|
worktrees[p.id] = this.worktrees(p.id);
|
|
6943
7759
|
return {
|
|
@@ -6955,7 +7771,7 @@ class Store {
|
|
|
6955
7771
|
};
|
|
6956
7772
|
}
|
|
6957
7773
|
}
|
|
6958
|
-
var WIRE_COLS = "seq, ts, type, project_id, session_id, json_remove(payload, '$.toolInput', '$.toolResponse', '$.prompt') AS payload";
|
|
7774
|
+
var WIRE_COLS = "seq, ts, type, project_id, session_id, actor_kind, actor_id, json_remove(payload, '$.toolInput', '$.toolResponse', '$.prompt') AS payload";
|
|
6959
7775
|
var RAW_TOOL_KEYS = ["tool_input", "tool_response", "toolInput", "toolResponse", "toolResult"];
|
|
6960
7776
|
var TOOL_INPUT_MAX = 2048;
|
|
6961
7777
|
var TOOL_RESPONSE_MAX = 4096;
|
|
@@ -7001,7 +7817,7 @@ function toWire(e) {
|
|
|
7001
7817
|
}
|
|
7002
7818
|
function wireRowToEvent(r) {
|
|
7003
7819
|
const p = JSON.parse(r.payload ?? "null");
|
|
7004
|
-
|
|
7820
|
+
const e = {
|
|
7005
7821
|
seq: r.seq,
|
|
7006
7822
|
ts: r.ts,
|
|
7007
7823
|
type: r.type,
|
|
@@ -7009,6 +7825,10 @@ function wireRowToEvent(r) {
|
|
|
7009
7825
|
sessionId: r.session_id ?? null,
|
|
7010
7826
|
payload: p
|
|
7011
7827
|
};
|
|
7828
|
+
const a = actorFromColumns(r.actor_kind, r.actor_id, r.session_id);
|
|
7829
|
+
if (a)
|
|
7830
|
+
e.actor = a;
|
|
7831
|
+
return e;
|
|
7012
7832
|
}
|
|
7013
7833
|
function rowToEvent(r) {
|
|
7014
7834
|
const e = {
|
|
@@ -7021,11 +7841,26 @@ function rowToEvent(r) {
|
|
|
7021
7841
|
};
|
|
7022
7842
|
if (r.raw)
|
|
7023
7843
|
e.raw = JSON.parse(r.raw);
|
|
7844
|
+
const a = actorFromColumns(r.actor_kind, r.actor_id, r.session_id);
|
|
7845
|
+
if (a)
|
|
7846
|
+
e.actor = a;
|
|
7024
7847
|
return e;
|
|
7025
7848
|
}
|
|
7849
|
+
function osUser() {
|
|
7850
|
+
try {
|
|
7851
|
+
return userInfo().username || process.env.USER || "me";
|
|
7852
|
+
} catch {
|
|
7853
|
+
return process.env.USER || "me";
|
|
7854
|
+
}
|
|
7855
|
+
}
|
|
7856
|
+
var actorCols = (a) => [a.kind, a.id];
|
|
7857
|
+
function isScratchRoot(root) {
|
|
7858
|
+
const tmp = [tmpdir(), "/tmp", "/private/tmp", "/private/var/folders", "/var/folders"];
|
|
7859
|
+
return tmp.some((t) => root === t || root.startsWith(`${t}/`));
|
|
7860
|
+
}
|
|
7026
7861
|
|
|
7027
7862
|
// packages/daemon/src/app.ts
|
|
7028
|
-
var VERSION = "0.
|
|
7863
|
+
var VERSION = "0.8.0";
|
|
7029
7864
|
var WEB_DIR = (() => {
|
|
7030
7865
|
if (process.env.SWARM_WEB_DIR)
|
|
7031
7866
|
return process.env.SWARM_WEB_DIR;
|
|
@@ -7043,6 +7878,10 @@ function wireJson(e) {
|
|
|
7043
7878
|
}
|
|
7044
7879
|
return s;
|
|
7045
7880
|
}
|
|
7881
|
+
function hookRepoRoot(store, raw2) {
|
|
7882
|
+
const cwd = typeof raw2.cwd === "string" ? raw2.cwd : "";
|
|
7883
|
+
return cwd && existsSync6(cwd) ? store.resolveProject(cwd)?.root ?? null : null;
|
|
7884
|
+
}
|
|
7046
7885
|
function createApp(store = new Store) {
|
|
7047
7886
|
const app = new Hono2;
|
|
7048
7887
|
const forge2 = new ForgeService(store);
|
|
@@ -7053,7 +7892,27 @@ function createApp(store = new Store) {
|
|
|
7053
7892
|
for (const run2 of runner.list(projectId))
|
|
7054
7893
|
runner.stop(run2.id);
|
|
7055
7894
|
});
|
|
7056
|
-
app.
|
|
7895
|
+
app.use("/v1/*", async (c, next) => {
|
|
7896
|
+
if (c.req.path === "/v1/health")
|
|
7897
|
+
return next();
|
|
7898
|
+
const token = readToken(store.home);
|
|
7899
|
+
const given = c.req.header("authorization")?.replace(/^Bearer\s+/i, "") ?? c.req.query("token");
|
|
7900
|
+
if (token && given && given === token)
|
|
7901
|
+
return next();
|
|
7902
|
+
if (given)
|
|
7903
|
+
return c.json({ error: "unauthorized: wrong daemon token" }, 401);
|
|
7904
|
+
const ip = c.env?.requestIP?.(c.req.raw)?.address;
|
|
7905
|
+
const loopback = !ip || ip === "127.0.0.1" || ip === "::1" || ip === "::ffff:127.0.0.1";
|
|
7906
|
+
if (loopback && store.policyFor(null).config.daemon.auth !== "required")
|
|
7907
|
+
return next();
|
|
7908
|
+
return c.json({ error: "unauthorized: send the daemon token (~/.swarm/token) as Authorization: Bearer" }, 401);
|
|
7909
|
+
});
|
|
7910
|
+
app.get("/v1/health", (c) => c.json({
|
|
7911
|
+
ok: true,
|
|
7912
|
+
version: VERSION,
|
|
7913
|
+
schema: store.schemaVersion(),
|
|
7914
|
+
auth: store.policyFor(null).config.daemon.auth
|
|
7915
|
+
}));
|
|
7057
7916
|
app.get("/v1/projects", (c) => c.json(store.snapshot().projects));
|
|
7058
7917
|
app.post("/v1/projects", async (c) => {
|
|
7059
7918
|
const { path, name } = await c.req.json();
|
|
@@ -7072,9 +7931,11 @@ function createApp(store = new Store) {
|
|
|
7072
7931
|
return c.json(store.reorderProjects(ids));
|
|
7073
7932
|
});
|
|
7074
7933
|
app.patch("/v1/projects/:id", async (c) => {
|
|
7075
|
-
const { pinned, name } = await c.req.json().catch(() => ({}));
|
|
7076
|
-
|
|
7077
|
-
|
|
7934
|
+
const { pinned, name, icon, color } = await c.req.json().catch(() => ({}));
|
|
7935
|
+
if (!store.project(c.req.param("id")))
|
|
7936
|
+
return c.json({ error: "not found" }, 404);
|
|
7937
|
+
const p = store.updateProject(c.req.param("id"), { pinned, name, icon, color });
|
|
7938
|
+
return p ? c.json(p) : c.json({ error: "icon is at most 4 characters; color is c1\u2026c7" }, 400);
|
|
7078
7939
|
});
|
|
7079
7940
|
app.delete("/v1/projects/:id", (c) => store.removeProject(c.req.param("id")) ? c.body(null, 204) : c.json({ error: "not found" }, 404));
|
|
7080
7941
|
app.get("/v1/fs/ls", (c) => {
|
|
@@ -7112,6 +7973,31 @@ function createApp(store = new Store) {
|
|
|
7112
7973
|
})
|
|
7113
7974
|
});
|
|
7114
7975
|
});
|
|
7976
|
+
app.get("/v1/policy", (c) => {
|
|
7977
|
+
const id = c.req.query("project");
|
|
7978
|
+
const p = id ? store.project(id) : null;
|
|
7979
|
+
if (id && !p)
|
|
7980
|
+
return c.json({ error: "unknown project" }, 404);
|
|
7981
|
+
const { provenance, overridden, policy: policy2 } = store.policyFor(p?.root ?? null);
|
|
7982
|
+
return c.json({ ...policy2, provenance, overridden });
|
|
7983
|
+
});
|
|
7984
|
+
app.get("/v1/audit", (c) => {
|
|
7985
|
+
const since = sinceToIso(c.req.query("since"));
|
|
7986
|
+
if (c.req.query("since") && !since)
|
|
7987
|
+
return c.json({ error: "since: use 30d / 12h / 90m or an ISO date" }, 400);
|
|
7988
|
+
const pid = c.req.query("project") || null;
|
|
7989
|
+
if (pid && !store.project(pid))
|
|
7990
|
+
return c.json({ error: "unknown project" }, 404);
|
|
7991
|
+
const rows = store.audit({
|
|
7992
|
+
since,
|
|
7993
|
+
projectId: pid,
|
|
7994
|
+
type: c.req.query("type") || null,
|
|
7995
|
+
limit: Number(c.req.query("limit")) || undefined
|
|
7996
|
+
});
|
|
7997
|
+
const format = c.req.query("format") === "csv" ? "csv" : c.req.query("format") === "jsonl" ? "jsonl" : "json";
|
|
7998
|
+
const ct = format === "csv" ? "text/csv; charset=utf-8" : format === "jsonl" ? "application/x-ndjson" : "application/json";
|
|
7999
|
+
return c.body(formatAudit(rows, format), 200, { "content-type": ct });
|
|
8000
|
+
});
|
|
7115
8001
|
app.get("/v1/rules/dryrun", (c) => {
|
|
7116
8002
|
const projectId = c.req.query("project");
|
|
7117
8003
|
if (!projectId)
|
|
@@ -7125,11 +8011,11 @@ function createApp(store = new Store) {
|
|
|
7125
8011
|
});
|
|
7126
8012
|
app.post("/v1/incidents/ack", async (c) => {
|
|
7127
8013
|
const body = await c.req.json().catch(() => ({}));
|
|
7128
|
-
return c.json({ ok: true, acked: store.ackAllIncidents(body.project || undefined) });
|
|
8014
|
+
return c.json({ ok: true, acked: store.ackAllIncidents(body.project || undefined, body.by) });
|
|
7129
8015
|
});
|
|
7130
8016
|
app.post("/v1/incidents/:seq/ack", (c) => {
|
|
7131
8017
|
const seq = Number(c.req.param("seq"));
|
|
7132
|
-
if (!Number.isInteger(seq) || !store.ackIncident(seq))
|
|
8018
|
+
if (!Number.isInteger(seq) || !store.ackIncident(seq, c.req.query("by")))
|
|
7133
8019
|
return c.json({ ok: false, error: "no such incident" }, 404);
|
|
7134
8020
|
return c.json({ ok: true });
|
|
7135
8021
|
});
|
|
@@ -7377,7 +8263,7 @@ function createApp(store = new Store) {
|
|
|
7377
8263
|
const b = await c.req.json();
|
|
7378
8264
|
if (!b.projectId || !b.task)
|
|
7379
8265
|
return c.json({ error: "projectId and task required" }, 400);
|
|
7380
|
-
const r = store.claim(b.projectId, b.task, b.owner ?? "cli", b.baseRef);
|
|
8266
|
+
const r = store.claim(b.projectId, b.task, b.owner ?? "cli", b.baseRef, b.sessionId ?? null);
|
|
7381
8267
|
return c.json(r, r.ok ? 201 : 409);
|
|
7382
8268
|
});
|
|
7383
8269
|
app.post("/v1/claims/renew", async (c) => {
|
|
@@ -7524,6 +8410,7 @@ function createApp(store = new Store) {
|
|
|
7524
8410
|
const raw2 = await c.req.json().catch(() => ({}));
|
|
7525
8411
|
store.ingestHook(event, raw2);
|
|
7526
8412
|
if (event === "SessionStart" && typeof raw2.cwd === "string") {
|
|
8413
|
+
store.checkPolicy(raw2.cwd, typeof raw2.session_id === "string" ? raw2.session_id : null);
|
|
7527
8414
|
const ctx = store.sessionContext(raw2.cwd);
|
|
7528
8415
|
if (ctx)
|
|
7529
8416
|
return c.json({
|
|
@@ -7533,7 +8420,7 @@ function createApp(store = new Store) {
|
|
|
7533
8420
|
}
|
|
7534
8421
|
const sid = typeof raw2.session_id === "string" ? raw2.session_id : null;
|
|
7535
8422
|
const answers = event === "UserPromptSubmit" || event === "PreToolUse" || event === "PostToolUse" ? store.answerContext(sid) : null;
|
|
7536
|
-
if (event === "PreToolUse" &&
|
|
8423
|
+
if (event === "PreToolUse" && !store.guardDisabled(hookRepoRoot(store, raw2))) {
|
|
7537
8424
|
const guard = store.guardHook(raw2);
|
|
7538
8425
|
if (guard) {
|
|
7539
8426
|
return c.json({
|
|
@@ -7609,6 +8496,7 @@ function serve() {
|
|
|
7609
8496
|
}
|
|
7610
8497
|
var server = serve();
|
|
7611
8498
|
var port = server.port ?? DEFAULT_PORT2;
|
|
8499
|
+
ensureToken();
|
|
7612
8500
|
writeDaemonInfo({ port, pid: process.pid, version: VERSION, startedAt: new Date().toISOString() });
|
|
7613
8501
|
var backfillDays = Number(process.env.SWARM_CODEX_BACKFILL_DAYS ?? 30);
|
|
7614
8502
|
var backfillMs = backfillDays * 24 * 60 * 60000;
|