@join-pantheon/cli 0.29.1 → 0.31.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.
Files changed (57) hide show
  1. package/README.md +142 -40
  2. package/bin/pantheon.js +12 -0
  3. package/dist/accounts.js +151 -26
  4. package/dist/agents.js +224 -17
  5. package/dist/claude-confine.js +299 -0
  6. package/dist/cli.js +1116 -137
  7. package/dist/client.js +294 -10
  8. package/dist/command-state.js +102 -0
  9. package/dist/config.js +287 -14
  10. package/dist/continued.js +42 -10
  11. package/dist/doctor.js +191 -23
  12. package/dist/file-lock.js +197 -0
  13. package/dist/grants.js +1 -1
  14. package/dist/history-view.js +2 -1
  15. package/dist/home-guard.js +565 -0
  16. package/dist/hooks-install.js +2 -1
  17. package/dist/identity.js +101 -1
  18. package/dist/install.js +76 -26
  19. package/dist/kiro-files.js +2 -2
  20. package/dist/ledger.js +111 -28
  21. package/dist/machine.js +494 -32
  22. package/dist/mcp-supervisor.js +29 -7
  23. package/dist/mcp-windows.js +47 -25
  24. package/dist/mcp.js +99 -36
  25. package/dist/network.js +39 -8
  26. package/dist/nonces.js +130 -0
  27. package/dist/notify.js +26 -9
  28. package/dist/pending.js +121 -22
  29. package/dist/persona.js +21 -0
  30. package/dist/pins.js +161 -0
  31. package/dist/profile.js +22 -1
  32. package/dist/protocol.js +40 -1
  33. package/dist/quickstart.js +173 -24
  34. package/dist/reads.js +37 -0
  35. package/dist/refusal.js +14 -1
  36. package/dist/relay.js +1341 -207
  37. package/dist/repo-find.js +127 -0
  38. package/dist/repo-route.js +70 -0
  39. package/dist/roster-file.js +190 -25
  40. package/dist/run-failure.js +99 -3
  41. package/dist/secret-guard.js +141 -0
  42. package/dist/service.js +91 -35
  43. package/dist/setup-guide.js +91 -31
  44. package/dist/spend.js +6 -2
  45. package/dist/store.js +94 -10
  46. package/dist/text-file.js +74 -0
  47. package/dist/tray.js +24 -3
  48. package/dist/untrusted.js +24 -5
  49. package/dist/update-check.js +2 -0
  50. package/dist/upgrade.js +9 -5
  51. package/dist/verify-act.js +102 -0
  52. package/dist/which.js +69 -12
  53. package/dist/worker-fleet.js +30 -9
  54. package/dist/worker-policy.js +231 -7
  55. package/dist/worker.js +1717 -130
  56. package/helper/macos/menubar.swift +63 -2
  57. package/package.json +2 -2
@@ -0,0 +1,565 @@
1
+ import { createHash } from "node:crypto";
2
+ import { chmodSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+ import { parseJsonText } from "./text-file.js";
5
+ import { withFileLock } from "./file-lock.js";
6
+ import { writeWhole } from "./pins.js";
7
+ export const POLICY_FILE = "worker-policy.json";
8
+ export const PROFILE_FILE = "profile.json";
9
+ export const HUD_FILE = join("bin", ".hud-action.json");
10
+ export const CONFIG_FILE = "config.json";
11
+ export const CONTINUED_FILE = "continued.json";
12
+ export const CURSOR_FILE = "worker-cursor.json";
13
+ export const PINS_FILE = "keys.json";
14
+ export const NONCES_FILE = "nonces.json";
15
+ export const GUARDED_FILES = [POLICY_FILE, CONFIG_FILE, PROFILE_FILE, HUD_FILE, CONTINUED_FILE, CURSOR_FILE, PINS_FILE, NONCES_FILE];
16
+ const sha = (b) => createHash("sha256").update(b).digest("hex");
17
+ const read = (f) => { try {
18
+ return statSync(f).isFile() ? readFileSync(f) : null;
19
+ }
20
+ catch {
21
+ return null;
22
+ } };
23
+ const parse = (b) => {
24
+ try {
25
+ const v = b ? parseJsonText(b.toString("utf8")) : undefined;
26
+ return v && typeof v === "object" && !Array.isArray(v) ? v : undefined;
27
+ }
28
+ catch {
29
+ return undefined;
30
+ }
31
+ };
32
+ const trusted = new Map();
33
+ const runBaseline = new Map();
34
+ export function trustWrite(file, prev) {
35
+ const k = resolve(file);
36
+ if (prev !== undefined && runBaseline.has(k)) {
37
+ const ph = prev ? sha(prev) : null;
38
+ if (ph !== runBaseline.get(k) && !trusted.get(k)?.has(ph ?? "absent"))
39
+ return;
40
+ }
41
+ const b = read(file);
42
+ const set = trusted.get(k) ?? new Set();
43
+ set.add(b ? sha(b) : "absent");
44
+ if (set.size > 50)
45
+ set.delete(set.values().next().value);
46
+ trusted.set(k, set);
47
+ }
48
+ export function snapshotHome(home, files = GUARDED_FILES) {
49
+ const s = { home, at: Date.now(), files: files.map((rel) => { const b = read(join(home, rel)); return { rel, bytes: b, hash: b ? sha(b) : null }; }) };
50
+ for (const f of s.files)
51
+ runBaseline.set(resolve(join(home, f.rel)), f.hash);
52
+ return s;
53
+ }
54
+ export function changedKeys(a, b, prefix = "", depth = 0) {
55
+ const isObj = (x) => !!x && typeof x === "object" && !Array.isArray(x);
56
+ if (!isObj(a) || !isObj(b) || depth >= 4)
57
+ return JSON.stringify(a) === JSON.stringify(b) ? [] : [prefix || "(the whole file)"];
58
+ const out = [];
59
+ for (const k of new Set([...Object.keys(a), ...Object.keys(b)])) {
60
+ const p = prefix ? `${prefix}.${k}` : k;
61
+ if (!(k in a))
62
+ out.push(`${p} added`);
63
+ else if (!(k in b))
64
+ out.push(`${p} removed`);
65
+ else
66
+ out.push(...changedKeys(a[k], b[k], p, depth + 1));
67
+ }
68
+ return out;
69
+ }
70
+ const obj = (x) => (x && typeof x === "object" && !Array.isArray(x) ? x : {});
71
+ const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
72
+ export const askedHere = new Set();
73
+ export function narrowPolicy(before, now, keepAsks = askedHere) {
74
+ const b = obj(before);
75
+ const keys = [];
76
+ const out = { ...now };
77
+ const bg = obj(b.grants), ng = obj(now.grants);
78
+ const grants = {};
79
+ for (const [cap, g] of Object.entries(ng)) {
80
+ if (!(cap in bg)) {
81
+ keys.push(`grants.${cap} added`);
82
+ continue;
83
+ }
84
+ if (!same(bg[cap], g)) {
85
+ keys.push(`grants.${cap} changed`);
86
+ grants[cap] = bg[cap];
87
+ continue;
88
+ }
89
+ grants[cap] = g;
90
+ }
91
+ out.grants = grants;
92
+ const bw = obj(b.wakers), nw = obj(now.wakers);
93
+ const wakers = {};
94
+ for (const [team, list] of Object.entries(nw)) {
95
+ const had = new Set(Array.isArray(bw[team]) ? bw[team].filter((x) => typeof x === "string") : []);
96
+ const keep = (Array.isArray(list) ? list : []).filter((m) => typeof m === "string" && had.has(m));
97
+ const added = (Array.isArray(list) ? list : []).filter((m) => typeof m === "string" && !had.has(m));
98
+ if (added.length)
99
+ keys.push(`wakers.${team} added ${added.length}`);
100
+ if (keep.length)
101
+ wakers[team] = keep;
102
+ }
103
+ if (Object.keys(wakers).length || "wakers" in now)
104
+ out.wakers = wakers;
105
+ const bd = obj(b.denied), nd = obj(now.denied);
106
+ const denied = { ...nd };
107
+ for (const [cap, at] of Object.entries(bd))
108
+ if (!(cap in nd)) {
109
+ denied[cap] = at;
110
+ keys.push(`denied.${cap} removed`);
111
+ }
112
+ out.denied = denied;
113
+ const bk = obj(b.asks), nk = obj(now.asks);
114
+ const pending = {};
115
+ for (const [cap, q] of Object.entries(nk)) {
116
+ if (!(cap in bk)) {
117
+ keys.push(`asks.${cap} added`);
118
+ continue;
119
+ }
120
+ if (!same(bk[cap], q)) {
121
+ keys.push(`asks.${cap} changed`);
122
+ pending[cap] = bk[cap];
123
+ continue;
124
+ }
125
+ pending[cap] = q;
126
+ }
127
+ if ("asks" in now || Object.keys(pending).length)
128
+ out.asks = pending;
129
+ const bo = obj(b.wakeOnce), no = obj(now.wakeOnce);
130
+ const once = {};
131
+ for (const [k, n] of Object.entries(no)) {
132
+ const was = typeof bo[k] === "number" ? bo[k] : 0;
133
+ if (typeof n === "number" && n > was) {
134
+ keys.push(`wakeOnce.${k} raised`);
135
+ if (was > 0)
136
+ once[k] = was;
137
+ continue;
138
+ }
139
+ once[k] = n;
140
+ }
141
+ if ("wakeOnce" in now || Object.keys(once).length)
142
+ out.wakeOnce = once;
143
+ const bn = obj(b.wakeNo), nn = obj(now.wakeNo);
144
+ const wakeNo = { ...nn };
145
+ for (const [k, at] of Object.entries(bn)) {
146
+ if (!(k in nn) || (typeof at === "number" && typeof nn[k] === "number" && nn[k] < at)) {
147
+ wakeNo[k] = at;
148
+ keys.push(`wakeNo.${k} lifted`);
149
+ }
150
+ }
151
+ const ba = obj(b.wakeAsks), na = obj(now.wakeAsks);
152
+ const asks = {};
153
+ for (const [id, q] of Object.entries(na)) {
154
+ if (!(id in ba)) {
155
+ if (keepAsks.has(id)) {
156
+ asks[id] = q;
157
+ continue;
158
+ }
159
+ keys.push(`wakeAsks.${id} added`);
160
+ continue;
161
+ }
162
+ if (!same(ba[id], q)) {
163
+ keys.push(`wakeAsks.${id} changed`);
164
+ asks[id] = ba[id];
165
+ continue;
166
+ }
167
+ asks[id] = q;
168
+ }
169
+ if ("wakeAsks" in now || Object.keys(asks).length)
170
+ out.wakeAsks = asks;
171
+ for (const f of ["wakersCarried", "wakersDeferred"]) {
172
+ const bf = obj(b[f]), nf = obj(now[f]);
173
+ const merged = { ...nf };
174
+ let lost = 0;
175
+ for (const [k, v] of Object.entries(bf))
176
+ if (!(k in nf) || !same(nf[k], v)) {
177
+ merged[k] = v;
178
+ lost++;
179
+ }
180
+ if (lost) {
181
+ out[f] = merged;
182
+ keys.push(`${f}: ${lost} removed`);
183
+ }
184
+ }
185
+ const bt = obj(b.wakeTeamFlag), nt = obj(now.wakeTeamFlag);
186
+ const flags = {};
187
+ for (const [k, v] of Object.entries(nt)) {
188
+ if (!(k in bt)) {
189
+ keys.push(`wakeTeamFlag.${k} added`);
190
+ continue;
191
+ }
192
+ flags[k] = v;
193
+ }
194
+ if ("wakeTeamFlag" in now)
195
+ out.wakeTeamFlag = flags;
196
+ if ("wakeNo" in now || Object.keys(wakeNo).length)
197
+ out.wakeNo = wakeNo;
198
+ return { policy: out, keys };
199
+ }
200
+ export function narrowConfig(before, now) {
201
+ const keys = [];
202
+ const out = { ...now };
203
+ if (typeof before.relay === "string" && now.relay !== before.relay) {
204
+ out.relay = before.relay;
205
+ keys.push("relay changed");
206
+ }
207
+ const bs = obj(before.spaces), ns = obj(now.spaces);
208
+ const spaces = { ...ns };
209
+ for (const [id, m] of Object.entries(ns)) {
210
+ const was = obj(bs[id]);
211
+ if (typeof was.relay === "string" && obj(m).relay !== was.relay) {
212
+ spaces[id] = { ...obj(m), relay: was.relay };
213
+ keys.push(`spaces.${id}.relay changed`);
214
+ }
215
+ }
216
+ for (const [id, was] of Object.entries(bs)) {
217
+ const w = obj(was), m = obj(spaces[id]);
218
+ if (id in spaces && w.since !== undefined && m.since !== w.since) {
219
+ spaces[id] = { ...m, since: w.since };
220
+ keys.push(`spaces.${id}.since changed`);
221
+ }
222
+ }
223
+ const ids = new Set(Object.values(bs).map((m) => obj(m).id).filter((x) => typeof x === "string"));
224
+ for (const id of Object.keys(spaces))
225
+ if (!(id in bs)) {
226
+ delete spaces[id];
227
+ keys.push(`spaces.${id} added${ids.has(obj(ns[id]).id) ? " (a second entry for an existing team)" : ""}`);
228
+ }
229
+ for (const [id, m] of Object.entries(spaces)) {
230
+ const w = obj(bs[id]);
231
+ if (w.id !== undefined && obj(m).id !== w.id) {
232
+ spaces[id] = { ...obj(m), id: w.id };
233
+ keys.push(`spaces.${id}.id changed`);
234
+ }
235
+ if (w.name !== undefined && obj(spaces[id]).name !== w.name) {
236
+ spaces[id] = { ...obj(spaces[id]), name: w.name };
237
+ keys.push(`spaces.${id}.name changed`);
238
+ }
239
+ }
240
+ if ("spaces" in now)
241
+ out.spaces = spaces;
242
+ if (now.default_space !== before.default_space) {
243
+ out.default_space = before.default_space;
244
+ keys.push("default_space changed");
245
+ }
246
+ if (!same(obj(now.network).proxy, obj(before.network).proxy)) {
247
+ out.network = { ...obj(now.network), proxy: obj(before.network).proxy };
248
+ if (obj(before.network).proxy === undefined)
249
+ delete out.network.proxy;
250
+ keys.push("network.proxy changed");
251
+ }
252
+ const bb = obj(before.bindings), nb = obj(now.bindings);
253
+ const bindings = {};
254
+ for (const [k, v] of Object.entries(nb)) {
255
+ if (!(k in bb)) {
256
+ keys.push(`bindings.${k} added`);
257
+ continue;
258
+ }
259
+ if (!same(bb[k], v)) {
260
+ keys.push(`bindings.${k} changed`);
261
+ bindings[k] = bb[k];
262
+ continue;
263
+ }
264
+ bindings[k] = v;
265
+ }
266
+ if ("bindings" in now || Object.keys(bindings).length)
267
+ out.bindings = bindings;
268
+ const bu = Array.isArray(before.unbound) ? before.unbound : [];
269
+ const nu = Array.isArray(now.unbound) ? now.unbound : [];
270
+ const lifted = bu.filter((x) => !nu.includes(x));
271
+ if (lifted.length) {
272
+ out.unbound = [...nu, ...lifted];
273
+ keys.push(`unbound: ${lifted.length} taken off`);
274
+ }
275
+ return { config: out, keys };
276
+ }
277
+ export function narrowPins(before, now) {
278
+ const b = obj(before), n = obj(now);
279
+ const keys = [];
280
+ const bt = obj(b.teams), nt = obj(n.teams);
281
+ const teams = {};
282
+ for (const id of new Set([...Object.keys(bt), ...Object.keys(nt)])) {
283
+ const was = obj(bt[id]), is = obj(nt[id]);
284
+ const wm = obj(was.members), im = obj(is.members);
285
+ const members = {};
286
+ for (const [m, pin] of Object.entries(im)) {
287
+ if (!(m in wm)) {
288
+ keys.push(`${id}.${m} pinned during the run`);
289
+ continue;
290
+ }
291
+ const w = obj(wm[m]), p = obj(pin);
292
+ const pendingOk = same(p.pending ?? null, w.pending ?? null);
293
+ if (p.fp !== w.fp || p.key !== w.key) {
294
+ members[m] = { ...w };
295
+ keys.push(`${id}.${m} key changed`);
296
+ continue;
297
+ }
298
+ if (!pendingOk) {
299
+ const { pending: _p, ...rest } = obj(pin);
300
+ members[m] = { ...rest, ...(w.pending ? { pending: w.pending } : {}) };
301
+ keys.push(`${id}.${m} held-back key changed during the run`);
302
+ continue;
303
+ }
304
+ if (w.signs && !p.signs) {
305
+ members[m] = { ...p, signs: w.signs };
306
+ keys.push(`${id}.${m} signing forgotten`);
307
+ continue;
308
+ }
309
+ members[m] = pin;
310
+ }
311
+ for (const [m, pin] of Object.entries(wm))
312
+ if (!(m in im)) {
313
+ members[m] = pin;
314
+ keys.push(`${id}.${m} unpinned`);
315
+ }
316
+ const t = { ...is, members };
317
+ if (was.sigs && !is.sigs) {
318
+ t.sigs = was.sigs;
319
+ keys.push(`${id}: signatures turned off`);
320
+ }
321
+ if (is.spaceId !== was.spaceId) {
322
+ if (was.spaceId === undefined)
323
+ delete t.spaceId;
324
+ else
325
+ t.spaceId = was.spaceId;
326
+ keys.push(`${id}: team id changed`);
327
+ }
328
+ teams[id] = t;
329
+ }
330
+ return { value: { ...n, v: 1, teams }, keys };
331
+ }
332
+ export function narrowNonces(before, now) {
333
+ const b = obj(before), n = obj(now);
334
+ const out = { ...n };
335
+ let lost = 0;
336
+ for (const [team, entries] of Object.entries(b)) {
337
+ const cur = { ...obj(n[team]) };
338
+ const forward = (c, w) => same(c, w) || (Array.isArray(c) && Array.isArray(w) && c[0] === w[0] && c[1] === w[1]
339
+ && (c[2] ?? null) === (w[2] ?? null) && typeof c[3] === "number" && (w[3] === undefined || w[3] === c[3]));
340
+ for (const [k, v] of Object.entries(obj(entries)))
341
+ if (!(k in cur) || !forward(cur[k], v)) {
342
+ cur[k] = v;
343
+ lost++;
344
+ }
345
+ out[team] = cur;
346
+ }
347
+ return { value: out, keys: lost ? [`${lost} seen request${lost === 1 ? "" : "s"} forgotten`] : [] };
348
+ }
349
+ const WAKE_ORDER = { no: 0, approved: 1, anyone: 2 };
350
+ export function narrowProfile(before, now) {
351
+ const bw = obj(obj(before).agent).wake_default, nw = obj(now.agent).wake_default;
352
+ const rank = (v) => (typeof v === "string" && v in WAKE_ORDER ? WAKE_ORDER[v] : WAKE_ORDER.anyone);
353
+ const agent = { ...obj(now.agent) };
354
+ const keys = [];
355
+ const ba = obj(obj(before).agent);
356
+ if (bw !== undefined && nw === undefined) {
357
+ agent.wake_default = bw;
358
+ keys.push("agent.wake_default removed");
359
+ }
360
+ else if (rank(nw) > rank(bw)) {
361
+ if (bw === undefined)
362
+ delete agent.wake_default;
363
+ else
364
+ agent.wake_default = bw;
365
+ keys.push(`agent.wake_default loosened to ${String(nw)}`);
366
+ }
367
+ if (agent.allow_bash === true && ba.allow_bash !== true) {
368
+ if (ba.allow_bash === undefined)
369
+ delete agent.allow_bash;
370
+ else
371
+ agent.allow_bash = ba.allow_bash;
372
+ keys.push("agent.allow_bash turned on");
373
+ }
374
+ else if (ba.allow_bash === false && agent.allow_bash !== false) {
375
+ agent.allow_bash = false;
376
+ keys.push("agent.allow_bash no removed");
377
+ }
378
+ if (agent.wake_enabled === true && ba.wake_enabled === false) {
379
+ agent.wake_enabled = false;
380
+ keys.push("agent.wake_enabled turned on");
381
+ }
382
+ if (!same(agent.primary_surface, ba.primary_surface) && ba.primary_surface !== undefined) {
383
+ agent.primary_surface = ba.primary_surface;
384
+ keys.push("agent.primary_surface changed");
385
+ }
386
+ return keys.length ? { profile: { ...now, agent }, keys } : { profile: now, keys: [] };
387
+ }
388
+ export function restoreHome(s) {
389
+ const out = [];
390
+ const takesFiles = () => { const p = join(s.home, `.write-check.${process.pid}.${Date.now()}`); try {
391
+ writeFileSync(p, "", { flag: "wx", mode: 0o600 });
392
+ rmSync(p, { force: true });
393
+ return true;
394
+ }
395
+ catch {
396
+ return false;
397
+ } };
398
+ if (!takesFiles()) {
399
+ try {
400
+ chmodSync(s.home, 0o700);
401
+ }
402
+ catch { }
403
+ out.push(takesFiles() ? { rel: "the Pantheon folder", what: "reverted", keys: ["made read-only"] } : { rel: "the Pantheon folder", what: "reported", keys: ["cannot take new files — fix its permissions"] });
404
+ }
405
+ for (const f of s.files)
406
+ runBaseline.delete(resolve(join(s.home, f.rel)));
407
+ for (const f of s.files) {
408
+ const file = join(s.home, f.rel);
409
+ const now = read(file);
410
+ const hashNow = now ? sha(now) : null;
411
+ if (hashNow === f.hash)
412
+ continue;
413
+ const alwaysNarrow = f.rel === POLICY_FILE || f.rel === PROFILE_FILE || f.rel === CONFIG_FILE || f.rel === CONTINUED_FILE || f.rel === CURSOR_FILE || f.rel === PINS_FILE || f.rel === NONCES_FILE;
414
+ if (!alwaysNarrow && trusted.get(resolve(file))?.has(hashNow ?? "absent"))
415
+ continue;
416
+ try {
417
+ if (f.rel === HUD_FILE) {
418
+ if (now) {
419
+ rmSync(file, { force: true });
420
+ out.push({ rel: f.rel, what: "removed", keys: [] });
421
+ }
422
+ continue;
423
+ }
424
+ try {
425
+ if (!statSync(file).isFile())
426
+ rmSync(file, { recursive: true, force: true });
427
+ }
428
+ catch { }
429
+ const a = parse(f.bytes), b = parse(now);
430
+ if (f.rel === POLICY_FILE) {
431
+ if (!b) {
432
+ if (f.bytes) {
433
+ writeFileSync(file, f.bytes, { mode: 0o600 });
434
+ out.push({ rel: f.rel, what: "reverted", keys: ["(the whole file)"] });
435
+ }
436
+ continue;
437
+ }
438
+ const n = narrowPolicy(a, b);
439
+ if (n.keys.length) {
440
+ writeFileSync(file, JSON.stringify(n.policy, null, 2) + "\n", { mode: 0o600 });
441
+ trustWrite(file);
442
+ out.push({ rel: f.rel, what: "reverted", keys: n.keys });
443
+ }
444
+ continue;
445
+ }
446
+ const locked = (fn) => {
447
+ if (!(f.rel === PINS_FILE || f.rel === NONCES_FILE || f.rel === CONTINUED_FILE))
448
+ return fn();
449
+ let ran = false;
450
+ try {
451
+ withFileLock(file, () => { ran = true; fn(); });
452
+ }
453
+ catch (e) {
454
+ if (ran)
455
+ throw e;
456
+ fn();
457
+ }
458
+ };
459
+ const put = (text) => { try {
460
+ writeWhole(file, text);
461
+ }
462
+ catch {
463
+ writeFileSync(file, text, { mode: 0o600 });
464
+ } };
465
+ if (f.rel === PINS_FILE || f.rel === NONCES_FILE) {
466
+ locked(() => {
467
+ const cur = parse(read(file));
468
+ const n = f.rel === PINS_FILE ? narrowPins(a, cur) : narrowNonces(a, cur);
469
+ if (n.keys.length) {
470
+ put(JSON.stringify(n.value, null, 2) + "\n");
471
+ trustWrite(file);
472
+ out.push({ rel: f.rel, what: "reverted", keys: n.keys });
473
+ }
474
+ });
475
+ continue;
476
+ }
477
+ if (f.rel === CONTINUED_FILE || f.rel === CURSOR_FILE)
478
+ locked(() => {
479
+ const before = obj(a), now = obj(parse(read(file)));
480
+ const merged = { ...now };
481
+ let lost = 0;
482
+ for (const [k, v] of Object.entries(before)) {
483
+ if (f.rel === CURSOR_FILE) {
484
+ if (typeof v === "number" && !(typeof now[k] === "number" && now[k] >= v)) {
485
+ merged[k] = v;
486
+ lost++;
487
+ }
488
+ }
489
+ else if (Array.isArray(v)) {
490
+ const cur = Array.isArray(now[k]) ? now[k] : [];
491
+ const back = v.filter((x) => !cur.includes(x));
492
+ if (back.length) {
493
+ merged[k] = [...back, ...cur];
494
+ lost += back.length;
495
+ }
496
+ }
497
+ }
498
+ let sentDropped = 0;
499
+ if (f.rel === CONTINUED_FILE)
500
+ for (const [k, v] of Object.entries(merged)) {
501
+ if (!k.endsWith(" sent") || !Array.isArray(v))
502
+ continue;
503
+ const was = Array.isArray(before[k]) ? before[k] : [];
504
+ const kept = v.filter((x) => was.includes(x));
505
+ if (kept.length !== v.length) {
506
+ merged[k] = kept;
507
+ sentDropped += v.length - kept.length;
508
+ }
509
+ }
510
+ if (sentDropped && !lost) {
511
+ put(JSON.stringify(merged) + "\n");
512
+ trustWrite(file);
513
+ }
514
+ if (lost) {
515
+ put(JSON.stringify(merged) + "\n");
516
+ trustWrite(file);
517
+ out.push({ rel: f.rel, what: "reverted", keys: [f.rel === CURSOR_FILE ? "read position moved back" : `${lost} handled request${lost === 1 ? "" : "s"} unmarked`] });
518
+ }
519
+ });
520
+ if (f.rel === CONTINUED_FILE || f.rel === CURSOR_FILE)
521
+ continue;
522
+ if (f.rel === PROFILE_FILE) {
523
+ if (!b)
524
+ continue;
525
+ const n = narrowProfile(a, b);
526
+ if (n.keys.length) {
527
+ writeFileSync(file, JSON.stringify(n.profile, null, 2) + "\n", { mode: 0o600 });
528
+ trustWrite(file);
529
+ out.push({ rel: f.rel, what: "reverted", keys: n.keys });
530
+ }
531
+ continue;
532
+ }
533
+ if (a && b) {
534
+ const n = narrowConfig(a, b);
535
+ if (n.keys.length) {
536
+ writeFileSync(file, JSON.stringify(n.config, null, 2) + "\n", { mode: 0o600 });
537
+ trustWrite(file);
538
+ out.push({ rel: f.rel, what: "reverted", keys: n.keys });
539
+ }
540
+ const rest = changedKeys(a, n.config).filter((k) => !/privateKey|token/i.test(k) && !/^spaces\.[^.]+\.repos\b/.test(k)).slice(0, 20);
541
+ if (rest.length)
542
+ out.push({ rel: f.rel, what: "reported", keys: rest });
543
+ }
544
+ else if (a && !b && now) {
545
+ writeFileSync(file, f.bytes, { mode: 0o600 });
546
+ trustWrite(file);
547
+ out.push({ rel: f.rel, what: "reverted", keys: ["(the whole file)"] });
548
+ }
549
+ else
550
+ out.push({ rel: f.rel, what: "reported", keys: [] });
551
+ }
552
+ catch {
553
+ out.push({ rel: f.rel, what: "reported", keys: ["(could not be checked)"] });
554
+ }
555
+ }
556
+ return out;
557
+ }
558
+ export function describeChange(c) {
559
+ if (c.what === "removed")
560
+ return `${c.rel} was written while the run could write it — removed, not acted on`;
561
+ if (c.what === "reported")
562
+ return `${c.rel} changed while the run was going${c.keys.length ? ` (${c.keys.join(", ")})` : ""} — left as it is; if you did not change it, check it`;
563
+ return `${c.rel}: what was widened while the run was going was taken back (${c.keys.join(", ")}) — if you did that yourself, do it again`;
564
+ }
565
+ //# sourceMappingURL=home-guard.js.map
@@ -3,6 +3,7 @@ import { homedir } from "node:os";
3
3
  import { dirname, join } from "node:path";
4
4
  import { MAX_CONSECUTIVE_BLOCKS, isOurHookCommand } from "./notify.js";
5
5
  import { copilotHome, kiroHome } from "./agents.js";
6
+ import { parseJsonText } from "./text-file.js";
6
7
  import { KIRO_AGENT_NAME, kiroAgentFile, kiroAgentDoc, readKiroAgent, writeKiroAgent, kiroDefaultIsPantheon, gitTracks } from "./kiro-files.js";
7
8
  function ours(cmd) {
8
9
  return isOurHookCommand(cmd);
@@ -11,7 +12,7 @@ function readJson(file) {
11
12
  if (!existsSync(file))
12
13
  return null;
13
14
  try {
14
- return JSON.parse(readFileSync(file, "utf8"));
15
+ return parseJsonText(readFileSync(file, "utf8"));
15
16
  }
16
17
  catch {
17
18
  return null;
package/dist/identity.js CHANGED
@@ -1,4 +1,4 @@
1
- import { generateKeyPairSync, sign, verify, createPublicKey, randomBytes } from "node:crypto";
1
+ import { generateKeyPairSync, sign, verify, createPublicKey, randomBytes, createHash } from "node:crypto";
2
2
  export function newKeypair() {
3
3
  const { publicKey, privateKey } = generateKeyPairSync("ed25519");
4
4
  return {
@@ -64,8 +64,108 @@ export function verifyAccount(publicKeyB64, method, path, params, ts, signatureB
64
64
  return false;
65
65
  }
66
66
  }
67
+ function readPayload(method, path, canonical) {
68
+ return Buffer.from(`pantheon-read/1\n${method}\n${path}\n${canonical}`, "utf8");
69
+ }
70
+ export function canonicalQuery(params) {
71
+ return Object.entries(params)
72
+ .filter(([k, v]) => v !== undefined && k !== "sig")
73
+ .map(([k, v]) => [encodeURIComponent(k), encodeURIComponent(String(v))])
74
+ .sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
75
+ .map(([k, v]) => `${k}=${v}`)
76
+ .join("&");
77
+ }
78
+ export function signRead(privateKeyB64, method, path, params) {
79
+ const key = { key: Buffer.from(privateKeyB64, "base64"), format: "der", type: "pkcs8" };
80
+ return sign(null, readPayload(method, path, canonicalQuery(params)), key).toString("base64");
81
+ }
82
+ export function verifyRead(publicKeyB64, method, path, params, signatureB64) {
83
+ try {
84
+ const key = createPublicKey({ key: Buffer.from(publicKeyB64, "base64"), format: "der", type: "spki" });
85
+ return verify(null, readPayload(method, path, canonicalQuery(params)), key, Buffer.from(signatureB64, "base64"));
86
+ }
87
+ catch {
88
+ return false;
89
+ }
90
+ }
91
+ export function canonicalKey(publicKeyB64) {
92
+ if (typeof publicKeyB64 !== "string" || publicKeyB64.length > 200)
93
+ return null;
94
+ try {
95
+ const raw = Buffer.from(publicKeyB64, "base64");
96
+ if (raw.length !== 44 || raw.toString("base64") !== publicKeyB64)
97
+ return null;
98
+ const k = createPublicKey({ key: raw, format: "der", type: "spki" });
99
+ if (k.asymmetricKeyType !== "ed25519")
100
+ return null;
101
+ const again = k.export({ type: "spki", format: "der" });
102
+ return again.equals(raw) ? publicKeyB64 : null;
103
+ }
104
+ catch {
105
+ return null;
106
+ }
107
+ }
67
108
  export function fingerprint(publicKeyB64) {
68
109
  const raw = Buffer.from(publicKeyB64, "base64").subarray(-16).toString("hex");
69
110
  return (raw.match(/.{4}/g) ?? []).join("-");
70
111
  }
112
+ const sha256hex = (s) => createHash("sha256").update(s, "utf8").digest("hex");
113
+ export function canonicalJson(v) {
114
+ if (v === null || typeof v !== "object")
115
+ return JSON.stringify(v ?? null);
116
+ if (Array.isArray(v))
117
+ return `[${v.map(canonicalJson).join(",")}]`;
118
+ const o = v;
119
+ return `{${Object.keys(o).filter((k) => o[k] !== undefined).sort().map((k) => `${JSON.stringify(k)}:${canonicalJson(o[k])}`).join(",")}}`;
120
+ }
121
+ export function newActId() { return `a_${Date.now().toString(36)}${randomBytes(9).toString("hex")}`; }
122
+ export function actPayload(f) {
123
+ const list = (a, none) => (a && a.length ? [...a].sort().join(",") : none);
124
+ return Buffer.from([
125
+ "pantheon-act/1", f.id, f.team, f.member, f.keyRole, f.nonce, String(f.ts),
126
+ f.act, f.goal ?? "-", f.ref ?? "-", f.to === null ? "*" : list(f.to, "*"),
127
+ f.defers_to ?? "-", list(f.affects, "-"), f.repo ?? "-",
128
+ f.summary ? sha256hex(f.summary) : "-",
129
+ sha256hex(f.body),
130
+ sha256hex(canonicalJson(f.meta ?? {})),
131
+ ].join("\n"), "utf8");
132
+ }
133
+ export function goalPayload(f) {
134
+ return Buffer.from([
135
+ "pantheon-goal/1", f.id, f.team, f.member, f.keyRole, f.nonce, String(f.ts),
136
+ sha256hex(f.title), sha256hex(f.acceptance), f.invite === null ? "*" : [...new Set(f.invite)].sort().join(",") || "-",
137
+ f.budget == null ? "-" : String(f.budget), f.deadline == null ? "-" : String(f.deadline), f.kind ?? "-",
138
+ ].join("\n"), "utf8");
139
+ }
140
+ export function delegatePayload(team, member, subFp, run, expires) {
141
+ return Buffer.from(`pantheon-delegate/1\n${team}\n${member}\nrun\n${subFp}\n${run}\n${expires}`, "utf8");
142
+ }
143
+ function privKey(b64) { return { key: Buffer.from(b64, "base64"), format: "der", type: "pkcs8" }; }
144
+ function pubKey(b64) { return createPublicKey({ key: Buffer.from(b64, "base64"), format: "der", type: "spki" }); }
145
+ export function signBytes(privateKeyB64, payload) {
146
+ return sign(null, payload, privKey(privateKeyB64)).toString("base64");
147
+ }
148
+ export function verifyBytes(publicKeyB64, payload, signatureB64) {
149
+ try {
150
+ return verify(null, payload, pubKey(publicKeyB64), Buffer.from(signatureB64, "base64"));
151
+ }
152
+ catch {
153
+ return false;
154
+ }
155
+ }
156
+ export function signDelegate(memberPrivateKeyB64, team, member, subPub, run, expires) {
157
+ return { pub: subPub, run, expires, sig: signBytes(memberPrivateKeyB64, delegatePayload(team, member, fingerprint(subPub), run, expires)) };
158
+ }
159
+ export function verifyDelegate(memberPublicKeyB64, team, member, cert, at = Date.now(), skewMs = 10 * 60_000) {
160
+ if (!cert || typeof cert.pub !== "string" || typeof cert.sig !== "string" || typeof cert.run !== "string" || !Number.isFinite(cert.expires))
161
+ return false;
162
+ if (!canonicalKey(cert.pub) || !canonicalKey(memberPublicKeyB64))
163
+ return false;
164
+ if (at > cert.expires + skewMs)
165
+ return false;
166
+ return verifyBytes(memberPublicKeyB64, delegatePayload(team, member, fingerprint(cert.pub), cert.run, cert.expires), cert.sig);
167
+ }
168
+ export function newActNonce() {
169
+ return randomBytes(16).toString("base64url");
170
+ }
71
171
  //# sourceMappingURL=identity.js.map