@join-pantheon/cli 0.30.0 → 0.31.1
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 +114 -35
- package/dist/agents.js +41 -17
- package/dist/claude-confine.js +37 -21
- package/dist/cli.js +414 -38
- package/dist/client.js +258 -9
- package/dist/command-state.js +25 -2
- package/dist/config.js +79 -7
- package/dist/continued.js +42 -10
- package/dist/doctor.js +93 -15
- package/dist/file-lock.js +197 -0
- package/dist/home-guard.js +372 -17
- package/dist/identity.js +77 -1
- package/dist/mcp-supervisor.js +5 -2
- package/dist/mcp.js +78 -30
- package/dist/nonces.js +130 -0
- package/dist/notifier.js +35 -0
- package/dist/notify.js +9 -3
- package/dist/pending.js +113 -8
- package/dist/pins.js +161 -0
- package/dist/profile.js +19 -1
- package/dist/protocol.js +40 -1
- package/dist/quickstart.js +94 -13
- package/dist/reads.js +8 -1
- package/dist/relay.js +208 -35
- package/dist/repo-find.js +127 -0
- package/dist/repo-route.js +70 -0
- package/dist/roster-file.js +21 -2
- package/dist/run-failure.js +16 -0
- package/dist/service.js +7 -1
- package/dist/setup-guide.js +70 -29
- package/dist/tray.js +55 -4
- package/dist/untrusted.js +24 -5
- package/dist/upgrade.js +9 -5
- package/dist/verify-act.js +102 -0
- package/dist/worker-fleet.js +30 -9
- package/dist/worker-policy.js +96 -6
- package/dist/worker.js +1059 -93
- package/helper/macos/menubar.swift +245 -105
- package/package.json +1 -1
package/dist/home-guard.js
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { chmodSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { parseJsonText } from "./text-file.js";
|
|
5
|
+
import { withFileLock } from "./file-lock.js";
|
|
6
|
+
import { writeWhole } from "./pins.js";
|
|
5
7
|
export const POLICY_FILE = "worker-policy.json";
|
|
6
8
|
export const PROFILE_FILE = "profile.json";
|
|
7
9
|
export const HUD_FILE = join("bin", ".hud-action.json");
|
|
8
10
|
export const CONFIG_FILE = "config.json";
|
|
9
|
-
export const
|
|
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];
|
|
10
16
|
const sha = (b) => createHash("sha256").update(b).digest("hex");
|
|
11
17
|
const read = (f) => { try {
|
|
12
|
-
return readFileSync(f);
|
|
18
|
+
return statSync(f).isFile() ? readFileSync(f) : null;
|
|
13
19
|
}
|
|
14
20
|
catch {
|
|
15
21
|
return null;
|
|
@@ -24,9 +30,15 @@ const parse = (b) => {
|
|
|
24
30
|
}
|
|
25
31
|
};
|
|
26
32
|
const trusted = new Map();
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
const runBaseline = new Map();
|
|
34
|
+
export function trustWrite(file, prev) {
|
|
29
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);
|
|
30
42
|
const set = trusted.get(k) ?? new Set();
|
|
31
43
|
set.add(b ? sha(b) : "absent");
|
|
32
44
|
if (set.size > 50)
|
|
@@ -34,7 +46,10 @@ export function trustWrite(file) {
|
|
|
34
46
|
trusted.set(k, set);
|
|
35
47
|
}
|
|
36
48
|
export function snapshotHome(home, files = GUARDED_FILES) {
|
|
37
|
-
|
|
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;
|
|
38
53
|
}
|
|
39
54
|
export function changedKeys(a, b, prefix = "", depth = 0) {
|
|
40
55
|
const isObj = (x) => !!x && typeof x === "object" && !Array.isArray(x);
|
|
@@ -54,7 +69,8 @@ export function changedKeys(a, b, prefix = "", depth = 0) {
|
|
|
54
69
|
}
|
|
55
70
|
const obj = (x) => (x && typeof x === "object" && !Array.isArray(x) ? x : {});
|
|
56
71
|
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
57
|
-
export
|
|
72
|
+
export const askedHere = new Set();
|
|
73
|
+
export function narrowPolicy(before, now, keepAsks = askedHere) {
|
|
58
74
|
const b = obj(before);
|
|
59
75
|
const keys = [];
|
|
60
76
|
const out = { ...now };
|
|
@@ -94,6 +110,91 @@ export function narrowPolicy(before, now) {
|
|
|
94
110
|
keys.push(`denied.${cap} removed`);
|
|
95
111
|
}
|
|
96
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;
|
|
97
198
|
return { policy: out, keys };
|
|
98
199
|
}
|
|
99
200
|
export function narrowConfig(before, now) {
|
|
@@ -112,32 +213,205 @@ export function narrowConfig(before, now) {
|
|
|
112
213
|
keys.push(`spaces.${id}.relay changed`);
|
|
113
214
|
}
|
|
114
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
|
+
}
|
|
115
240
|
if ("spaces" in now)
|
|
116
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
|
+
}
|
|
117
275
|
return { config: out, keys };
|
|
118
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
|
+
}
|
|
119
349
|
const WAKE_ORDER = { no: 0, approved: 1, anyone: 2 };
|
|
120
350
|
export function narrowProfile(before, now) {
|
|
121
351
|
const bw = obj(obj(before).agent).wake_default, nw = obj(now.agent).wake_default;
|
|
122
|
-
const rank = (v) => (typeof v === "string" && v in WAKE_ORDER ? WAKE_ORDER[v] : WAKE_ORDER.
|
|
123
|
-
if (rank(nw) <= rank(bw))
|
|
124
|
-
return { profile: now, keys: [] };
|
|
352
|
+
const rank = (v) => (typeof v === "string" && v in WAKE_ORDER ? WAKE_ORDER[v] : WAKE_ORDER.anyone);
|
|
125
353
|
const agent = { ...obj(now.agent) };
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
354
|
+
const keys = [];
|
|
355
|
+
const ba = obj(obj(before).agent);
|
|
356
|
+
if (bw !== undefined && nw === undefined) {
|
|
129
357
|
agent.wake_default = bw;
|
|
130
|
-
|
|
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: [] };
|
|
131
387
|
}
|
|
132
388
|
export function restoreHome(s) {
|
|
133
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)));
|
|
134
407
|
for (const f of s.files) {
|
|
135
408
|
const file = join(s.home, f.rel);
|
|
136
409
|
const now = read(file);
|
|
137
410
|
const hashNow = now ? sha(now) : null;
|
|
138
411
|
if (hashNow === f.hash)
|
|
139
412
|
continue;
|
|
140
|
-
|
|
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"))
|
|
141
415
|
continue;
|
|
142
416
|
try {
|
|
143
417
|
if (f.rel === HUD_FILE) {
|
|
@@ -147,6 +421,11 @@ export function restoreHome(s) {
|
|
|
147
421
|
}
|
|
148
422
|
continue;
|
|
149
423
|
}
|
|
424
|
+
try {
|
|
425
|
+
if (!statSync(file).isFile())
|
|
426
|
+
rmSync(file, { recursive: true, force: true });
|
|
427
|
+
}
|
|
428
|
+
catch { }
|
|
150
429
|
const a = parse(f.bytes), b = parse(now);
|
|
151
430
|
if (f.rel === POLICY_FILE) {
|
|
152
431
|
if (!b) {
|
|
@@ -164,6 +443,82 @@ export function restoreHome(s) {
|
|
|
164
443
|
}
|
|
165
444
|
continue;
|
|
166
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;
|
|
167
522
|
if (f.rel === PROFILE_FILE) {
|
|
168
523
|
if (!b)
|
|
169
524
|
continue;
|
|
@@ -182,7 +537,7 @@ export function restoreHome(s) {
|
|
|
182
537
|
trustWrite(file);
|
|
183
538
|
out.push({ rel: f.rel, what: "reverted", keys: n.keys });
|
|
184
539
|
}
|
|
185
|
-
const rest = changedKeys(a, n.config).filter((k) => !/privateKey|token/i.test(k)).slice(0, 20);
|
|
540
|
+
const rest = changedKeys(a, n.config).filter((k) => !/privateKey|token/i.test(k) && !/^spaces\.[^.]+\.repos\b/.test(k)).slice(0, 20);
|
|
186
541
|
if (rest.length)
|
|
187
542
|
out.push({ rel: f.rel, what: "reported", keys: rest });
|
|
188
543
|
}
|
|
@@ -205,6 +560,6 @@ export function describeChange(c) {
|
|
|
205
560
|
return `${c.rel} was written while the run could write it — removed, not acted on`;
|
|
206
561
|
if (c.what === "reported")
|
|
207
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`;
|
|
208
|
-
return `${c.rel}: what the run
|
|
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`;
|
|
209
564
|
}
|
|
210
565
|
//# sourceMappingURL=home-guard.js.map
|
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 {
|
|
@@ -88,8 +88,84 @@ export function verifyRead(publicKeyB64, method, path, params, signatureB64) {
|
|
|
88
88
|
return false;
|
|
89
89
|
}
|
|
90
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
|
+
}
|
|
91
108
|
export function fingerprint(publicKeyB64) {
|
|
92
109
|
const raw = Buffer.from(publicKeyB64, "base64").subarray(-16).toString("hex");
|
|
93
110
|
return (raw.match(/.{4}/g) ?? []).join("-");
|
|
94
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
|
+
}
|
|
95
171
|
//# sourceMappingURL=identity.js.map
|
package/dist/mcp-supervisor.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
3
4
|
import { dirname, join, resolve } from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { CONFIG_DIR } from "./config.js";
|
|
@@ -78,7 +79,9 @@ export async function superviseMcp() {
|
|
|
78
79
|
pid: process.pid, child_pid: current?.proc.pid, cwd: process.cwd(), started: STARTED,
|
|
79
80
|
supervisor: VERSION, child_version: current?.version ?? "unknown", agent: process.env.PANTHEON_AGENT,
|
|
80
81
|
};
|
|
81
|
-
|
|
82
|
+
const tmp = `${stateFile}.${randomBytes(8).toString("hex")}.tmp`;
|
|
83
|
+
writeFileSync(tmp, JSON.stringify(s), { flag: "wx" });
|
|
84
|
+
renameSync(tmp, stateFile);
|
|
82
85
|
}
|
|
83
86
|
catch { }
|
|
84
87
|
};
|