@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
package/dist/client.js CHANGED
@@ -1,5 +1,13 @@
1
1
  import WebSocket from "ws";
2
- import { signChallenge } from "./identity.js";
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.enrolledToken = msg.token;
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
- this.seen.set(msg.act.id, msg.act);
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(() => { }); }, Math.round(this.backoff * (0.5 + Math.random())));
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 r = await this.rpc({ t: "act", ...a, ...(meta ? { meta } : {}), ...(repo ? { repo } : {}) });
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
- return this.rpc({ t: "open_goal", acceptance: "", ...g });
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
- return r ? `${r.act} from ${r.from}${r.goal ? ` (goal ${r.goal})` : ""} id=${r.id}\n${r.body}` : null;
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