@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.
- package/README.md +142 -40
- package/bin/pantheon.js +12 -0
- package/dist/accounts.js +151 -26
- package/dist/agents.js +224 -17
- package/dist/claude-confine.js +299 -0
- package/dist/cli.js +1116 -137
- package/dist/client.js +294 -10
- package/dist/command-state.js +102 -0
- package/dist/config.js +287 -14
- package/dist/continued.js +42 -10
- package/dist/doctor.js +191 -23
- package/dist/file-lock.js +197 -0
- package/dist/grants.js +1 -1
- package/dist/history-view.js +2 -1
- package/dist/home-guard.js +565 -0
- package/dist/hooks-install.js +2 -1
- package/dist/identity.js +101 -1
- package/dist/install.js +76 -26
- package/dist/kiro-files.js +2 -2
- package/dist/ledger.js +111 -28
- package/dist/machine.js +494 -32
- package/dist/mcp-supervisor.js +29 -7
- package/dist/mcp-windows.js +47 -25
- package/dist/mcp.js +99 -36
- package/dist/network.js +39 -8
- package/dist/nonces.js +130 -0
- package/dist/notify.js +26 -9
- package/dist/pending.js +121 -22
- package/dist/persona.js +21 -0
- package/dist/pins.js +161 -0
- package/dist/profile.js +22 -1
- package/dist/protocol.js +40 -1
- package/dist/quickstart.js +173 -24
- package/dist/reads.js +37 -0
- package/dist/refusal.js +14 -1
- package/dist/relay.js +1341 -207
- package/dist/repo-find.js +127 -0
- package/dist/repo-route.js +70 -0
- package/dist/roster-file.js +190 -25
- package/dist/run-failure.js +99 -3
- package/dist/secret-guard.js +141 -0
- package/dist/service.js +91 -35
- package/dist/setup-guide.js +91 -31
- package/dist/spend.js +6 -2
- package/dist/store.js +94 -10
- package/dist/text-file.js +74 -0
- package/dist/tray.js +24 -3
- package/dist/untrusted.js +24 -5
- package/dist/update-check.js +2 -0
- package/dist/upgrade.js +9 -5
- package/dist/verify-act.js +102 -0
- package/dist/which.js +69 -12
- package/dist/worker-fleet.js +30 -9
- package/dist/worker-policy.js +231 -7
- package/dist/worker.js +1717 -130
- package/helper/macos/menubar.swift +63 -2
- package/package.json +2 -2
package/dist/client.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import WebSocket from "ws";
|
|
2
|
-
import {
|
|
2
|
+
import { adoptSpaceId, rememberTeamRepos, updateMembershipToken } from "./config.js";
|
|
3
|
+
import { markContinued } from "./continued.js";
|
|
4
|
+
import { markAnswered, nonceOf } from "./nonces.js";
|
|
5
|
+
import { signedRead } from "./reads.js";
|
|
6
|
+
import { repoName } from "./repo-route.js";
|
|
7
|
+
import { signChallenge, signBytes, actPayload, goalPayload, newActId, newActNonce, canonicalJson } from "./identity.js";
|
|
8
|
+
import { expectsSigs, joinedSigning, loadPins, markMemberSigns, markSigs, memberSigns, observeMembers, pinFor, pinScope, teamPinId, updatePins } from "./pins.js";
|
|
9
|
+
export { joinedSigning } from "./pins.js";
|
|
10
|
+
import { trusted, verifyAct, verifyGoal } from "./verify-act.js";
|
|
3
11
|
import { newId } from "./protocol.js";
|
|
4
12
|
import { VERSION } from "./version.js";
|
|
5
13
|
import { describeNetError, explainNetError, noteRelayAnswered, settleSystemFallback, systemFallbackPending, trySystemFallback, wsOptions } from "./network.js";
|
|
@@ -23,6 +31,11 @@ export class PantheonClient {
|
|
|
23
31
|
return !this.connected && !this.closed && this.everConnected;
|
|
24
32
|
}
|
|
25
33
|
cid = 0;
|
|
34
|
+
static REFUSED_RETRY_MS = 5 * 60_000;
|
|
35
|
+
static REFUSED_GRACE_MS = 2 * 60_000;
|
|
36
|
+
get refused() { return this.connected ? null : this.refusedCode; }
|
|
37
|
+
refusedCode = null;
|
|
38
|
+
refusedSince = 0;
|
|
26
39
|
seq = 0;
|
|
27
40
|
readSeq = 0;
|
|
28
41
|
head = 0;
|
|
@@ -46,13 +59,33 @@ export class PantheonClient {
|
|
|
46
59
|
enroll = null;
|
|
47
60
|
inviteUrl = null;
|
|
48
61
|
invitesList = null;
|
|
62
|
+
persistToken;
|
|
49
63
|
constructor(cfg, onEvent, opts) {
|
|
50
64
|
this.cfg = cfg;
|
|
51
65
|
this.onEvent = onEvent;
|
|
66
|
+
this.pinTeam = teamPinId(cfg);
|
|
67
|
+
if (opts?.fromSeq && opts.fromSeq > 0)
|
|
68
|
+
this.seq = opts.fromSeq;
|
|
69
|
+
this.persistToken = opts?.persistToken ?? ((space, oldToken, newToken) => { updateMembershipToken(space, oldToken, newToken); });
|
|
52
70
|
this.enroll = opts?.enroll ?? null;
|
|
53
71
|
this.session = opts?.session ?? newId("s");
|
|
54
72
|
this.role = opts?.role ?? "interactive";
|
|
55
73
|
}
|
|
74
|
+
keepRepos() {
|
|
75
|
+
try {
|
|
76
|
+
rememberTeamRepos({ spaceId: this.cfg.spaceId ?? this.spaceId, space: this.cfg.space, relay: this.cfg.relay }, this.repos);
|
|
77
|
+
}
|
|
78
|
+
catch { }
|
|
79
|
+
}
|
|
80
|
+
adoptToken(token) {
|
|
81
|
+
const old = this.cfg.token;
|
|
82
|
+
if (!token || token === old)
|
|
83
|
+
return;
|
|
84
|
+
try {
|
|
85
|
+
this.persistToken(this.cfg.space, old, token);
|
|
86
|
+
}
|
|
87
|
+
catch { }
|
|
88
|
+
}
|
|
56
89
|
async loopbackFallback() {
|
|
57
90
|
let u;
|
|
58
91
|
try {
|
|
@@ -181,14 +214,15 @@ export class PantheonClient {
|
|
|
181
214
|
...(this.enroll && !this.enrolledToken ? { enroll: this.enroll } : { token: this.cfg.token }),
|
|
182
215
|
pubkey: this.cfg.publicKey,
|
|
183
216
|
nonce: msg.nonce,
|
|
184
|
-
sig: signChallenge(this.cfg.privateKey, this.cfg.space, this.cfg.member, msg.nonce),
|
|
217
|
+
sig: signChallenge(this.cfg.delegate?.privateKey ?? this.cfg.privateKey, this.cfg.space, this.cfg.member, msg.nonce),
|
|
218
|
+
...(this.cfg.delegate ? { delegate: this.cfg.delegate.cert } : {}),
|
|
185
219
|
session: this.session,
|
|
186
220
|
role: this.role,
|
|
187
221
|
display: this.cfg.display ?? this.cfg.member,
|
|
188
222
|
agent: this.cfg.agent ?? "unknown",
|
|
189
223
|
pkg: VERSION,
|
|
190
224
|
...(this.seq > 0 ? { from_seq: this.seq } : {}),
|
|
191
|
-
...(this.cfg.repo ? { repo: this.cfg.repo, ...(this.cfg.repoOrigin ? { repo_origin: this.cfg.repoOrigin } : {}) } : {}),
|
|
225
|
+
...(this.cfg.repo ? { repo: this.cfg.repo, ...(this.cfg.repoOrigin ? { repo_origin: repoName(this.cfg.repoOrigin) } : {}) } : {}),
|
|
192
226
|
}));
|
|
193
227
|
break;
|
|
194
228
|
case "pending":
|
|
@@ -202,6 +236,7 @@ export class PantheonClient {
|
|
|
202
236
|
break;
|
|
203
237
|
case "members":
|
|
204
238
|
this.members = msg.members;
|
|
239
|
+
this.observePins();
|
|
205
240
|
break;
|
|
206
241
|
case "profiles":
|
|
207
242
|
this.profiles = new Map((msg.profiles ?? []).map((p) => [p.member, p]));
|
|
@@ -215,8 +250,19 @@ export class PantheonClient {
|
|
|
215
250
|
this.inviteUrl = { url: msg.url, expires_at: msg.expires_at };
|
|
216
251
|
break;
|
|
217
252
|
case "welcome":
|
|
253
|
+
if (this.cfg.spaceId && msg.space_id && msg.space_id !== this.cfg.spaceId) {
|
|
254
|
+
this.lastError = `SPACE_MISMATCH: the relay answered for team ${msg.space_id}, not the ${this.cfg.spaceId} this machine joined — not connecting (pantheon spaces; rejoin with a fresh invite if the team was recreated)`;
|
|
255
|
+
this.closed = true;
|
|
256
|
+
ws.close();
|
|
257
|
+
if (!settled) {
|
|
258
|
+
settled = true;
|
|
259
|
+
reject(new Error(this.lastError));
|
|
260
|
+
}
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
218
263
|
this.connected = true;
|
|
219
264
|
this.pending = null;
|
|
265
|
+
this.refusedCode = null;
|
|
220
266
|
this.session = msg.session;
|
|
221
267
|
this.everConnected = true;
|
|
222
268
|
this.head = msg.seq;
|
|
@@ -228,20 +274,45 @@ export class PantheonClient {
|
|
|
228
274
|
this.readSeq = msg.seq;
|
|
229
275
|
this.everReadMarked = true;
|
|
230
276
|
if (msg.token) {
|
|
231
|
-
this.
|
|
277
|
+
if (this.enroll)
|
|
278
|
+
this.enrolledToken = msg.token;
|
|
279
|
+
else
|
|
280
|
+
this.adoptToken(msg.token);
|
|
232
281
|
this.cfg.token = msg.token;
|
|
233
282
|
}
|
|
234
283
|
if (msg.space_id) {
|
|
284
|
+
if (!this.cfg.spaceId) {
|
|
285
|
+
let adopted = false;
|
|
286
|
+
try {
|
|
287
|
+
adopted = adoptSpaceId(this.cfg.relay, this.cfg.space, msg.space_id);
|
|
288
|
+
}
|
|
289
|
+
catch { }
|
|
290
|
+
if (adopted === "conflict") {
|
|
291
|
+
this.lastError = `SPACE_MISMATCH: the relay says "${this.cfg.space}" is team ${msg.space_id}, which is another team this machine is in — not connecting`;
|
|
292
|
+
this.closed = true;
|
|
293
|
+
ws.close();
|
|
294
|
+
if (!settled) {
|
|
295
|
+
settled = true;
|
|
296
|
+
reject(new Error(this.lastError));
|
|
297
|
+
}
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (!this.cfg.spaceId)
|
|
302
|
+
this.cfg.pinTeam ??= this.pinTeam;
|
|
235
303
|
this.spaceId = msg.space_id;
|
|
236
304
|
this.cfg.spaceId = msg.space_id;
|
|
237
305
|
}
|
|
238
306
|
this.repos = msg.repos ?? [];
|
|
307
|
+
this.keepRepos();
|
|
239
308
|
this.plan = msg.plan ?? null;
|
|
240
309
|
this.adoptKnownRepo();
|
|
241
310
|
this.backoff = 500;
|
|
242
311
|
this.roster = msg.roster;
|
|
243
312
|
this.obligations = msg.obligations;
|
|
244
313
|
this.members = msg.members;
|
|
314
|
+
this.features = msg.features ?? [];
|
|
315
|
+
this.observePins();
|
|
245
316
|
this.profiles = new Map((msg.profiles ?? []).map((p) => [p.member, p]));
|
|
246
317
|
for (const g of msg.goals)
|
|
247
318
|
this.goals.set(g.id, g);
|
|
@@ -250,16 +321,33 @@ export class PantheonClient {
|
|
|
250
321
|
resolve();
|
|
251
322
|
}
|
|
252
323
|
break;
|
|
324
|
+
case "token":
|
|
325
|
+
if (typeof msg.token === "string" && msg.token) {
|
|
326
|
+
this.adoptToken(msg.token);
|
|
327
|
+
this.cfg.token = msg.token;
|
|
328
|
+
}
|
|
329
|
+
break;
|
|
253
330
|
case "repos":
|
|
254
331
|
this.repos = msg.repos;
|
|
332
|
+
this.keepRepos();
|
|
255
333
|
this.adoptKnownRepo();
|
|
256
334
|
break;
|
|
257
335
|
case "act":
|
|
336
|
+
if (msg.act.sig && !this.sigsSeen) {
|
|
337
|
+
try {
|
|
338
|
+
this.trustOf(msg.act);
|
|
339
|
+
}
|
|
340
|
+
catch { }
|
|
341
|
+
}
|
|
258
342
|
if (msg.act.seq > this.seq)
|
|
259
343
|
this.seq = msg.act.seq;
|
|
260
344
|
if (msg.act.seq > this.head)
|
|
261
345
|
this.head = msg.act.seq;
|
|
262
|
-
|
|
346
|
+
{
|
|
347
|
+
const had = this.seen.get(msg.act.id);
|
|
348
|
+
if (!had || canonicalJson(had) === canonicalJson(msg.act))
|
|
349
|
+
this.seen.set(msg.act.id, msg.act);
|
|
350
|
+
}
|
|
263
351
|
if (this.seen.size > 500)
|
|
264
352
|
this.seen.delete(this.seen.keys().next().value);
|
|
265
353
|
if (msg.act.from !== this.cfg.member && msg.act.act !== "JOIN" && msg.act.act !== "LEAVE") {
|
|
@@ -285,6 +373,12 @@ export class PantheonClient {
|
|
|
285
373
|
}
|
|
286
374
|
case "error": {
|
|
287
375
|
this.lastError = `${msg.code}: ${msg.message}`;
|
|
376
|
+
if (!msg.cid) {
|
|
377
|
+
const code = ["REVOKED", "KEY_MISMATCH", "BAD_TOKEN"].includes(msg.code) ? msg.code : null;
|
|
378
|
+
if (code && !this.refusedCode)
|
|
379
|
+
this.refusedSince = Date.now();
|
|
380
|
+
this.refusedCode = code;
|
|
381
|
+
}
|
|
288
382
|
if (msg.code === "SPACE_DELETED")
|
|
289
383
|
this.closed = true;
|
|
290
384
|
const fn = msg.cid ? this.acks.get(msg.cid) : undefined;
|
|
@@ -325,8 +419,10 @@ export class PantheonClient {
|
|
|
325
419
|
}
|
|
326
420
|
reject(new Error(this.lastError ?? "relay connection failed"));
|
|
327
421
|
}
|
|
422
|
+
const slow = !!this.refused && Date.now() - this.refusedSince >= PantheonClient.REFUSED_GRACE_MS;
|
|
423
|
+
const wait = slow ? PantheonClient.REFUSED_RETRY_MS : Math.round(this.backoff * (0.5 + Math.random()));
|
|
328
424
|
setTimeout(() => { if (!this.closed)
|
|
329
|
-
this.connect().catch(() => { }); },
|
|
425
|
+
this.connect().catch(() => { }); }, wait);
|
|
330
426
|
this.backoff = Math.min(this.backoff * 2, 15_000);
|
|
331
427
|
};
|
|
332
428
|
ws.on("close", down);
|
|
@@ -414,7 +510,48 @@ export class PantheonClient {
|
|
|
414
510
|
async send(a) {
|
|
415
511
|
const repo = this.cfg.repo;
|
|
416
512
|
const meta = a.act === "DELIVERED" && repo && !(a.meta && "repo" in a.meta) ? { ...(a.meta ?? {}), repo } : a.meta;
|
|
417
|
-
const
|
|
513
|
+
const id = newActId();
|
|
514
|
+
const sig = this.signAct(id, { ...a, ...(meta ? { meta } : {}) }, repo);
|
|
515
|
+
let unrecorded = false, unsignedRef = false;
|
|
516
|
+
if (a.ref && this.ws?.readyState === WebSocket.OPEN && (a.act === "ANSWER" || a.act === "ACCEPT" || a.act === "OBJECT" || a.act === "DELIVERED")) {
|
|
517
|
+
try {
|
|
518
|
+
markContinued(this.pinTeam, `handled:${a.ref}`);
|
|
519
|
+
}
|
|
520
|
+
catch { }
|
|
521
|
+
const noted = await this.noteAnswered(a.ref).catch(() => false);
|
|
522
|
+
unrecorded = noted === false && this.signaturesExpected();
|
|
523
|
+
unsignedRef = noted === "unsigned";
|
|
524
|
+
}
|
|
525
|
+
const r = await this.rpc({ t: "act", id, ...a, ...(meta ? { meta } : {}), ...(repo ? { repo } : {}), sig });
|
|
526
|
+
let sentId = r.id === id ? id : null;
|
|
527
|
+
if (r.id !== id && this.features.includes("act-sig")) {
|
|
528
|
+
let prior = this.seen.get(r.id) ?? null;
|
|
529
|
+
if (!prior) {
|
|
530
|
+
try {
|
|
531
|
+
const res = await signedRead(this.cfg, "/act", { id: r.id }, { timeoutMs: 3000 });
|
|
532
|
+
prior = res.ok ? (await res.json()).act ?? null : null;
|
|
533
|
+
}
|
|
534
|
+
catch { }
|
|
535
|
+
}
|
|
536
|
+
const retry = !!prior && prior.id === r.id && prior.from === this.cfg.member && prior.act === a.act && prior.body === a.body
|
|
537
|
+
&& (prior.goal ?? null) === (a.goal ?? null) && (prior.ref ?? null) === (a.ref ?? null) && !!prior.sig && trusted(this.trustOf(prior).trust);
|
|
538
|
+
if (retry)
|
|
539
|
+
sentId = r.id;
|
|
540
|
+
else {
|
|
541
|
+
r.id = id;
|
|
542
|
+
sentId = id;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
if (unrecorded)
|
|
546
|
+
r.unrecorded = true;
|
|
547
|
+
else if (unsignedRef && this.signaturesExpected())
|
|
548
|
+
r.unrecorded = "unsigned";
|
|
549
|
+
if (sentId) {
|
|
550
|
+
try {
|
|
551
|
+
markContinued(`${this.pinKey()} sent`, sentId);
|
|
552
|
+
}
|
|
553
|
+
catch { }
|
|
554
|
+
}
|
|
418
555
|
if (a.ref) {
|
|
419
556
|
for (const [id, o] of this.claimedByMe)
|
|
420
557
|
if (o.ref === a.ref)
|
|
@@ -428,7 +565,150 @@ export class PantheonClient {
|
|
|
428
565
|
return r;
|
|
429
566
|
}
|
|
430
567
|
openGoal(g) {
|
|
431
|
-
|
|
568
|
+
const k = this.signingKey();
|
|
569
|
+
const nonce = newActNonce(), ts = Date.now();
|
|
570
|
+
const id = `g_${ts.toString(36)}${Buffer.from(nonce, "base64url").toString("hex").slice(0, 12)}`;
|
|
571
|
+
const invite = g.invite ? [...new Set(g.invite)] : null;
|
|
572
|
+
const sig = {
|
|
573
|
+
v: 1, role: k.role, nonce, ts, pub: k.pub, invite,
|
|
574
|
+
budget: g.budget ?? null, deadline: g.deadline ?? null, kind: g.kind ?? null,
|
|
575
|
+
...(k.cert ? { cert: k.cert } : {}),
|
|
576
|
+
s: signBytes(k.priv, goalPayload({
|
|
577
|
+
id, team: this.teamKey(), member: this.cfg.member, keyRole: k.role, nonce, ts,
|
|
578
|
+
title: g.title, acceptance: g.acceptance ?? "", invite,
|
|
579
|
+
budget: g.budget ?? null, deadline: g.deadline ?? null, kind: g.kind ?? null,
|
|
580
|
+
})),
|
|
581
|
+
};
|
|
582
|
+
return this.rpc({ t: "open_goal", id, acceptance: "", ...g, ...(invite ? { invite } : {}), sig });
|
|
583
|
+
}
|
|
584
|
+
features = [];
|
|
585
|
+
pinChanges = [];
|
|
586
|
+
pinsCache = null;
|
|
587
|
+
observePins() {
|
|
588
|
+
try {
|
|
589
|
+
const team = this.pinKey();
|
|
590
|
+
let changes = [];
|
|
591
|
+
const p = updatePins((p) => {
|
|
592
|
+
if (p.corrupt)
|
|
593
|
+
return false;
|
|
594
|
+
const before = JSON.stringify(p);
|
|
595
|
+
changes = observeMembers(p, team, this.members, { member: this.cfg.member, publicKey: this.cfg.publicKey });
|
|
596
|
+
if (this.features.includes("act-sig"))
|
|
597
|
+
markSigs(p, team);
|
|
598
|
+
return JSON.stringify(p) !== before;
|
|
599
|
+
});
|
|
600
|
+
this.pinsCache = p;
|
|
601
|
+
this.pinChanges.push(...changes);
|
|
602
|
+
if (this.pinChanges.length > 50)
|
|
603
|
+
this.pinChanges.splice(0, this.pinChanges.length - 50);
|
|
604
|
+
}
|
|
605
|
+
catch { }
|
|
606
|
+
}
|
|
607
|
+
pins() {
|
|
608
|
+
try {
|
|
609
|
+
this.pinsCache = loadPins();
|
|
610
|
+
}
|
|
611
|
+
catch {
|
|
612
|
+
this.pinsCache ??= { v: 1, teams: {} };
|
|
613
|
+
}
|
|
614
|
+
return this.pinsCache;
|
|
615
|
+
}
|
|
616
|
+
trustOf(rec) {
|
|
617
|
+
let pin = pinFor(this.pins(), this.pinKey(), rec.from);
|
|
618
|
+
if (!pin || (rec.sig?.role === "run" && !pin.key)) {
|
|
619
|
+
this.pinsCache = null;
|
|
620
|
+
this.observePins();
|
|
621
|
+
pin = pinFor(this.pins(), this.pinKey(), rec.from);
|
|
622
|
+
}
|
|
623
|
+
const r = verifyAct(rec, [this.cfg.spaceId, this.spaceId, this.cfg.space], pin?.fp, pin?.key);
|
|
624
|
+
if (trusted(r.trust) && rec.sig && !this.verifiedIds.has(rec.id)) {
|
|
625
|
+
this.verifiedIds.set(rec.id, { member: rec.from, nonce: rec.sig.nonce });
|
|
626
|
+
if (this.verifiedIds.size > 5000)
|
|
627
|
+
this.verifiedIds.delete(this.verifiedIds.keys().next().value);
|
|
628
|
+
}
|
|
629
|
+
if ((r.trust === "verified" || r.trust === "verified-run") && !this.sigsSeen)
|
|
630
|
+
this.noteSigsSeen();
|
|
631
|
+
if ((r.trust === "verified" || r.trust === "verified-run") && !this.signersSeen.has(rec.from)) {
|
|
632
|
+
this.signersSeen.add(rec.from);
|
|
633
|
+
try {
|
|
634
|
+
updatePins((p) => !p.corrupt && markMemberSigns(p, this.pinKey(), rec.from));
|
|
635
|
+
}
|
|
636
|
+
catch { }
|
|
637
|
+
}
|
|
638
|
+
return r;
|
|
639
|
+
}
|
|
640
|
+
signersSeen = new Set();
|
|
641
|
+
memberSigns(member) { return memberSigns(this.pins(), this.pinKey(), member); }
|
|
642
|
+
sigsSeen = false;
|
|
643
|
+
noteSigsSeen() {
|
|
644
|
+
this.sigsSeen = true;
|
|
645
|
+
try {
|
|
646
|
+
updatePins((p) => !p.corrupt && markSigs(p, this.pinKey()));
|
|
647
|
+
}
|
|
648
|
+
catch { }
|
|
649
|
+
}
|
|
650
|
+
goalTrust(g) {
|
|
651
|
+
const pin = pinFor(this.pins(), this.pinKey(), g.opened_by);
|
|
652
|
+
return verifyGoal(g, [this.cfg.spaceId, this.spaceId, this.cfg.space], pin?.fp, pin?.key);
|
|
653
|
+
}
|
|
654
|
+
signaturesExpected() {
|
|
655
|
+
return this.features.includes("act-sig") || expectsSigs(this.pins(), this.pinKey()) || joinedSigning(this.cfg.joinedWith);
|
|
656
|
+
}
|
|
657
|
+
signingSince() {
|
|
658
|
+
return this.pins().teams[this.pinKey()]?.sigs ?? null;
|
|
659
|
+
}
|
|
660
|
+
async noteAnswered(ref) {
|
|
661
|
+
const known = this.verifiedIds.get(ref);
|
|
662
|
+
if (known) {
|
|
663
|
+
markAnswered(this.pinTeam, known.member, known.nonce, ref);
|
|
664
|
+
return true;
|
|
665
|
+
}
|
|
666
|
+
let r = this.seen.get(ref) ?? null;
|
|
667
|
+
if (!r) {
|
|
668
|
+
try {
|
|
669
|
+
const res = await signedRead(this.cfg, "/act", { id: ref }, { timeoutMs: 3000 });
|
|
670
|
+
r = res.ok ? (await res.json()).act ?? null : null;
|
|
671
|
+
}
|
|
672
|
+
catch {
|
|
673
|
+
r = null;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
if (!r) {
|
|
677
|
+
const local = nonceOf(this.pinTeam, ref);
|
|
678
|
+
if (local)
|
|
679
|
+
markAnswered(this.pinTeam, local.member, local.nonce, ref);
|
|
680
|
+
return false;
|
|
681
|
+
}
|
|
682
|
+
if (r && r.id === ref && !r.sig && this.trustOf(r).trust === "unsigned" && !this.memberSigns(r.from))
|
|
683
|
+
return "unsigned";
|
|
684
|
+
if (!r || r.id !== ref || !r.sig || !trusted(this.trustOf(r).trust))
|
|
685
|
+
return false;
|
|
686
|
+
markAnswered(this.pinTeam, r.from, r.sig.nonce, r.id, undefined, r.sig.s);
|
|
687
|
+
return true;
|
|
688
|
+
}
|
|
689
|
+
verifiedIds = new Map();
|
|
690
|
+
pinKey() { return pinScope(this.pinTeam, this.cfg.member); }
|
|
691
|
+
pinTeam;
|
|
692
|
+
teamKey() { return this.cfg.spaceId ?? this.spaceId ?? this.cfg.space; }
|
|
693
|
+
signingKey() {
|
|
694
|
+
const d = this.cfg.delegate;
|
|
695
|
+
return d ? { role: "run", priv: d.privateKey, pub: d.cert.pub, cert: d.cert } : { role: "member", priv: this.cfg.privateKey, pub: this.cfg.publicKey };
|
|
696
|
+
}
|
|
697
|
+
signAct(id, a, repo) {
|
|
698
|
+
const k = this.signingKey();
|
|
699
|
+
const nonce = newActNonce(), ts = Date.now();
|
|
700
|
+
return {
|
|
701
|
+
v: 1, role: k.role, nonce, ts, pub: k.pub,
|
|
702
|
+
...(a.meta ? { meta: a.meta } : {}),
|
|
703
|
+
...(repo ? { repo } : {}),
|
|
704
|
+
...(k.cert ? { cert: k.cert } : {}),
|
|
705
|
+
s: signBytes(k.priv, actPayload({
|
|
706
|
+
id, team: this.teamKey(), member: this.cfg.member, keyRole: k.role, nonce, ts,
|
|
707
|
+
act: a.act, goal: a.goal ?? null, ref: a.ref ?? null, to: a.to ?? null,
|
|
708
|
+
defers_to: a.defers_to ?? null, affects: a.affects ?? null, repo: repo ?? null,
|
|
709
|
+
summary: a.summary ?? null, body: a.body, meta: a.meta ?? {},
|
|
710
|
+
})),
|
|
711
|
+
};
|
|
432
712
|
}
|
|
433
713
|
claimedByMe = new Map();
|
|
434
714
|
async claim(obligation) {
|
|
@@ -453,6 +733,9 @@ export class PantheonClient {
|
|
|
453
733
|
admit(member, mayWake = false, revoke = false, forget = false) {
|
|
454
734
|
return this.rpc({ t: "admit", member, may_wake: mayWake, revoke, forget });
|
|
455
735
|
}
|
|
736
|
+
setMayWake(member, mayWake) {
|
|
737
|
+
return this.rpc({ t: "admit", member, may_wake: mayWake, wake_only: true });
|
|
738
|
+
}
|
|
456
739
|
publishProfile(profile) {
|
|
457
740
|
return this.rpc({ t: "publish_profile", profile });
|
|
458
741
|
}
|
|
@@ -478,7 +761,7 @@ export class PantheonClient {
|
|
|
478
761
|
bindRepo(fingerprint = this.cfg.repo, origin = this.cfg.repoOrigin ?? null, base = null) {
|
|
479
762
|
if (!fingerprint)
|
|
480
763
|
return Promise.reject(new Error("not inside a git repo — run this from the team's checkout"));
|
|
481
|
-
return this.rpc({ t: "bind_repo", repo: fingerprint, origin, ...(base ? { base } : {}) });
|
|
764
|
+
return this.rpc({ t: "bind_repo", repo: fingerprint, origin: origin ? repoName(origin) : origin, ...(base ? { base } : {}) });
|
|
482
765
|
}
|
|
483
766
|
makeInvite(mayWake = true, ttlMinutes = 60) {
|
|
484
767
|
return new Promise((res, rej) => {
|
|
@@ -549,7 +832,8 @@ export class PantheonClient {
|
|
|
549
832
|
}
|
|
550
833
|
recent(id) {
|
|
551
834
|
const r = this.seen.get(id);
|
|
552
|
-
|
|
835
|
+
const plain = (s) => (typeof s === "string" && /^[A-Za-z0-9_][A-Za-z0-9_:.\-]{0,119}$/.test(s) ? s : "(an id not printed)");
|
|
836
|
+
return r ? `${plain(r.act)} from ${plain(r.from)}${r.goal ? ` (goal ${plain(r.goal)})` : ""} id=${plain(r.id)}\n${r.body}` : null;
|
|
553
837
|
}
|
|
554
838
|
recentFrom(id) {
|
|
555
839
|
return this.seen.get(id)?.from ?? null;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { platform } from "node:os";
|
|
4
|
+
import { chooseAgent, claudeSandboxable, NOT_FOR_UNATTENDED_RUNS, parseRepoCommands, REPO_COMMANDS_FILE, SANDBOXED_FOR_UNATTENDED } from "./agents.js";
|
|
5
|
+
import { detectClaudeFeatures } from "./claude-confine.js";
|
|
6
|
+
import { loadPersonas } from "./persona.js";
|
|
7
|
+
import { plistPath, existingUnitPath } from "./service.js";
|
|
8
|
+
import { loadPolicy, unsandboxedAllowed } from "./worker-policy.js";
|
|
9
|
+
export function workerServiceAllowsBash(plat = platform()) {
|
|
10
|
+
try {
|
|
11
|
+
if (plat === "darwin") {
|
|
12
|
+
const p = plistPath("worker");
|
|
13
|
+
if (!existsSync(p))
|
|
14
|
+
return null;
|
|
15
|
+
const xml = readFileSync(p, "utf8");
|
|
16
|
+
const args = [...(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/.exec(xml)?.[1] ?? "").matchAll(/<string>([^<]*)<\/string>/g)].map((m) => m[1]);
|
|
17
|
+
return args.includes("--allow-bash");
|
|
18
|
+
}
|
|
19
|
+
if (plat === "win32")
|
|
20
|
+
return null;
|
|
21
|
+
const u = existingUnitPath("worker");
|
|
22
|
+
if (!u)
|
|
23
|
+
return null;
|
|
24
|
+
const exec = /^ExecStart=(.*)$/m.exec(readFileSync(u, "utf8"))?.[1] ?? "";
|
|
25
|
+
return /(^|\s|")--allow-bash("|\s|$)/.test(exec);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function commandState(o = {}) {
|
|
32
|
+
const allowBash = o.allowBash ?? workerServiceAllowsBash(o.plat) ?? false;
|
|
33
|
+
let grant = null;
|
|
34
|
+
try {
|
|
35
|
+
grant = loadPolicy(o.home).grants.run_commands ?? null;
|
|
36
|
+
}
|
|
37
|
+
catch { }
|
|
38
|
+
let personasOff = [];
|
|
39
|
+
try {
|
|
40
|
+
personasOff = loadPersonas(o.project ?? process.cwd(), o.home).filter((p) => p.allowBash === false).map((p) => p.name);
|
|
41
|
+
}
|
|
42
|
+
catch { }
|
|
43
|
+
const level = allowBash || grant?.mode === "always" ? "always" : grant ? "once" : "off";
|
|
44
|
+
const except = personasOff.length ? `, except runs handled by ${personasOff.length === 1 ? "persona" : "personas"} ${personasOff.join(", ")} (allow_bash: false)` : "";
|
|
45
|
+
const summary = level === "off"
|
|
46
|
+
? "off — woken runs edit files only; a run that needs more asks here (pantheon policy grant run-commands)"
|
|
47
|
+
: allowBash
|
|
48
|
+
? `on — the worker runs with --allow-bash${grant ? ", and run-commands is granted here too" : ""}${except}`
|
|
49
|
+
: grant.mode === "always"
|
|
50
|
+
? `on — run-commands granted on this machine (${grant.by}, ${new Date(grant.at).toLocaleDateString()})${except}`
|
|
51
|
+
: `on for the next run only — run-commands granted once on this machine${except}`;
|
|
52
|
+
return { level, allowBash, grant, personasOff, summary };
|
|
53
|
+
}
|
|
54
|
+
export function repoCommandsAtBase(project, base) {
|
|
55
|
+
const git = (...a) => execFileSync("git", ["-C", project, ...a], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000 }).trim();
|
|
56
|
+
const exists = (ref) => { try {
|
|
57
|
+
git("rev-parse", "--verify", "--quiet", `${ref}^{commit}`);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return false;
|
|
62
|
+
} };
|
|
63
|
+
let remoteHead = null;
|
|
64
|
+
try {
|
|
65
|
+
remoteHead = git("symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD") || null;
|
|
66
|
+
}
|
|
67
|
+
catch { }
|
|
68
|
+
const ref = [base ? `origin/${base}` : null, base ?? null, remoteHead, "HEAD"].filter((r) => !!r).find(exists) ?? null;
|
|
69
|
+
if (!ref)
|
|
70
|
+
return { ref: null, ok: [], refused: [] };
|
|
71
|
+
let text = "";
|
|
72
|
+
try {
|
|
73
|
+
text = git("show", `${ref}:${REPO_COMMANDS_FILE}`);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return { ref, ok: [], refused: [] };
|
|
77
|
+
}
|
|
78
|
+
return { ref, ...parseRepoCommands(text) };
|
|
79
|
+
}
|
|
80
|
+
export function commandCaveat(o = {}) {
|
|
81
|
+
try {
|
|
82
|
+
const unsandboxedOk = unsandboxedAllowed(loadPolicy(o.home));
|
|
83
|
+
const c = (o.choose ?? chooseAgent)("auto", { check: null, unsandboxedOk });
|
|
84
|
+
if (NOT_FOR_UNATTENDED_RUNS.has(c.backend.id))
|
|
85
|
+
return `${c.backend.label}, the only agent here, answers in your live sessions — for runs while you're away, install Claude Code, Codex, Cursor or Copilot`;
|
|
86
|
+
if (unsandboxedOk)
|
|
87
|
+
return "";
|
|
88
|
+
if (!SANDBOXED_FOR_UNATTENDED.has(c.backend.id))
|
|
89
|
+
return `${c.backend.label}, the agent here, is set to leave teammates' requests for your next session (to have it answer them: pantheon policy grant unsandboxed-runs)`;
|
|
90
|
+
const f = (o.features ?? detectClaudeFeatures)(c.binary);
|
|
91
|
+
const plat = o.plat ?? process.platform;
|
|
92
|
+
if (f.unknown && plat !== "win32")
|
|
93
|
+
return `but \`${c.binary} --help\` did not answer in time, so this Claude Code's sandbox could not be confirmed and a woken run gets no commands until it does (it is asked again at each run)`;
|
|
94
|
+
if (!claudeSandboxable(f, plat))
|
|
95
|
+
return `this Claude Code (${f.version ?? "version unknown"}${plat === "win32" ? ", Windows" : ""}) is set to keep runs while you're away to edits — for commands, update it to 2.1.83+ on macOS or Linux, or: pantheon policy grant unsandboxed-runs`;
|
|
96
|
+
return "";
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return "";
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=command-state.js.map
|