@join-pantheon/cli 0.27.0 → 0.29.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/dist/accounts.js +13 -2
- package/dist/agents.js +377 -9
- package/dist/cli.js +419 -182
- package/dist/client.js +91 -8
- package/dist/config.js +15 -0
- package/dist/doctor.js +35 -6
- package/dist/grants.js +16 -1
- package/dist/hooks-install.js +4 -2
- package/dist/host-env.js +51 -6
- package/dist/install.js +216 -16
- package/dist/kiro-files.js +23 -6
- package/dist/ledger.js +8 -2
- package/dist/machine.js +273 -54
- package/dist/mcp-supervisor.js +5 -0
- package/dist/mcp.js +33 -6
- package/dist/network.js +595 -0
- package/dist/pending.js +14 -7
- package/dist/quickstart.js +180 -21
- package/dist/refusal.js +29 -0
- package/dist/relay.js +686 -440
- package/dist/roster-file.js +19 -3
- package/dist/run-failure.js +18 -7
- package/dist/service.js +173 -40
- package/dist/setup-guide.js +21 -4
- package/dist/store.js +9 -3
- package/dist/update-check.js +2 -1
- package/dist/upgrade.js +2 -1
- package/dist/which.js +71 -7
- package/dist/workdir.js +7 -1
- package/dist/worker.js +524 -99
- package/helper/macos/menubar.swift +11 -3
- package/package.json +2 -1
package/dist/client.js
CHANGED
|
@@ -2,6 +2,7 @@ import WebSocket from "ws";
|
|
|
2
2
|
import { signChallenge } from "./identity.js";
|
|
3
3
|
import { newId } from "./protocol.js";
|
|
4
4
|
import { VERSION } from "./version.js";
|
|
5
|
+
import { describeNetError, explainNetError, noteRelayAnswered, settleSystemFallback, systemFallbackPending, trySystemFallback, wsOptions } from "./network.js";
|
|
5
6
|
export class PantheonClient {
|
|
6
7
|
cfg;
|
|
7
8
|
onEvent;
|
|
@@ -33,6 +34,9 @@ export class PantheonClient {
|
|
|
33
34
|
connected = false;
|
|
34
35
|
pending = null;
|
|
35
36
|
lastError = null;
|
|
37
|
+
everReadMarked = false;
|
|
38
|
+
lastInbound = 0;
|
|
39
|
+
watchdog = null;
|
|
36
40
|
session;
|
|
37
41
|
role;
|
|
38
42
|
enrolledToken = null;
|
|
@@ -74,7 +78,46 @@ export class PantheonClient {
|
|
|
74
78
|
connect() {
|
|
75
79
|
if (!this.everConnected)
|
|
76
80
|
this.closed = false;
|
|
77
|
-
|
|
81
|
+
const first = this.localOverride ?? this.cfg.relay;
|
|
82
|
+
const trial = !this.localOverride && systemFallbackPending(first);
|
|
83
|
+
const answered = (x) => x?.relayAnswered === true;
|
|
84
|
+
return this.attempt(first).then(() => { noteRelayAnswered(first); }, async (e) => {
|
|
85
|
+
if (trial) {
|
|
86
|
+
settleSystemFallback(first, answered(e));
|
|
87
|
+
}
|
|
88
|
+
else if (answered(e)) {
|
|
89
|
+
noteRelayAnswered(first);
|
|
90
|
+
}
|
|
91
|
+
else if (!this.localOverride && trySystemFallback(first, e)) {
|
|
92
|
+
if (this.everConnected)
|
|
93
|
+
throw e;
|
|
94
|
+
this.closed = false;
|
|
95
|
+
const kept = this.lastError;
|
|
96
|
+
try {
|
|
97
|
+
await this.attempt(first);
|
|
98
|
+
settleSystemFallback(first, true);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
catch (e2) {
|
|
102
|
+
const reached = answered(e2);
|
|
103
|
+
settleSystemFallback(first, reached);
|
|
104
|
+
if (reached)
|
|
105
|
+
e = e2;
|
|
106
|
+
else
|
|
107
|
+
this.lastError = kept;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return this.afterFailure(e);
|
|
111
|
+
}).catch((e) => {
|
|
112
|
+
if (!explainNetError(e, first))
|
|
113
|
+
throw e;
|
|
114
|
+
const out = new Error(describeNetError(e, first));
|
|
115
|
+
out.code = e.code;
|
|
116
|
+
throw out;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
async afterFailure(e) {
|
|
120
|
+
{
|
|
78
121
|
if (this.localOverride)
|
|
79
122
|
throw e;
|
|
80
123
|
const transport = /ETIMEDOUT|ECONNREFUSED|ENOTFOUND|EHOSTUNREACH|ENETUNREACH|handshake has timed out|socket hang up/i
|
|
@@ -87,17 +130,39 @@ export class PantheonClient {
|
|
|
87
130
|
this.closed = false;
|
|
88
131
|
this.localOverride = local;
|
|
89
132
|
this.usingLoopback = true;
|
|
133
|
+
if (this.everConnected)
|
|
134
|
+
throw e;
|
|
90
135
|
return this.attempt(local);
|
|
91
|
-
}
|
|
136
|
+
}
|
|
92
137
|
}
|
|
93
138
|
attempt(address) {
|
|
94
139
|
return new Promise((resolve, reject) => {
|
|
95
140
|
let settled = false;
|
|
141
|
+
let heard = false;
|
|
142
|
+
const fail = reject;
|
|
143
|
+
reject = (err) => fail(heard ? Object.assign(err, { relayAnswered: true }) : err);
|
|
96
144
|
this.ws?.terminate();
|
|
97
|
-
|
|
145
|
+
if (this.watchdog) {
|
|
146
|
+
clearInterval(this.watchdog);
|
|
147
|
+
this.watchdog = null;
|
|
148
|
+
}
|
|
149
|
+
const ws = new WebSocket(address, { handshakeTimeout: 6000, ...wsOptions(address) });
|
|
98
150
|
this.ws = ws;
|
|
151
|
+
this.lastInbound = Date.now();
|
|
152
|
+
ws.on("ping", () => { this.lastInbound = Date.now(); });
|
|
153
|
+
ws.on("pong", () => { this.lastInbound = Date.now(); });
|
|
154
|
+
this.watchdog = setInterval(() => {
|
|
155
|
+
if (this.ws === ws && this.connected && Date.now() - this.lastInbound > 45_000) {
|
|
156
|
+
try {
|
|
157
|
+
ws.terminate();
|
|
158
|
+
}
|
|
159
|
+
catch { }
|
|
160
|
+
}
|
|
161
|
+
}, 5_000);
|
|
162
|
+
this.watchdog.unref?.();
|
|
99
163
|
ws.on("open", () => { });
|
|
100
164
|
ws.on("message", (raw) => {
|
|
165
|
+
this.lastInbound = Date.now();
|
|
101
166
|
let msg;
|
|
102
167
|
try {
|
|
103
168
|
msg = JSON.parse(String(raw));
|
|
@@ -105,6 +170,8 @@ export class PantheonClient {
|
|
|
105
170
|
catch {
|
|
106
171
|
return;
|
|
107
172
|
}
|
|
173
|
+
if (typeof msg?.t === "string")
|
|
174
|
+
heard = true;
|
|
108
175
|
this.onEvent?.(msg);
|
|
109
176
|
switch (msg.t) {
|
|
110
177
|
case "challenge":
|
|
@@ -120,7 +187,7 @@ export class PantheonClient {
|
|
|
120
187
|
display: this.cfg.display ?? this.cfg.member,
|
|
121
188
|
agent: this.cfg.agent ?? "unknown",
|
|
122
189
|
pkg: VERSION,
|
|
123
|
-
from_seq: this.seq,
|
|
190
|
+
...(this.seq > 0 ? { from_seq: this.seq } : {}),
|
|
124
191
|
...(this.cfg.repo ? { repo: this.cfg.repo, ...(this.cfg.repoOrigin ? { repo_origin: this.cfg.repoOrigin } : {}) } : {}),
|
|
125
192
|
}));
|
|
126
193
|
break;
|
|
@@ -153,8 +220,13 @@ export class PantheonClient {
|
|
|
153
220
|
this.session = msg.session;
|
|
154
221
|
this.everConnected = true;
|
|
155
222
|
this.head = msg.seq;
|
|
156
|
-
if (
|
|
223
|
+
if (msg.seq < this.seq) {
|
|
224
|
+
this.seq = msg.seq;
|
|
225
|
+
this.readSeq = Math.min(this.readSeq, msg.seq);
|
|
226
|
+
}
|
|
227
|
+
if (!this.everReadMarked && this.readSeq < msg.seq)
|
|
157
228
|
this.readSeq = msg.seq;
|
|
229
|
+
this.everReadMarked = true;
|
|
158
230
|
if (msg.token) {
|
|
159
231
|
this.enrolledToken = msg.token;
|
|
160
232
|
this.cfg.token = msg.token;
|
|
@@ -213,6 +285,8 @@ export class PantheonClient {
|
|
|
213
285
|
}
|
|
214
286
|
case "error": {
|
|
215
287
|
this.lastError = `${msg.code}: ${msg.message}`;
|
|
288
|
+
if (msg.code === "SPACE_DELETED")
|
|
289
|
+
this.closed = true;
|
|
216
290
|
const fn = msg.cid ? this.acks.get(msg.cid) : undefined;
|
|
217
291
|
if (fn && msg.cid) {
|
|
218
292
|
this.acks.delete(msg.cid);
|
|
@@ -252,13 +326,22 @@ export class PantheonClient {
|
|
|
252
326
|
reject(new Error(this.lastError ?? "relay connection failed"));
|
|
253
327
|
}
|
|
254
328
|
setTimeout(() => { if (!this.closed)
|
|
255
|
-
this.connect().catch(() => { }); }, this.backoff);
|
|
329
|
+
this.connect().catch(() => { }); }, Math.round(this.backoff * (0.5 + Math.random())));
|
|
256
330
|
this.backoff = Math.min(this.backoff * 2, 15_000);
|
|
257
331
|
};
|
|
258
332
|
ws.on("close", down);
|
|
259
333
|
ws.on("error", (e) => { this.lastError = String(e.message ?? e); down(); });
|
|
260
334
|
});
|
|
261
335
|
}
|
|
336
|
+
handOff() {
|
|
337
|
+
this.closed = true;
|
|
338
|
+
for (const w of this.waiters) {
|
|
339
|
+
clearTimeout(w.timer);
|
|
340
|
+
w.resolve([]);
|
|
341
|
+
}
|
|
342
|
+
this.waiters = [];
|
|
343
|
+
this.ws?.close(4000, "handover");
|
|
344
|
+
}
|
|
262
345
|
close() {
|
|
263
346
|
this.closed = true;
|
|
264
347
|
for (const w of this.waiters) {
|
|
@@ -266,7 +349,7 @@ export class PantheonClient {
|
|
|
266
349
|
w.resolve([]);
|
|
267
350
|
}
|
|
268
351
|
this.waiters = [];
|
|
269
|
-
this.ws?.close();
|
|
352
|
+
this.ws?.close(1000);
|
|
270
353
|
}
|
|
271
354
|
push(rec) {
|
|
272
355
|
for (let i = 0; i < this.waiters.length; i++) {
|
|
@@ -312,7 +395,7 @@ export class PantheonClient {
|
|
|
312
395
|
});
|
|
313
396
|
}
|
|
314
397
|
rpc(payload) {
|
|
315
|
-
if (!this.ws || this.ws.readyState !== 1) {
|
|
398
|
+
if (!this.ws || this.ws.readyState !== 1 || !this.connected) {
|
|
316
399
|
return Promise.reject(new Error(`not connected to relay (${this.lastError ?? "offline"})`));
|
|
317
400
|
}
|
|
318
401
|
const cid = `c${++this.cid}`;
|
package/dist/config.js
CHANGED
|
@@ -192,6 +192,21 @@ export function resolveForHook(cwd = process.cwd()) {
|
|
|
192
192
|
const here = r.repo;
|
|
193
193
|
return bound.some((b) => b.path === here.root || (!!b.origin && b.origin === here.origin)) ? r.cfg : null;
|
|
194
194
|
}
|
|
195
|
+
export function identity(file = loadAll()) {
|
|
196
|
+
const env = envView(file);
|
|
197
|
+
if (env)
|
|
198
|
+
return applyAgentEnv({
|
|
199
|
+
...env,
|
|
200
|
+
...(file?.account ? { account: file.account } : {}), ...(file?.pendingAuth ? { pendingAuth: file.pendingAuth } : {}),
|
|
201
|
+
});
|
|
202
|
+
if (!file)
|
|
203
|
+
return null;
|
|
204
|
+
return applyAgentEnv({
|
|
205
|
+
relay: file.relay, space: "", token: "", member: file.member, publicKey: file.publicKey, privateKey: file.privateKey,
|
|
206
|
+
...(file.display ? { display: file.display } : {}), ...(file.agent ? { agent: file.agent } : {}),
|
|
207
|
+
...(file.account ? { account: file.account } : {}), ...(file.pendingAuth ? { pendingAuth: file.pendingAuth } : {}),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
195
210
|
export function load() {
|
|
196
211
|
return resolveFor(process.cwd()).cfg;
|
|
197
212
|
}
|
package/dist/doctor.js
CHANGED
|
@@ -15,11 +15,13 @@ import { approvalSummary, chosenApproval, onPath } from "./install.js";
|
|
|
15
15
|
import { hookState, readIntent, agentHookStates } from "./notify.js";
|
|
16
16
|
import { loadPersonas } from "./persona.js";
|
|
17
17
|
import { workerProc, menubarProc, notifierProc } from "./machine.js";
|
|
18
|
-
import { chooseAgent, describeChoice, unattendedRefusal } from "./agents.js";
|
|
18
|
+
import { chooseAgent, describeChoice, unattendedRefusal, skippedSummary } from "./agents.js";
|
|
19
19
|
import { onPathNamed } from "./which.js";
|
|
20
20
|
import { VERSION } from "./version.js";
|
|
21
21
|
import { serviceInstalled, servicesUnavailable } from "./service.js";
|
|
22
22
|
import { hostEnv, platformName } from "./host-env.js";
|
|
23
|
+
import { currentPlan, explainNetError, routeFor, redactProxy } from "./network.js";
|
|
24
|
+
import { refusalOf } from "./refusal.js";
|
|
23
25
|
const HOSTED_RELAY = "relay.joinpantheon.network";
|
|
24
26
|
const git = (args, cwd) => {
|
|
25
27
|
try {
|
|
@@ -167,14 +169,16 @@ export async function doctor(projectDir) {
|
|
|
167
169
|
const line = describeChoice(choice);
|
|
168
170
|
const bad = choice.signIn && !choice.signIn.ok && !choice.signIn.unknown;
|
|
169
171
|
const fellBack = !!prof.agent.primary_surface && choice.backend.id !== prof.agent.primary_surface && agents.length > 0;
|
|
170
|
-
const shareLine = `${choice.backend.label} — ${choice.signIn?.detail ?? "not checked"}${choice.skipped.length ? `; ${choice
|
|
172
|
+
const shareLine = `${choice.backend.label} — ${choice.signIn?.detail ?? "not checked"}${choice.skipped.length ? `; ${skippedSummary(choice)}` : ""}`;
|
|
171
173
|
const refused = unattendedRefusal(choice);
|
|
172
174
|
if (refused)
|
|
173
175
|
add("warn", "worker agent", line, `woken runs are refused here, not tried: ${refused}`, shareLine);
|
|
174
176
|
else if (bad)
|
|
175
177
|
add("warn", "worker agent", line, `nothing is signed in, so every woken run here fails until you sign in: ${choice.skipped.map((x) => x.why).join("; ") || "sign in to the agent CLI"}`, shareLine);
|
|
176
178
|
else if (fellBack)
|
|
177
|
-
add("warn", "worker agent", line,
|
|
179
|
+
add("warn", "worker agent", line, prof.agent.primary_surface === "kiro"
|
|
180
|
+
? `you chose Kiro at setup; Kiro cannot yet be fenced to its worktree for unattended runs, so runs use ${choice.backend.label}`
|
|
181
|
+
: `you chose ${prof.agent.primary_surface} at setup; runs use ${choice.backend.label} until it is signed in again`, shareLine);
|
|
178
182
|
else
|
|
179
183
|
add("pass", "worker agent", line, undefined, shareLine);
|
|
180
184
|
}
|
|
@@ -222,6 +226,20 @@ export async function doctor(projectDir) {
|
|
|
222
226
|
else {
|
|
223
227
|
add("pass", "transport", url.protocol === "wss:" ? "wss:// (TLS)" : `ws:// on a private address (${url.host})`, undefined, url.protocol === "wss:" ? "wss:// (TLS)" : "ws:// on a private address");
|
|
224
228
|
}
|
|
229
|
+
const plan = currentPlan();
|
|
230
|
+
const via = routeFor(cfg.relay, plan);
|
|
231
|
+
if (via) {
|
|
232
|
+
add("pass", "network", `through the proxy ${redactProxy(via)} (from ${plan.source})`, undefined, `through a proxy (${plan.source})`);
|
|
233
|
+
}
|
|
234
|
+
else if (plan.fallbackOnly && (plan.https || plan.http)) {
|
|
235
|
+
add("pass", "network", `direct; the ${plan.source === "macos" ? "macOS" : "Windows"} proxy ${redactProxy(plan.https ?? plan.http)} is used only if a direct connection fails`, undefined, `direct, system proxy as fallback`);
|
|
236
|
+
}
|
|
237
|
+
else if (plan.pac) {
|
|
238
|
+
add("warn", "network", `direct, but this machine uses a proxy auto-config script, which Pantheon cannot read`, "if the relay turns out unreachable below, save the proxy that script points to: pantheon network --proxy http://host:port", "direct; PAC script present");
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
add("pass", "network", "direct — no proxy set", undefined, "direct");
|
|
242
|
+
}
|
|
225
243
|
const health = `${cfg.relay.replace(/^ws/, "http")}/health`;
|
|
226
244
|
const where = hosted ? "hosted" : "self-hosted";
|
|
227
245
|
try {
|
|
@@ -245,6 +263,11 @@ export async function doctor(projectDir) {
|
|
|
245
263
|
}
|
|
246
264
|
catch (e) {
|
|
247
265
|
const msg = e.message;
|
|
266
|
+
const nx = explainNetError(e, health);
|
|
267
|
+
if (nx) {
|
|
268
|
+
add("fail", "relay", `cannot reach ${health} — ${nx.why}`, nx.fix, `unreachable (${where}): ${nx.share}`);
|
|
269
|
+
return out;
|
|
270
|
+
}
|
|
248
271
|
add("fail", "relay", `cannot reach ${health} — ${msg}`, "start it with `pantheon relay`, or check the tunnel/tailnet. Your teammate must be able to reach the SAME address.", `unreachable (${where}): ${/abort|timeout/i.test(msg) ? "timed out" : /ECONNREFUSED/.test(msg) ? "connection refused" : /ENOTFOUND|EAI_AGAIN/.test(msg) ? "name did not resolve" : "fetch failed"}`);
|
|
249
272
|
return out;
|
|
250
273
|
}
|
|
@@ -252,9 +275,15 @@ export async function doctor(projectDir) {
|
|
|
252
275
|
let joined = false;
|
|
253
276
|
await client.connect().then(() => { joined = true; }).catch(() => { });
|
|
254
277
|
if (!joined) {
|
|
278
|
+
const nx = client.pending ? null : explainNetError(new Error(client.lastError ?? ""), cfg.relay);
|
|
279
|
+
if (nx) {
|
|
280
|
+
add("fail", "live link", `the health check got through but the live connection did not — ${nx.why}`, nx.fix, `live connection failed: ${nx.share}`);
|
|
281
|
+
client.close();
|
|
282
|
+
return out;
|
|
283
|
+
}
|
|
255
284
|
client.pending
|
|
256
|
-
? add("fail", "membership", "registered but not admitted", `ask an admitted member to run: pantheon admit ${cfg.member}
|
|
257
|
-
: add("fail", "membership", `refused: ${client.lastError ?? "unknown"}`,
|
|
285
|
+
? add("fail", "membership", "registered but not admitted", `ask an admitted member to run: pantheon admit ${cfg.member} (confirm fingerprint ${fingerprint(cfg.publicKey)} with them by voice first)`, "registered, not admitted")
|
|
286
|
+
: ((r) => add("fail", "membership", `refused: ${r?.message ?? client.lastError ?? "unknown"}`, (r?.fix ?? "run pantheon status for the relay's reason").replace("<you>", cfg.member), `refused: ${r?.code ?? String(client.lastError ?? "unknown").split(/[\s:]/)[0]}`))(refusalOf(client.lastError ?? ""));
|
|
258
287
|
client.close();
|
|
259
288
|
return out;
|
|
260
289
|
}
|
|
@@ -269,7 +298,7 @@ export async function doctor(projectDir) {
|
|
|
269
298
|
.map((p) => `${p.member}[${online.has(p.member) ? "online" : "no session open"}]`).join(" "), undefined, `${admittedPeers.length} admitted, ${peersOnline} with a session open`)
|
|
270
299
|
: add("warn", "peers", "nobody else is admitted to this space yet", "send your teammate an invite from `pantheon invite` (or the one `pantheon start` printed)", "nobody else admitted yet");
|
|
271
300
|
if (pending.length) {
|
|
272
|
-
add("warn", "pending", pending.map((p) => `${p.member} (${p.fingerprint})`).join(" "), `confirm the fingerprint by voice, then: pantheon admit ${pending[0].member} --may-wake`, `${pending.length} waiting to be admitted`);
|
|
301
|
+
add("warn", "pending", pending.map((p) => `${p.member} (${p.fingerprint})`).join(" "), `confirm the fingerprint by voice, then: pantheon admit ${pending[0].member} (add --may-wake only if they should be able to wake this machine)`, `${pending.length} waiting to be admitted`);
|
|
273
302
|
}
|
|
274
303
|
const wakers = client.members.filter((m) => m.admitted && m.may_wake && m.member !== cfg.member);
|
|
275
304
|
const hookStates = agentHookStates(projectDir);
|
package/dist/grants.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { PANTHEON_TOOLS, PANTHEON_TOOL_NAMES, toolsAllowed } from "./tools.js";
|
|
5
|
+
import { kiroDir } from "./kiro-files.js";
|
|
5
6
|
export const CLAUDE_ALLOW = PANTHEON_TOOLS;
|
|
6
7
|
export const CURSOR_ALLOW = PANTHEON_TOOL_NAMES.map((t) => `Mcp(pantheon:${t})`);
|
|
7
8
|
export const claudeAllow = (approval) => toolsAllowed(approval).map((t) => `mcp__pantheon__${t}`);
|
|
@@ -12,6 +13,7 @@ const isOursClaude = (t) => t === CLAUDE_SERVER_RULE || t.startsWith("mcp__panth
|
|
|
12
13
|
const isOursCursor = (t) => /^Mcp\(pantheon(?::[^)]*)?\)$/.test(t);
|
|
13
14
|
export const claudeSettingsFile = () => join(homedir(), ".claude", "settings.json");
|
|
14
15
|
export const cursorCliConfigFile = () => join(homedir(), ".cursor", "cli-config.json");
|
|
16
|
+
export const kiroMcpFile = () => join(kiroDir(), "settings", "mcp.json");
|
|
15
17
|
function readJson(file) {
|
|
16
18
|
if (!existsSync(file))
|
|
17
19
|
return null;
|
|
@@ -125,8 +127,21 @@ export function grantToolPermissions(approval = "approve") {
|
|
|
125
127
|
{ tool: "cursor perms", status: grantCursorToolPermissions(undefined, approval) },
|
|
126
128
|
];
|
|
127
129
|
}
|
|
128
|
-
export function revokeToolPermissions(claudeFile = claudeSettingsFile(), cursorFile = cursorCliConfigFile()) {
|
|
130
|
+
export function revokeToolPermissions(claudeFile = claudeSettingsFile(), cursorFile = cursorCliConfigFile(), kiroFile = kiroMcpFile()) {
|
|
129
131
|
const out = [];
|
|
132
|
+
{
|
|
133
|
+
const doc = readJson(kiroFile);
|
|
134
|
+
const servers = doc && doc !== "invalid" ? doc.mcpServers : undefined;
|
|
135
|
+
const entry = servers && typeof servers === "object" ? servers.pantheon : undefined;
|
|
136
|
+
if (entry && typeof entry === "object" && "autoApprove" in entry) {
|
|
137
|
+
delete entry.autoApprove;
|
|
138
|
+
try {
|
|
139
|
+
writeJson(kiroFile, doc);
|
|
140
|
+
out.push(` removed pantheon tool approvals (autoApprove) from ${kiroFile}`);
|
|
141
|
+
}
|
|
142
|
+
catch { }
|
|
143
|
+
}
|
|
144
|
+
}
|
|
130
145
|
for (const [file, isOurs] of [
|
|
131
146
|
[claudeFile, isOursClaude],
|
|
132
147
|
[cursorFile, isOursCursor],
|
package/dist/hooks-install.js
CHANGED
|
@@ -3,7 +3,7 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { MAX_CONSECUTIVE_BLOCKS, isOurHookCommand } from "./notify.js";
|
|
5
5
|
import { copilotHome, kiroHome } from "./agents.js";
|
|
6
|
-
import { KIRO_AGENT_NAME, kiroAgentFile, kiroAgentDoc, readKiroAgent, writeKiroAgent, kiroDefaultIsPantheon } from "./kiro-files.js";
|
|
6
|
+
import { KIRO_AGENT_NAME, kiroAgentFile, kiroAgentDoc, readKiroAgent, writeKiroAgent, kiroDefaultIsPantheon, gitTracks } from "./kiro-files.js";
|
|
7
7
|
function ours(cmd) {
|
|
8
8
|
return isOurHookCommand(cmd);
|
|
9
9
|
}
|
|
@@ -262,6 +262,8 @@ export function kiroHookState(scope, projectDir = process.cwd()) {
|
|
|
262
262
|
export function installKiroHooks(bin, scope, projectDir = process.cwd(), allowed = []) {
|
|
263
263
|
const file = kiroAgentPath(scope, projectDir);
|
|
264
264
|
const a = readKiroAgent(file);
|
|
265
|
+
if (a?.tracked)
|
|
266
|
+
return [` skipped ${file} is tracked by git — it belongs to the repo, so pantheon neither writes nor removes it`];
|
|
265
267
|
if (a && !a.ours)
|
|
266
268
|
return [` skipped ${file} is an agent called "${KIRO_AGENT_NAME}" that pantheon did not write — left as it is`];
|
|
267
269
|
writeKiroAgent(file, kiroAgentDoc(bin, allowed));
|
|
@@ -279,7 +281,7 @@ export function removeKiroHooks(projectDir = process.cwd()) {
|
|
|
279
281
|
const out = [];
|
|
280
282
|
for (const scope of ["project", "user"]) {
|
|
281
283
|
const file = kiroAgentPath(scope, projectDir);
|
|
282
|
-
if (!kiroHookState(scope, projectDir).installed)
|
|
284
|
+
if (!kiroHookState(scope, projectDir).installed || gitTracks(file))
|
|
283
285
|
continue;
|
|
284
286
|
try {
|
|
285
287
|
rmSync(file, { force: true });
|
package/dist/host-env.js
CHANGED
|
@@ -1,12 +1,35 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
const CLOUD_VARS = [
|
|
3
3
|
["CODESPACES", "GitHub Codespaces ($CODESPACES)"],
|
|
4
|
+
["CLOUDENV_ENVIRONMENT_ID", "GitHub Codespaces ($CLOUDENV_ENVIRONMENT_ID)"],
|
|
4
5
|
["REMOTE_CONTAINERS", "a VS Code dev container ($REMOTE_CONTAINERS)"],
|
|
5
6
|
["REMOTE_CONTAINERS_IPC", "a VS Code dev container ($REMOTE_CONTAINERS_IPC)"],
|
|
6
7
|
["DEVCONTAINER", "a dev container ($DEVCONTAINER)"],
|
|
7
8
|
["GITPOD_WORKSPACE_ID", "a Gitpod workspace ($GITPOD_WORKSPACE_ID)"],
|
|
8
9
|
["CLOUD_SHELL", "a cloud shell ($CLOUD_SHELL)"],
|
|
10
|
+
["REPL_ID", "a Replit workspace ($REPL_ID)"],
|
|
9
11
|
];
|
|
12
|
+
const CONTAINER_VARS = [
|
|
13
|
+
["KUBERNETES_SERVICE_HOST", "a Kubernetes pod ($KUBERNETES_SERVICE_HOST)"],
|
|
14
|
+
];
|
|
15
|
+
const HOST_INITS = new Set(["systemd", "init", "openrc-init", "runit", "runit-init", "launchd", "dinit", "s6-svscan", "s6-linux-init", "shepherd"]);
|
|
16
|
+
const SANDBOX_ROOT_FS = new Set(["overlay", "overlayfs", "9p", "virtiofs", "aufs"]);
|
|
17
|
+
export function rootFsType(mountinfo) {
|
|
18
|
+
return rootMount(mountinfo)?.type ?? null;
|
|
19
|
+
}
|
|
20
|
+
export function rootMount(mountinfo) {
|
|
21
|
+
for (const line of mountinfo.split("\n")) {
|
|
22
|
+
const [pre, post] = line.split(" - ");
|
|
23
|
+
if (!pre || !post)
|
|
24
|
+
continue;
|
|
25
|
+
const f = pre.trim().split(/\s+/);
|
|
26
|
+
if (f[4] === "/") {
|
|
27
|
+
const [type = "", source = ""] = post.trim().split(/\s+/);
|
|
28
|
+
return { type, source };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
10
33
|
export function hostEnv(env = process.env, plat = process.platform, exists = existsSync, read = (p) => readFileSync(p, "utf8")) {
|
|
11
34
|
if (plat === "darwin") {
|
|
12
35
|
return { platform: plat, kind: "local", evidence: [], ephemeral: false, desktop: true };
|
|
@@ -19,17 +42,39 @@ export function hostEnv(env = process.env, plat = process.platform, exists = exi
|
|
|
19
42
|
kind = "cloud-workspace";
|
|
20
43
|
}
|
|
21
44
|
}
|
|
45
|
+
for (const [name, label] of CONTAINER_VARS)
|
|
46
|
+
if (env[name])
|
|
47
|
+
evidence.push(label);
|
|
48
|
+
if (env.container)
|
|
49
|
+
evidence.push(`a container runtime set $container=${env.container}`);
|
|
50
|
+
const readIf = (p) => { try {
|
|
51
|
+
return exists(p) ? read(p) : "";
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return "";
|
|
55
|
+
} };
|
|
22
56
|
if (exists("/.dockerenv"))
|
|
23
57
|
evidence.push("/.dockerenv exists");
|
|
24
58
|
if (exists("/run/.containerenv"))
|
|
25
59
|
evidence.push("/run/.containerenv exists (podman)");
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (/docker|containerd|kubepods|lxc/.test(cgroup))
|
|
60
|
+
if (exists("/var/run/secrets/kubernetes.io"))
|
|
61
|
+
evidence.push("/var/run/secrets/kubernetes.io exists (a Kubernetes pod)");
|
|
62
|
+
const version = readIf("/proc/version");
|
|
63
|
+
const wsl = !!env.WSL_DISTRO_NAME || /microsoft/i.test(version);
|
|
64
|
+
const cgroup = readIf("/proc/1/cgroup");
|
|
65
|
+
if (/docker|containerd|kubepods|lxc|libpod|crio/.test(cgroup))
|
|
32
66
|
evidence.push("pid 1 is in a container cgroup (/proc/1/cgroup)");
|
|
67
|
+
else if (cgroup.trim() === "0::/" && !wsl)
|
|
68
|
+
evidence.push("pid 1 is at the root of its own cgroup namespace (/proc/1/cgroup: 0::/)");
|
|
69
|
+
const comm = readIf("/proc/1/comm").trim();
|
|
70
|
+
if (comm && !HOST_INITS.has(comm))
|
|
71
|
+
evidence.push(`pid 1 is "${comm}", not an init system (/proc/1/comm)`);
|
|
72
|
+
if (/gvisor/i.test(version) || /#1 SMP Sun Jan 10 15:06:54 PST 2016/.test(version))
|
|
73
|
+
evidence.push("the kernel is gVisor's (/proc/version)");
|
|
74
|
+
const root = rootMount(readIf("/proc/1/mountinfo"));
|
|
75
|
+
const ostree = exists("/run/ostree-booted") || root?.source === "composefs" || root?.type === "composefs";
|
|
76
|
+
if (root && SANDBOX_ROOT_FS.has(root.type) && !ostree)
|
|
77
|
+
evidence.push(`the root filesystem is ${root.type} (/proc/1/mountinfo)`);
|
|
33
78
|
if (evidence.length && kind === "local")
|
|
34
79
|
kind = "container";
|
|
35
80
|
return {
|