@join-pantheon/cli 0.30.0 → 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 +114 -35
- package/dist/agents.js +41 -17
- package/dist/claude-confine.js +37 -21
- package/dist/cli.js +408 -34
- 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/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 +206 -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 +13 -1
- 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 +48 -1
- package/package.json +1 -1
package/dist/pins.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { accessSync, constants as fsConstants, existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { readStateFile, withFileLock } from "./file-lock.js";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { CONFIG_PATH } from "./config.js";
|
|
6
|
+
import { canonicalKey, fingerprint } from "./identity.js";
|
|
7
|
+
export const PINS_FILE = "keys.json";
|
|
8
|
+
const home = (h) => h ?? process.env.PANTHEON_HOME ?? dirname(CONFIG_PATH);
|
|
9
|
+
export const pinsPath = (h) => join(home(h), PINS_FILE);
|
|
10
|
+
const isObj = (x) => !!x && typeof x === "object" && !Array.isArray(x);
|
|
11
|
+
export const unsafeKey = (k) => k === "__proto__" || k === "constructor" || k === "prototype";
|
|
12
|
+
const dict = () => Object.create(null);
|
|
13
|
+
export function loadPins(h) {
|
|
14
|
+
let text;
|
|
15
|
+
try {
|
|
16
|
+
const t = readStateFile(pinsPath(h));
|
|
17
|
+
if (t === null)
|
|
18
|
+
return { v: 1, teams: dict() };
|
|
19
|
+
accessSync(pinsPath(h), fsConstants.W_OK);
|
|
20
|
+
text = t;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return { v: 1, teams: {}, corrupt: true };
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const raw = JSON.parse(text);
|
|
27
|
+
if (!isObj(raw) || raw.v !== 1 || !isObj(raw.teams))
|
|
28
|
+
return { v: 1, teams: {}, corrupt: true };
|
|
29
|
+
const teams = dict();
|
|
30
|
+
for (const [id, t] of Object.entries(raw.teams)) {
|
|
31
|
+
if (!isObj(t) || !isObj(t.members) || unsafeKey(id))
|
|
32
|
+
return { v: 1, teams: {}, corrupt: true };
|
|
33
|
+
const members = dict();
|
|
34
|
+
for (const [m, p] of Object.entries(t.members)) {
|
|
35
|
+
if (!isObj(p) || typeof p.fp !== "string" || unsafeKey(m))
|
|
36
|
+
return { v: 1, teams: {}, corrupt: true };
|
|
37
|
+
members[m] = p;
|
|
38
|
+
}
|
|
39
|
+
teams[id] = { members, ...(typeof t.sigs === "number" ? { sigs: t.sigs } : {}), ...(typeof t.spaceId === "string" ? { spaceId: t.spaceId } : {}) };
|
|
40
|
+
}
|
|
41
|
+
return { v: 1, teams };
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return { v: 1, teams: {}, corrupt: true };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function savePins(p, h) {
|
|
48
|
+
if (p.corrupt)
|
|
49
|
+
throw new Error(`${pinsPath(h)} could not be read, so it is not written over — fix or remove it (pantheon doctor)`);
|
|
50
|
+
const f = pinsPath(h);
|
|
51
|
+
mkdirSync(dirname(f), { recursive: true, mode: 0o700 });
|
|
52
|
+
writeWhole(f, JSON.stringify(p, null, 2) + "\n");
|
|
53
|
+
}
|
|
54
|
+
export function updatePins(fn, h) {
|
|
55
|
+
return withFileLock(pinsPath(h), () => {
|
|
56
|
+
const p = loadPins(h);
|
|
57
|
+
if (fn(p) && !p.corrupt)
|
|
58
|
+
savePins(p, h);
|
|
59
|
+
return p;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
export { withFileLock };
|
|
63
|
+
export function writeWhole(f, text) {
|
|
64
|
+
const tmp = `${f}.${randomBytes(8).toString("hex")}.tmp`;
|
|
65
|
+
try {
|
|
66
|
+
writeFileSync(tmp, text, { mode: 0o600, flag: "wx" });
|
|
67
|
+
renameSync(tmp, f);
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
try {
|
|
71
|
+
rmSync(tmp, { force: true });
|
|
72
|
+
}
|
|
73
|
+
catch { }
|
|
74
|
+
throw e;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function observeMembers(p, team, members, me, at = Date.now()) {
|
|
78
|
+
const t = (p.teams[team] ??= { members: dict() });
|
|
79
|
+
const out = [];
|
|
80
|
+
const mine = fingerprint(me.publicKey);
|
|
81
|
+
if (t.members[me.member]?.fp !== mine)
|
|
82
|
+
t.members[me.member] = { fp: mine, key: me.publicKey, since: at };
|
|
83
|
+
for (const m of members) {
|
|
84
|
+
if (!m.admitted || m.member === me.member || typeof m.member !== "string" || unsafeKey(m.member))
|
|
85
|
+
continue;
|
|
86
|
+
const fp = m.fingerprint;
|
|
87
|
+
const key = m.pubkey && canonicalKey(m.pubkey) && fingerprint(m.pubkey) === fp ? m.pubkey : undefined;
|
|
88
|
+
const cur = t.members[m.member];
|
|
89
|
+
if (!cur) {
|
|
90
|
+
t.members[m.member] = { fp, ...(key ? { key } : {}), since: at };
|
|
91
|
+
out.push({ member: m.member, kind: "pinned", fp });
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (cur.fp === fp) {
|
|
95
|
+
if (key && !cur.key)
|
|
96
|
+
cur.key = key;
|
|
97
|
+
if (cur.pending)
|
|
98
|
+
delete cur.pending;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (cur.pending?.fp !== fp) {
|
|
102
|
+
cur.pending = { fp, ...(key ? { key } : {}), since: at };
|
|
103
|
+
out.push({ member: m.member, kind: "changed", fp, was: cur.fp });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
export const pinFor = (p, team, member) => p.teams[team]?.members[member];
|
|
109
|
+
export const pinScope = (team, me) => `${team} ${me}`;
|
|
110
|
+
export function joinedSigning(since) {
|
|
111
|
+
const m = /^(\d+)\.(\d+)\.(\d+)/.exec(since ?? "");
|
|
112
|
+
if (!m)
|
|
113
|
+
return false;
|
|
114
|
+
const [maj, min] = [Number(m[1]), Number(m[2])];
|
|
115
|
+
return maj > 0 || min >= 31;
|
|
116
|
+
}
|
|
117
|
+
export function teamPinId(cfg) {
|
|
118
|
+
return cfg.pinTeam ?? cfg.spaceId ?? `${cfg.space}@${cfg.relay}`;
|
|
119
|
+
}
|
|
120
|
+
export function trustPending(p, team, member) {
|
|
121
|
+
const cur = p.teams[team]?.members[member];
|
|
122
|
+
if (!cur?.pending)
|
|
123
|
+
return null;
|
|
124
|
+
const next = { fp: cur.pending.fp, ...(cur.pending.key ? { key: cur.pending.key } : {}), since: Date.now(), ...(cur.signs ? { signs: cur.signs } : {}) };
|
|
125
|
+
p.teams[team].members[member] = next;
|
|
126
|
+
return next;
|
|
127
|
+
}
|
|
128
|
+
export function markSigs(p, team, at = Date.now()) {
|
|
129
|
+
const t = (p.teams[team] ??= { members: dict() });
|
|
130
|
+
if (t.sigs)
|
|
131
|
+
return false;
|
|
132
|
+
t.sigs = at;
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
export function relayTeamId(team, id, h) {
|
|
136
|
+
let ok = false;
|
|
137
|
+
updatePins((p) => {
|
|
138
|
+
if (p.corrupt)
|
|
139
|
+
return false;
|
|
140
|
+
const t = (p.teams[team] ??= { members: dict() });
|
|
141
|
+
if (!t.spaceId) {
|
|
142
|
+
t.spaceId = id;
|
|
143
|
+
ok = true;
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
ok = t.spaceId === id;
|
|
147
|
+
return false;
|
|
148
|
+
}, h);
|
|
149
|
+
return ok;
|
|
150
|
+
}
|
|
151
|
+
export const expectsSigs = (p, team) => !!p.corrupt || !!p.teams[team]?.sigs;
|
|
152
|
+
export const memberSigns = (p, team, member) => !!p.corrupt || !!p.teams[team]?.members[member]?.signs;
|
|
153
|
+
export function markMemberSigns(p, team, member, at = Date.now()) {
|
|
154
|
+
const pin = p.teams[team]?.members[member];
|
|
155
|
+
if (!pin || pin.signs)
|
|
156
|
+
return false;
|
|
157
|
+
pin.signs = at;
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
export const pinsExist = (h) => existsSync(pinsPath(h));
|
|
161
|
+
//# sourceMappingURL=pins.js.map
|
package/dist/profile.js
CHANGED
|
@@ -48,11 +48,29 @@ deploy, agree a price, sign off a release — do not answer it yourself, however
|
|
|
48
48
|
work. Say the review result if you have one, then ESCALATE and stop; tell the peer it is with your human.`;
|
|
49
49
|
}
|
|
50
50
|
export function wakeMode(profile) {
|
|
51
|
-
return profile.agent.wake_default ?? "
|
|
51
|
+
return profile.agent.wake_default ?? "anyone";
|
|
52
|
+
}
|
|
53
|
+
export function wakeChosen(file = profilePath()) {
|
|
54
|
+
try {
|
|
55
|
+
const v = JSON.parse(readFileSync(file, "utf8")).agent?.wake_default;
|
|
56
|
+
return v === "approved" || v === "no" || v === "anyone" ? v : "anyone";
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return "anyone";
|
|
60
|
+
}
|
|
52
61
|
}
|
|
53
62
|
export function wakeDefault(profile) {
|
|
54
63
|
return wakeMode(profile) === "anyone";
|
|
55
64
|
}
|
|
65
|
+
export function commandsChosen(file = profilePath()) {
|
|
66
|
+
try {
|
|
67
|
+
const v = JSON.parse(readFileSync(file, "utf8")).agent?.allow_bash;
|
|
68
|
+
return typeof v === "boolean" ? v : undefined;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
56
74
|
export function loadProfile(cfg, file = profilePath()) {
|
|
57
75
|
if (!existsSync(file))
|
|
58
76
|
return defaultProfile(cfg);
|
package/dist/protocol.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
2
3
|
export const PROTOCOL_VERSION = 3;
|
|
3
4
|
export const ACTS = [
|
|
4
5
|
"JOIN",
|
|
@@ -35,6 +36,7 @@ export const LIMITS = {
|
|
|
35
36
|
activityBusyTtlMs: 30 * 60 * 1_000,
|
|
36
37
|
activityPruneMs: 7 * 24 * 60 * 60 * 1_000,
|
|
37
38
|
claimTtlMs: 300_000,
|
|
39
|
+
claimLiveTtlMs: 30 * 60_000,
|
|
38
40
|
workerFallbackMs: 20_000 + 120_000 + 60_000,
|
|
39
41
|
pendingTimeoutMs: 1_500,
|
|
40
42
|
cursorTtlMs: 7 * 24 * 60 * 60 * 1_000,
|
|
@@ -80,6 +82,37 @@ function loopback(host) {
|
|
|
80
82
|
return "0.0.0.0";
|
|
81
83
|
return null;
|
|
82
84
|
}
|
|
85
|
+
export const DelegateCertSchema = z.object({
|
|
86
|
+
pub: z.string().min(32).max(200),
|
|
87
|
+
run: z.string().min(1).max(64),
|
|
88
|
+
expires: z.number().int().positive(),
|
|
89
|
+
sig: z.string().min(32).max(200),
|
|
90
|
+
});
|
|
91
|
+
export const ActSigSchema = z.object({
|
|
92
|
+
v: z.literal(1),
|
|
93
|
+
role: z.enum(["member", "run"]),
|
|
94
|
+
nonce: z.string().min(8).max(64),
|
|
95
|
+
ts: z.number().int().positive(),
|
|
96
|
+
pub: z.string().min(32).max(200),
|
|
97
|
+
s: z.string().min(32).max(200),
|
|
98
|
+
meta: z.record(z.unknown()).optional(),
|
|
99
|
+
repo: z.string().regex(/^[rl]_[0-9a-f]{16}$/).optional(),
|
|
100
|
+
cert: DelegateCertSchema.optional(),
|
|
101
|
+
});
|
|
102
|
+
export const GoalSigSchema = z.object({
|
|
103
|
+
v: z.literal(1),
|
|
104
|
+
role: z.enum(["member", "run"]),
|
|
105
|
+
nonce: z.string().min(8).max(64),
|
|
106
|
+
ts: z.number().int().positive(),
|
|
107
|
+
pub: z.string().min(32).max(200),
|
|
108
|
+
s: z.string().min(32).max(200),
|
|
109
|
+
invite: z.array(z.string().min(1).max(48)).max(16).nullable(),
|
|
110
|
+
budget: z.number().int().nullable().optional(),
|
|
111
|
+
deadline: z.number().int().nullable().optional(),
|
|
112
|
+
kind: z.string().max(32).nullable().optional(),
|
|
113
|
+
cert: DelegateCertSchema.optional(),
|
|
114
|
+
});
|
|
115
|
+
export const RELAY_FEATURES = ["act-sig", "delegate"];
|
|
83
116
|
export const Hello = z.object({
|
|
84
117
|
t: z.literal("hello"),
|
|
85
118
|
v: z.number().int().optional(),
|
|
@@ -98,6 +131,7 @@ export const Hello = z.object({
|
|
|
98
131
|
from_seq: z.number().int().nonnegative().optional(),
|
|
99
132
|
repo: z.string().regex(/^[rl]_[0-9a-f]{16}$/).optional(),
|
|
100
133
|
repo_origin: z.string().max(200).optional(),
|
|
134
|
+
delegate: DelegateCertSchema.optional(),
|
|
101
135
|
});
|
|
102
136
|
export const OrganizationProfileSchema = z.object({
|
|
103
137
|
version: z.literal(1),
|
|
@@ -147,12 +181,14 @@ export const Admit = z.object({
|
|
|
147
181
|
member: z.string().min(1).max(48),
|
|
148
182
|
may_wake: z.boolean().default(false),
|
|
149
183
|
revoke: z.boolean().default(false),
|
|
184
|
+
wake_only: z.boolean().optional(),
|
|
150
185
|
forget: z.boolean().default(false),
|
|
151
186
|
});
|
|
152
187
|
export const SendAct = z.object({
|
|
153
188
|
t: z.literal("act"),
|
|
154
189
|
cid: z.string().max(64).optional(),
|
|
155
190
|
after_seq: z.number().int().nonnegative().optional(),
|
|
191
|
+
id: z.string().regex(/^a_[A-Za-z0-9]{6,48}$/).optional(),
|
|
156
192
|
act: z.enum(ACTS),
|
|
157
193
|
body: z.string().max(LIMITS.bodyChars),
|
|
158
194
|
summary: z.string().max(LIMITS.summaryChars).optional(),
|
|
@@ -163,6 +199,7 @@ export const SendAct = z.object({
|
|
|
163
199
|
affects: z.array(z.string().min(1).max(48)).max(16).optional(),
|
|
164
200
|
meta: z.record(z.unknown()).optional(),
|
|
165
201
|
repo: z.string().regex(/^[rl]_[0-9a-f]{16}$/).optional(),
|
|
202
|
+
sig: ActSigSchema.optional(),
|
|
166
203
|
});
|
|
167
204
|
export const BindRepo = z.object({
|
|
168
205
|
t: z.literal("bind_repo"),
|
|
@@ -174,12 +211,14 @@ export const BindRepo = z.object({
|
|
|
174
211
|
export const OpenGoal = z.object({
|
|
175
212
|
t: z.literal("open_goal"),
|
|
176
213
|
cid: z.string().max(64).optional(),
|
|
214
|
+
id: z.string().regex(/^g_[a-z0-9]{8,40}$/).optional(),
|
|
177
215
|
title: z.string().min(1).max(200),
|
|
178
216
|
acceptance: z.string().max(2000).default(""),
|
|
179
217
|
invite: z.array(z.string().min(1).max(48)).max(16).optional(),
|
|
180
218
|
budget: z.number().int().min(4).max(1000).optional(),
|
|
181
219
|
deadline: z.number().int().positive().optional(),
|
|
182
220
|
kind: z.enum(["worklist"]).optional(),
|
|
221
|
+
sig: GoalSigSchema.optional(),
|
|
183
222
|
});
|
|
184
223
|
export const Ping = z.object({ t: z.literal("ping") });
|
|
185
224
|
export const DeleteSpace = z.object({
|
|
@@ -223,6 +262,6 @@ export function isContinuationFor(act, me, ctx) {
|
|
|
223
262
|
&& ctx.goalMembers.includes(me);
|
|
224
263
|
}
|
|
225
264
|
export function newId(prefix) {
|
|
226
|
-
return `${prefix}_${Date.now().toString(36)}${
|
|
265
|
+
return `${prefix}_${Date.now().toString(36)}${randomBytes(5).readUIntBE(0, 5).toString(36).padStart(8, "0").slice(0, 8)}`;
|
|
227
266
|
}
|
|
228
267
|
//# sourceMappingURL=protocol.js.map
|
package/dist/quickstart.js
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
3
|
import { createServer } from "node:net";
|
|
4
|
-
import { basename, join as pathJoin } from "node:path";
|
|
4
|
+
import { basename, dirname, join as pathJoin, resolve as resolvePath } from "node:path";
|
|
5
5
|
import { homedir, hostname, platform, userInfo } from "node:os";
|
|
6
6
|
import { lookup } from "node:dns/promises";
|
|
7
7
|
import { describeNetError } from "./network.js";
|
|
8
8
|
import { PantheonClient } from "./client.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { holdsRepo } from "./repo-find.js";
|
|
10
|
+
import { load, loadAll, identity, save, toInvite, resolveFor, repoInfo, bindRepo, bindFolder, folderBinding, DEFAULT_RELAY } from "./config.js";
|
|
11
|
+
import { commandsChosen, loadProfile, wakeChosen, wakeDefault } from "./profile.js";
|
|
11
12
|
import { newKeypair } from "./identity.js";
|
|
12
13
|
import { installForUser } from "./install.js";
|
|
13
14
|
import { writeIntent, installHooks } from "./notify.js";
|
|
14
|
-
import { detectAvailable, agentById } from "./agents.js";
|
|
15
|
+
import { detectAvailable, agentById, agentsNeedingUnsandboxed } from "./agents.js";
|
|
15
16
|
import { installService, servicesUnavailable } from "./service.js";
|
|
16
17
|
import { ensureNativeNotifier } from "./notifier.js";
|
|
17
18
|
import { ensureTrayBinary, writeTrayCommands } from "./tray.js";
|
|
18
19
|
import { desktopUnavailable, ephemeralWarning, hostEnv } from "./host-env.js";
|
|
19
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
20
21
|
import { localAddresses } from "./machine.js";
|
|
21
|
-
import { grant as grantCap, loadPolicy, policyPath, savePolicy } from "./worker-policy.js";
|
|
22
|
+
import { grant as grantCap, loadPolicy, policyPath, savePolicy, unsandboxedAllowed } from "./worker-policy.js";
|
|
22
23
|
import { Relay } from "./relay.js";
|
|
23
24
|
import { which } from "./which.js";
|
|
24
25
|
const sh = (cmd) => {
|
|
@@ -121,23 +122,68 @@ export function wireUp(opts, deps = {}) {
|
|
|
121
122
|
catch {
|
|
122
123
|
return false;
|
|
123
124
|
} })();
|
|
124
|
-
|
|
125
|
+
const boundFolder = !isRepo && (() => { try {
|
|
126
|
+
const f = loadAll();
|
|
127
|
+
return !!f && !!folderBinding(f, project);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return false;
|
|
131
|
+
} })();
|
|
132
|
+
if (boundFolder) {
|
|
133
|
+
try {
|
|
134
|
+
const why = notLoaded(installSvc("worker", project, []));
|
|
135
|
+
if (why) {
|
|
136
|
+
tools.push({ tool: "worker", status: `installed, not running: ${why}` });
|
|
137
|
+
warnings.push(`the worker is installed but not running (${why}) — this machine cannot answer the team while you are away. Try \`pantheon service restart worker\``);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
tools.push({ tool: "worker", status: `on — answers peers from this folder while you are away (it reads it; anything it writes goes to a drafts folder for you)` });
|
|
141
|
+
granted.push(`${wakeChosen() === "approved" ? "A teammate you approve from this machine (pantheon wake allow <name>)" : "Your teammates"} can start an agent run that READS ${project} and answers them. It never changes that folder: drafts go to a folder of its own for you to review.`);
|
|
142
|
+
const hasClaude = (() => { try {
|
|
143
|
+
return detectAvailable().some((a) => a.backend.id === "claude-code");
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return true;
|
|
147
|
+
} })();
|
|
148
|
+
if (!hasClaude)
|
|
149
|
+
warnings.push(`answering from a plain folder while you are away needs Claude Code (claude), and it is not installed here — until it is, teammates' requests for this folder wait for your live session`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
tools.push({ tool: "worker", status: `could not install: ${e.message}` });
|
|
154
|
+
warnings.push(`the worker did not install: ${e.message}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else if (!isRepo && how === "explicit") {
|
|
125
158
|
tools.push({ tool: "worker", status: `not installed — ${project} is not a git repo` });
|
|
126
159
|
warnings.push(`--unattended needs a git repo to work in; pass --project <a clone>. Nothing was installed.`);
|
|
127
160
|
}
|
|
128
161
|
else if (!isRepo) {
|
|
129
|
-
tools.push({ tool: "worker", status: `not on —
|
|
162
|
+
tools.push({ tool: "worker", status: `not on yet — nothing here is bound to the team. Bind a repo or docs folder (pantheon repo find, or pantheon use <team> --here), then: pantheon service install worker` });
|
|
130
163
|
}
|
|
131
164
|
else {
|
|
132
165
|
try {
|
|
133
|
-
const
|
|
166
|
+
const commandsOn = commandsChosen() !== false;
|
|
167
|
+
const why = notLoaded(installSvc("worker", project, commandsOn ? ["--allow-bash"] : []));
|
|
134
168
|
if (why) {
|
|
135
169
|
tools.push({ tool: "worker", status: `installed, not running: ${why}` });
|
|
136
170
|
warnings.push(`the worker is installed but not running (${why}) — this machine cannot answer the team while you are away. Try \`pantheon service restart worker\``);
|
|
137
171
|
return { tools, granted, warnings };
|
|
138
172
|
}
|
|
139
173
|
tools.push({ tool: "worker", status: `on — answers peers while you are away (${project})` });
|
|
140
|
-
granted.push(
|
|
174
|
+
granted.push(wakeChosen() === "approved"
|
|
175
|
+
? `A teammate you approve here can start an agent run on this machine, in an isolated worktree under ${project}: you are asked the first time each one's request needs it (Always / Just this once / No), or approve ahead with pantheon wake allow <name>. Under wake "anyone", every teammate the team lets wake can.`
|
|
176
|
+
: `Your teammates can hand work to this machine while you're away, so nothing waits on you: it runs in its own copy of ${project} and delivers on a branch for your review. To approve each teammate first: pantheon setup --wake approved`);
|
|
177
|
+
if (commandsOn)
|
|
178
|
+
granted.push(`That work runs your tests, commits, and pushes to its own branch, so it arrives ready to review. Edits only instead: pantheon setup --commands no`);
|
|
179
|
+
const away = (() => { try {
|
|
180
|
+
return agentsNeedingUnsandboxed();
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return [];
|
|
184
|
+
} })();
|
|
185
|
+
if (away.length && unsandboxedAllowed(loadPolicy()))
|
|
186
|
+
granted.push(`${away.join(", ")} ${away.length === 1 ? "answers" : "answer"} those requests, so teammates get answers even when you're offline. To have them wait for you instead: pantheon setup --unsandboxed no`);
|
|
141
187
|
if (!existsSync(policyPath())) {
|
|
142
188
|
const pol = loadPolicy();
|
|
143
189
|
grantCap(pol, "self_upgrade", "always", "setup-default");
|
|
@@ -357,8 +403,7 @@ export async function start(opts) {
|
|
|
357
403
|
const reach = chosen
|
|
358
404
|
? (place === "loopback"
|
|
359
405
|
? { url: chosen, how: "as given", shareable: false,
|
|
360
|
-
hint: `
|
|
361
|
-
` Use a tailnet address (tailscale ip -4) or a tunnel, then re-run pantheon start.` }
|
|
406
|
+
hint: `A teammate cannot reach it. Use a tailnet address (tailscale ip -4) or a tunnel, then re-run pantheon start.` }
|
|
362
407
|
: { url: chosen, how: place === "this-machine" ? "this machine's address" : place === "tunnel" ? "a tunnel to this machine" : opts.relay ? "as given" : "your signed-in relay", shareable: true })
|
|
363
408
|
: findReach(opts.port);
|
|
364
409
|
const nothing = (why) => ({ cfg: priorCfg ?? { relay: reach.url }, invite: null, inviteMayWake: false, claimed: false, reach, tools: [], granted: [], relay: null, warnings: [why] });
|
|
@@ -430,6 +475,7 @@ export async function start(opts) {
|
|
|
430
475
|
}
|
|
431
476
|
let invite = null;
|
|
432
477
|
let inviteMayWake = false;
|
|
478
|
+
let folderBound = null;
|
|
433
479
|
let claimed = false;
|
|
434
480
|
const claim = async (c) => {
|
|
435
481
|
const client = new PantheonClient(c);
|
|
@@ -441,6 +487,10 @@ export async function start(opts) {
|
|
|
441
487
|
if (repo && !client.repos.some((r) => r.fingerprint === repo.fingerprint)) {
|
|
442
488
|
await client.bindRepo(repo.fingerprint, repo.origin).catch((e) => warnings.push(`could not register this repo with the team: ${e.message}`));
|
|
443
489
|
}
|
|
490
|
+
if (!repo && c.spaceId && bindableFolder(process.cwd())) {
|
|
491
|
+
bindFolder(process.cwd(), c.spaceId);
|
|
492
|
+
folderBound = process.cwd();
|
|
493
|
+
}
|
|
444
494
|
if (repo && c.spaceId) {
|
|
445
495
|
const { replaced } = bindRepo(repo, c.spaceId);
|
|
446
496
|
const was = replaced ?? (here.binding && here.binding.space !== c.spaceId ? here.binding : null);
|
|
@@ -449,7 +499,7 @@ export async function start(opts) {
|
|
|
449
499
|
warnings.push(`this repo was bound to "${name}"; it now belongs to "${c.space}" — agents opened here speak in "${c.space}" from now on (pantheon use ${name} --here moves it back)`);
|
|
450
500
|
}
|
|
451
501
|
}
|
|
452
|
-
opts.log(` space ${c.space} ${reuse ? "— reconnected" : "— created, you own it"}${repo ? ` ${repo.origin ?? "local repo"} → bound to it` : ""}`);
|
|
502
|
+
opts.log(` space ${c.space} ${reuse ? "— reconnected" : "— created, you own it"}${repo ? ` ${repo.origin ?? "local repo"} → bound to it` : folderBound ? ` this folder → bound to it` : ""}`);
|
|
453
503
|
const mayWake = opts.wake ? opts.wake === "anyone" : wakeDefault(loadProfile(c));
|
|
454
504
|
const inv = await client.makeInvite(mayWake, opts.ttlMinutes);
|
|
455
505
|
invite = toInvite({ ...c, token: inv.url });
|
|
@@ -524,6 +574,10 @@ export async function join(opts) {
|
|
|
524
574
|
cfg.spaceId = client.spaceId;
|
|
525
575
|
save(cfg);
|
|
526
576
|
const warnings = [];
|
|
577
|
+
if (!repo && cfg.spaceId && bindableFolder(process.cwd())) {
|
|
578
|
+
bindFolder(process.cwd(), cfg.spaceId);
|
|
579
|
+
opts.log(` folder ${process.cwd()} → bound to "${cfg.space}" (agents opened here speak in it; no git needed)`);
|
|
580
|
+
}
|
|
527
581
|
if (repo && cfg.spaceId) {
|
|
528
582
|
const { replaced } = bindRepo(repo, cfg.spaceId);
|
|
529
583
|
if (replaced)
|
|
@@ -544,6 +598,33 @@ export async function join(opts) {
|
|
|
544
598
|
});
|
|
545
599
|
return { cfg, tools: wired.tools, granted: wired.granted, warnings: [...warnings, ...wired.warnings] };
|
|
546
600
|
}
|
|
601
|
+
export function bindableFolder(dir) {
|
|
602
|
+
const real = (d) => { try {
|
|
603
|
+
return realpathSync.native(d);
|
|
604
|
+
}
|
|
605
|
+
catch {
|
|
606
|
+
return resolvePath(d);
|
|
607
|
+
} };
|
|
608
|
+
const here = real(dir), home = real(homedir());
|
|
609
|
+
if (dirname(here) === here || here === home || home.startsWith(here.endsWith("/") ? here : `${here}/`))
|
|
610
|
+
return false;
|
|
611
|
+
if (["Downloads", "Desktop", "Documents", "Library", "tmp"].some((n) => here === pathJoin(home, n)))
|
|
612
|
+
return false;
|
|
613
|
+
try {
|
|
614
|
+
if (holdsRepo(here))
|
|
615
|
+
return false;
|
|
616
|
+
}
|
|
617
|
+
catch {
|
|
618
|
+
return false;
|
|
619
|
+
}
|
|
620
|
+
try {
|
|
621
|
+
const f = loadAll();
|
|
622
|
+
if (Object.values(f?.bindings ?? {}).some((b) => { const p = real(b.path); return p !== here && p.startsWith(`${here}/`); }))
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
625
|
+
catch { }
|
|
626
|
+
return true;
|
|
627
|
+
}
|
|
547
628
|
function detectAgent() {
|
|
548
629
|
const found = detectAvailable();
|
|
549
630
|
return found[0]?.backend.id ?? "unknown";
|
package/dist/reads.js
CHANGED
|
@@ -16,7 +16,14 @@ export function signedReadQuery(cfg, path, params, at) {
|
|
|
16
16
|
full[k] = String(v);
|
|
17
17
|
full.reader = cfg.member;
|
|
18
18
|
full.ts = String(at);
|
|
19
|
-
const
|
|
19
|
+
const d = cfg.delegate;
|
|
20
|
+
if (d) {
|
|
21
|
+
full.dpub = d.cert.pub;
|
|
22
|
+
full.drun = d.cert.run;
|
|
23
|
+
full.dexp = String(d.cert.expires);
|
|
24
|
+
full.dcert = d.cert.sig;
|
|
25
|
+
}
|
|
26
|
+
const sig = signRead(d ? d.privateKey : cfg.privateKey, "GET", path, full);
|
|
20
27
|
return new URLSearchParams({ ...full, sig }).toString();
|
|
21
28
|
}
|
|
22
29
|
export async function signedRead(cfg, path, params = {}, opts = {}) {
|