@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/client.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import WebSocket from "ws";
|
|
2
|
-
import { updateMembershipToken } from "./config.js";
|
|
3
|
-
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";
|
|
4
11
|
import { newId } from "./protocol.js";
|
|
5
12
|
import { VERSION } from "./version.js";
|
|
6
13
|
import { describeNetError, explainNetError, noteRelayAnswered, settleSystemFallback, systemFallbackPending, trySystemFallback, wsOptions } from "./network.js";
|
|
@@ -56,11 +63,20 @@ export class PantheonClient {
|
|
|
56
63
|
constructor(cfg, onEvent, opts) {
|
|
57
64
|
this.cfg = cfg;
|
|
58
65
|
this.onEvent = onEvent;
|
|
66
|
+
this.pinTeam = teamPinId(cfg);
|
|
67
|
+
if (opts?.fromSeq && opts.fromSeq > 0)
|
|
68
|
+
this.seq = opts.fromSeq;
|
|
59
69
|
this.persistToken = opts?.persistToken ?? ((space, oldToken, newToken) => { updateMembershipToken(space, oldToken, newToken); });
|
|
60
70
|
this.enroll = opts?.enroll ?? null;
|
|
61
71
|
this.session = opts?.session ?? newId("s");
|
|
62
72
|
this.role = opts?.role ?? "interactive";
|
|
63
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
|
+
}
|
|
64
80
|
adoptToken(token) {
|
|
65
81
|
const old = this.cfg.token;
|
|
66
82
|
if (!token || token === old)
|
|
@@ -198,14 +214,15 @@ export class PantheonClient {
|
|
|
198
214
|
...(this.enroll && !this.enrolledToken ? { enroll: this.enroll } : { token: this.cfg.token }),
|
|
199
215
|
pubkey: this.cfg.publicKey,
|
|
200
216
|
nonce: msg.nonce,
|
|
201
|
-
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 } : {}),
|
|
202
219
|
session: this.session,
|
|
203
220
|
role: this.role,
|
|
204
221
|
display: this.cfg.display ?? this.cfg.member,
|
|
205
222
|
agent: this.cfg.agent ?? "unknown",
|
|
206
223
|
pkg: VERSION,
|
|
207
224
|
...(this.seq > 0 ? { from_seq: this.seq } : {}),
|
|
208
|
-
...(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) } : {}) } : {}),
|
|
209
226
|
}));
|
|
210
227
|
break;
|
|
211
228
|
case "pending":
|
|
@@ -219,6 +236,7 @@ export class PantheonClient {
|
|
|
219
236
|
break;
|
|
220
237
|
case "members":
|
|
221
238
|
this.members = msg.members;
|
|
239
|
+
this.observePins();
|
|
222
240
|
break;
|
|
223
241
|
case "profiles":
|
|
224
242
|
this.profiles = new Map((msg.profiles ?? []).map((p) => [p.member, p]));
|
|
@@ -232,6 +250,16 @@ export class PantheonClient {
|
|
|
232
250
|
this.inviteUrl = { url: msg.url, expires_at: msg.expires_at };
|
|
233
251
|
break;
|
|
234
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
|
+
}
|
|
235
263
|
this.connected = true;
|
|
236
264
|
this.pending = null;
|
|
237
265
|
this.refusedCode = null;
|
|
@@ -253,16 +281,38 @@ export class PantheonClient {
|
|
|
253
281
|
this.cfg.token = msg.token;
|
|
254
282
|
}
|
|
255
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;
|
|
256
303
|
this.spaceId = msg.space_id;
|
|
257
304
|
this.cfg.spaceId = msg.space_id;
|
|
258
305
|
}
|
|
259
306
|
this.repos = msg.repos ?? [];
|
|
307
|
+
this.keepRepos();
|
|
260
308
|
this.plan = msg.plan ?? null;
|
|
261
309
|
this.adoptKnownRepo();
|
|
262
310
|
this.backoff = 500;
|
|
263
311
|
this.roster = msg.roster;
|
|
264
312
|
this.obligations = msg.obligations;
|
|
265
313
|
this.members = msg.members;
|
|
314
|
+
this.features = msg.features ?? [];
|
|
315
|
+
this.observePins();
|
|
266
316
|
this.profiles = new Map((msg.profiles ?? []).map((p) => [p.member, p]));
|
|
267
317
|
for (const g of msg.goals)
|
|
268
318
|
this.goals.set(g.id, g);
|
|
@@ -279,14 +329,25 @@ export class PantheonClient {
|
|
|
279
329
|
break;
|
|
280
330
|
case "repos":
|
|
281
331
|
this.repos = msg.repos;
|
|
332
|
+
this.keepRepos();
|
|
282
333
|
this.adoptKnownRepo();
|
|
283
334
|
break;
|
|
284
335
|
case "act":
|
|
336
|
+
if (msg.act.sig && !this.sigsSeen) {
|
|
337
|
+
try {
|
|
338
|
+
this.trustOf(msg.act);
|
|
339
|
+
}
|
|
340
|
+
catch { }
|
|
341
|
+
}
|
|
285
342
|
if (msg.act.seq > this.seq)
|
|
286
343
|
this.seq = msg.act.seq;
|
|
287
344
|
if (msg.act.seq > this.head)
|
|
288
345
|
this.head = msg.act.seq;
|
|
289
|
-
|
|
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
|
+
}
|
|
290
351
|
if (this.seen.size > 500)
|
|
291
352
|
this.seen.delete(this.seen.keys().next().value);
|
|
292
353
|
if (msg.act.from !== this.cfg.member && msg.act.act !== "JOIN" && msg.act.act !== "LEAVE") {
|
|
@@ -449,7 +510,48 @@ export class PantheonClient {
|
|
|
449
510
|
async send(a) {
|
|
450
511
|
const repo = this.cfg.repo;
|
|
451
512
|
const meta = a.act === "DELIVERED" && repo && !(a.meta && "repo" in a.meta) ? { ...(a.meta ?? {}), repo } : a.meta;
|
|
452
|
-
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
|
+
}
|
|
453
555
|
if (a.ref) {
|
|
454
556
|
for (const [id, o] of this.claimedByMe)
|
|
455
557
|
if (o.ref === a.ref)
|
|
@@ -463,7 +565,150 @@ export class PantheonClient {
|
|
|
463
565
|
return r;
|
|
464
566
|
}
|
|
465
567
|
openGoal(g) {
|
|
466
|
-
|
|
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
|
+
};
|
|
467
712
|
}
|
|
468
713
|
claimedByMe = new Map();
|
|
469
714
|
async claim(obligation) {
|
|
@@ -488,6 +733,9 @@ export class PantheonClient {
|
|
|
488
733
|
admit(member, mayWake = false, revoke = false, forget = false) {
|
|
489
734
|
return this.rpc({ t: "admit", member, may_wake: mayWake, revoke, forget });
|
|
490
735
|
}
|
|
736
|
+
setMayWake(member, mayWake) {
|
|
737
|
+
return this.rpc({ t: "admit", member, may_wake: mayWake, wake_only: true });
|
|
738
|
+
}
|
|
491
739
|
publishProfile(profile) {
|
|
492
740
|
return this.rpc({ t: "publish_profile", profile });
|
|
493
741
|
}
|
|
@@ -513,7 +761,7 @@ export class PantheonClient {
|
|
|
513
761
|
bindRepo(fingerprint = this.cfg.repo, origin = this.cfg.repoOrigin ?? null, base = null) {
|
|
514
762
|
if (!fingerprint)
|
|
515
763
|
return Promise.reject(new Error("not inside a git repo — run this from the team's checkout"));
|
|
516
|
-
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 } : {}) });
|
|
517
765
|
}
|
|
518
766
|
makeInvite(mayWake = true, ttlMinutes = 60) {
|
|
519
767
|
return new Promise((res, rej) => {
|
|
@@ -584,7 +832,8 @@ export class PantheonClient {
|
|
|
584
832
|
}
|
|
585
833
|
recent(id) {
|
|
586
834
|
const r = this.seen.get(id);
|
|
587
|
-
|
|
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;
|
|
588
837
|
}
|
|
589
838
|
recentFrom(id) {
|
|
590
839
|
return this.seen.get(id)?.from ?? null;
|
package/dist/command-state.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { platform } from "node:os";
|
|
4
|
-
import { parseRepoCommands, REPO_COMMANDS_FILE } from "./agents.js";
|
|
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";
|
|
5
6
|
import { loadPersonas } from "./persona.js";
|
|
6
7
|
import { plistPath, existingUnitPath } from "./service.js";
|
|
7
|
-
import { loadPolicy } from "./worker-policy.js";
|
|
8
|
+
import { loadPolicy, unsandboxedAllowed } from "./worker-policy.js";
|
|
8
9
|
export function workerServiceAllowsBash(plat = platform()) {
|
|
9
10
|
try {
|
|
10
11
|
if (plat === "darwin") {
|
|
@@ -76,4 +77,26 @@ export function repoCommandsAtBase(project, base) {
|
|
|
76
77
|
}
|
|
77
78
|
return { ref, ...parseRepoCommands(text) };
|
|
78
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
|
+
}
|
|
79
102
|
//# sourceMappingURL=command-state.js.map
|
package/dist/config.js
CHANGED
|
@@ -4,6 +4,7 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { join, resolve, sep } from "node:path";
|
|
6
6
|
import { VERSION } from "./version.js";
|
|
7
|
+
import { repoName } from "./repo-route.js";
|
|
7
8
|
export const CONFIG_DIR = process.env.PANTHEON_HOME ?? join(homedir(), ".pantheon");
|
|
8
9
|
export const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
9
10
|
export const DEFAULT_RELAY = process.env.PANTHEON_DEFAULT_RELAY
|
|
@@ -112,7 +113,10 @@ export function migrate(raw) {
|
|
|
112
113
|
f.bindings ??= {};
|
|
113
114
|
if (!raw.version && f.space && f.token && !Object.values(f.spaces).some((m) => m.name === f.space && m.relay === f.relay)) {
|
|
114
115
|
const id = legacySpaceId(f.relay, f.space);
|
|
115
|
-
f.spaces[id] = {
|
|
116
|
+
f.spaces[id] = {
|
|
117
|
+
id, name: f.space, relay: f.relay, token: f.token, joined_at: Date.now(),
|
|
118
|
+
...(raw.pinTeam ? { pin_team: raw.pinTeam } : {}), ...(raw.joinedWith ? { since: raw.joinedWith } : {}),
|
|
119
|
+
};
|
|
116
120
|
f.default_space ??= id;
|
|
117
121
|
}
|
|
118
122
|
if (f.default_space && !f.spaces[f.default_space])
|
|
@@ -125,6 +129,8 @@ export function migrate(raw) {
|
|
|
125
129
|
export function loadAll() {
|
|
126
130
|
if (!existsSync(CONFIG_PATH))
|
|
127
131
|
return null;
|
|
132
|
+
if (!statSync(CONFIG_PATH).isFile())
|
|
133
|
+
throw new Error(`${CONFIG_PATH} is not a plain file — remove it and set up again (pantheon doctor)`);
|
|
128
134
|
return migrate(JSON.parse(readFileSync(CONFIG_PATH, "utf8").replace(/^\uFEFF/, "")));
|
|
129
135
|
}
|
|
130
136
|
export function saveAll(f) {
|
|
@@ -250,7 +256,10 @@ function view(f, m, repo) {
|
|
|
250
256
|
publicKey: f.publicKey, privateKey: f.privateKey,
|
|
251
257
|
...(f.display ? { display: f.display } : {}), ...(f.agent ? { agent: f.agent } : {}),
|
|
252
258
|
...(f.account ? { account: f.account } : {}), ...(f.pendingAuth ? { pendingAuth: f.pendingAuth } : {}),
|
|
253
|
-
...(
|
|
259
|
+
...(f.delegate ? { delegate: f.delegate } : {}),
|
|
260
|
+
...(m.since ? { joinedWith: m.since } : {}),
|
|
261
|
+
...(m.pin_team ? { pinTeam: m.pin_team } : {}),
|
|
262
|
+
...(m.id.startsWith("legacy:") ? (f.spaceId ? { spaceId: f.spaceId } : {}) : { spaceId: m.id }),
|
|
254
263
|
...(repo ? { repo: repo.fingerprint, ...(repo.origin ? { repoOrigin: repo.origin } : {}) } : {}),
|
|
255
264
|
};
|
|
256
265
|
}
|
|
@@ -261,18 +270,23 @@ function envView(onDisk) {
|
|
|
261
270
|
const e = process.env;
|
|
262
271
|
if (!(e.PANTHEON_RELAY && e.PANTHEON_SPACE && e.PANTHEON_MEMBER))
|
|
263
272
|
return null;
|
|
264
|
-
const
|
|
265
|
-
? Object.values(onDisk.spaces ?? {}).find((m) => (e.PANTHEON_SPACE_ID && m.id === e.PANTHEON_SPACE_ID) || (m.relay === e.PANTHEON_RELAY && m.name === e.PANTHEON_SPACE))
|
|
266
|
-
: undefined
|
|
273
|
+
const mine = onDisk?.member === e.PANTHEON_MEMBER
|
|
274
|
+
? Object.values(onDisk.spaces ?? {}).find((m) => (e.PANTHEON_SPACE_ID && m.id === e.PANTHEON_SPACE_ID) || (m.relay === e.PANTHEON_RELAY && m.name === e.PANTHEON_SPACE))
|
|
275
|
+
: undefined;
|
|
276
|
+
const token = e.PANTHEON_TOKEN ?? mine?.token;
|
|
267
277
|
if (!token)
|
|
268
278
|
return null;
|
|
269
279
|
const pub = e.PANTHEON_PUBKEY ?? onDisk?.publicKey;
|
|
270
280
|
const priv = e.PANTHEON_PRIVKEY ?? onDisk?.privateKey;
|
|
271
|
-
|
|
281
|
+
const delegate = onDisk?.member === e.PANTHEON_MEMBER ? onDisk?.delegate : undefined;
|
|
282
|
+
if (!pub || (!priv && !delegate))
|
|
272
283
|
return null;
|
|
273
284
|
return {
|
|
274
285
|
relay: e.PANTHEON_RELAY, space: e.PANTHEON_SPACE, token, member: e.PANTHEON_MEMBER,
|
|
275
|
-
publicKey: pub, privateKey: priv, display: e.PANTHEON_DISPLAY, agent: e.PANTHEON_AGENT,
|
|
286
|
+
publicKey: pub, privateKey: priv ?? "", display: e.PANTHEON_DISPLAY, agent: e.PANTHEON_AGENT,
|
|
287
|
+
...(delegate ? { delegate } : {}),
|
|
288
|
+
...(mine?.pin_team ? { pinTeam: mine.pin_team } : {}),
|
|
289
|
+
...(mine?.since ? { joinedWith: mine.since } : {}),
|
|
276
290
|
...(e.PANTHEON_SPACE_ID ? { spaceId: e.PANTHEON_SPACE_ID } : {}),
|
|
277
291
|
...(e.PANTHEON_REPO ? { repo: e.PANTHEON_REPO } : {}),
|
|
278
292
|
};
|
|
@@ -295,6 +309,14 @@ export function resolveFor(cwd = process.cwd(), opts = {}) {
|
|
|
295
309
|
return { cfg: applyAgentEnv(view(f, m, repo)), reason: "binding", memberships: n, binding: { ...b, fingerprint: repo.fingerprint }, repo };
|
|
296
310
|
return { cfg: null, reason: "stale", memberships: n, binding: { ...b, fingerprint: repo.fingerprint }, repo };
|
|
297
311
|
}
|
|
312
|
+
const owners = f.unbound?.includes(repo.fingerprint) ? [] : Object.values(spaces).filter((m) => m.repos?.some((r) => r.fp === repo.fingerprint));
|
|
313
|
+
if (owners.length === 1) {
|
|
314
|
+
const m = owners[0];
|
|
315
|
+
const binding = { space: m.id, path: repo.root, origin: repo.origin, bound_at: 0, fingerprint: repo.fingerprint };
|
|
316
|
+
return { cfg: applyAgentEnv(view(f, m, repo)), reason: "binding", memberships: n, binding, repo, autoBound: true };
|
|
317
|
+
}
|
|
318
|
+
if (owners.length > 1)
|
|
319
|
+
return { cfg: null, reason: "unbound", memberships: n, repo, claimedBy: owners.map((m) => m.name) };
|
|
298
320
|
}
|
|
299
321
|
if (!repo) {
|
|
300
322
|
const fb = folderBinding(f, cwd);
|
|
@@ -402,6 +424,34 @@ export function save(cfg) {
|
|
|
402
424
|
}
|
|
403
425
|
return saveAll(f);
|
|
404
426
|
}
|
|
427
|
+
export function rememberTeamRepos(team, repos) {
|
|
428
|
+
if (!existsSync(CONFIG_PATH))
|
|
429
|
+
return false;
|
|
430
|
+
const next = repos.map((r) => ({ fp: r.fingerprint, name: r.origin ? teamRepoName(r.origin) : r.fingerprint })).sort((a, b) => a.fp.localeCompare(b.fp));
|
|
431
|
+
try {
|
|
432
|
+
return withConfigLock(() => {
|
|
433
|
+
let f;
|
|
434
|
+
try {
|
|
435
|
+
f = loadAll();
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
const m = f && Object.values(f.spaces ?? {}).find((x) => (team.spaceId && x.id === team.spaceId) || (x.name === team.space && x.relay === team.relay));
|
|
441
|
+
if (!f || !m)
|
|
442
|
+
return false;
|
|
443
|
+
if (JSON.stringify(m.repos ?? []) === JSON.stringify(next))
|
|
444
|
+
return false;
|
|
445
|
+
m.repos = next;
|
|
446
|
+
saveAll(f);
|
|
447
|
+
return true;
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
catch {
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
export const teamRepoName = (origin) => repoName(origin);
|
|
405
455
|
export function updateMembershipToken(space, oldToken, newToken) {
|
|
406
456
|
if (!space || !oldToken || !newToken || oldToken === newToken)
|
|
407
457
|
return false;
|
|
@@ -430,6 +480,24 @@ export function updateMembershipToken(space, oldToken, newToken) {
|
|
|
430
480
|
return true;
|
|
431
481
|
});
|
|
432
482
|
}
|
|
483
|
+
export function adoptSpaceId(relay, name, id) {
|
|
484
|
+
if (!/^[A-Za-z0-9_:.-]{1,64}$/.test(id) || !existsSync(CONFIG_PATH))
|
|
485
|
+
return false;
|
|
486
|
+
const f = loadAll();
|
|
487
|
+
if (!f?.spaces)
|
|
488
|
+
return false;
|
|
489
|
+
const legacy = legacySpaceId(relay, name);
|
|
490
|
+
const m = f.spaces[legacy];
|
|
491
|
+
if (!m)
|
|
492
|
+
return false;
|
|
493
|
+
if (f.spaces[id])
|
|
494
|
+
return "conflict";
|
|
495
|
+
delete f.spaces[legacy];
|
|
496
|
+
f.spaces[id] = { ...m, id, pin_team: m.pin_team ?? `${name}@${relay}` };
|
|
497
|
+
adoptId(f, legacy, id);
|
|
498
|
+
saveAll(f);
|
|
499
|
+
return true;
|
|
500
|
+
}
|
|
433
501
|
function adoptId(f, oldId, newId) {
|
|
434
502
|
for (const b of Object.values(f.bindings ?? {}))
|
|
435
503
|
if (b.space === oldId)
|
|
@@ -476,6 +544,8 @@ export function bindRepo(repo, spaceId) {
|
|
|
476
544
|
f.bindings ??= {};
|
|
477
545
|
const prev = f.bindings[repo.fingerprint] ?? null;
|
|
478
546
|
f.bindings[repo.fingerprint] = { space: spaceId, path: repo.root, origin: repo.origin, bound_at: Date.now() };
|
|
547
|
+
if (f.unbound?.includes(repo.fingerprint))
|
|
548
|
+
f.unbound = f.unbound.filter((x) => x !== repo.fingerprint);
|
|
479
549
|
saveAll(f);
|
|
480
550
|
return { replaced: prev && prev.space !== spaceId ? prev : null };
|
|
481
551
|
}
|
|
@@ -484,6 +554,8 @@ export function unbindRepo(fingerprint) {
|
|
|
484
554
|
if (!f?.bindings?.[fingerprint])
|
|
485
555
|
return false;
|
|
486
556
|
delete f.bindings[fingerprint];
|
|
557
|
+
if (/^[rl]_/.test(fingerprint))
|
|
558
|
+
f.unbound = [...new Set([...(f.unbound ?? []), fingerprint])].slice(-200);
|
|
487
559
|
saveAll(f);
|
|
488
560
|
return true;
|
|
489
561
|
}
|
package/dist/continued.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { CONFIG_DIR } from "./config.js";
|
|
4
|
+
import { readStateFile, withFileLock } from "./file-lock.js";
|
|
5
|
+
import { writeWhole } from "./pins.js";
|
|
4
6
|
const KEEP = 500;
|
|
5
7
|
export function continuedPath(home) {
|
|
6
8
|
return join(home ?? CONFIG_DIR, "continued.json");
|
|
7
9
|
}
|
|
8
10
|
function readAll(home) {
|
|
9
11
|
try {
|
|
10
|
-
const
|
|
11
|
-
if (
|
|
12
|
+
const t = readStateFile(continuedPath(home));
|
|
13
|
+
if (t === null)
|
|
12
14
|
return {};
|
|
13
|
-
const v = JSON.parse(
|
|
15
|
+
const v = JSON.parse(t);
|
|
14
16
|
return v && typeof v === "object" ? v : {};
|
|
15
17
|
}
|
|
16
18
|
catch {
|
|
@@ -20,15 +22,45 @@ function readAll(home) {
|
|
|
20
22
|
export function loadContinued(space, home) {
|
|
21
23
|
return new Set(readAll(home)[space] ?? []);
|
|
22
24
|
}
|
|
25
|
+
export function handledIn(h) {
|
|
26
|
+
return [...new Set(Object.values(readAll(h)).flatMap((l) => (Array.isArray(l) ? l : [])).filter((x) => typeof x === "string" && x.startsWith("handled:")))];
|
|
27
|
+
}
|
|
23
28
|
export function markContinued(space, id, home) {
|
|
24
29
|
try {
|
|
25
|
-
const all = readAll(home);
|
|
26
|
-
const list = (all[space] ?? []).filter((x) => x !== id);
|
|
27
|
-
list.push(id);
|
|
28
|
-
all[space] = list.slice(-KEEP);
|
|
29
30
|
const p = continuedPath(home);
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
const write = () => {
|
|
32
|
+
const all = readAll(home);
|
|
33
|
+
const list = (all[space] ?? []).filter((x) => x !== id);
|
|
34
|
+
list.push(id);
|
|
35
|
+
all[space] = list.slice(-KEEP);
|
|
36
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
37
|
+
writeWhole(p, JSON.stringify(all));
|
|
38
|
+
};
|
|
39
|
+
try {
|
|
40
|
+
withFileLock(p, write);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
write();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch { }
|
|
47
|
+
}
|
|
48
|
+
export function unmarkContinued(space, id, home) {
|
|
49
|
+
try {
|
|
50
|
+
const p = continuedPath(home);
|
|
51
|
+
const write = () => {
|
|
52
|
+
const all = readAll(home);
|
|
53
|
+
if (!(all[space] ?? []).includes(id))
|
|
54
|
+
return;
|
|
55
|
+
all[space] = (all[space] ?? []).filter((x) => x !== id);
|
|
56
|
+
writeWhole(p, JSON.stringify(all));
|
|
57
|
+
};
|
|
58
|
+
try {
|
|
59
|
+
withFileLock(p, write);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
write();
|
|
63
|
+
}
|
|
32
64
|
}
|
|
33
65
|
catch { }
|
|
34
66
|
}
|