@join-pantheon/cli 0.28.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 +377 -192
- package/dist/client.js +73 -12
- package/dist/config.js +15 -0
- package/dist/doctor.js +13 -10
- 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 +258 -57
- 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/which.js +66 -4
- package/dist/workdir.js +7 -1
- package/dist/worker.js +522 -101
- package/helper/macos/menubar.swift +11 -3
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -2,7 +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, trySystemFallback, wsOptions } from "./network.js";
|
|
5
|
+
import { describeNetError, explainNetError, noteRelayAnswered, settleSystemFallback, systemFallbackPending, trySystemFallback, wsOptions } from "./network.js";
|
|
6
6
|
export class PantheonClient {
|
|
7
7
|
cfg;
|
|
8
8
|
onEvent;
|
|
@@ -34,6 +34,9 @@ export class PantheonClient {
|
|
|
34
34
|
connected = false;
|
|
35
35
|
pending = null;
|
|
36
36
|
lastError = null;
|
|
37
|
+
everReadMarked = false;
|
|
38
|
+
lastInbound = 0;
|
|
39
|
+
watchdog = null;
|
|
37
40
|
session;
|
|
38
41
|
role;
|
|
39
42
|
enrolledToken = null;
|
|
@@ -76,21 +79,39 @@ export class PantheonClient {
|
|
|
76
79
|
if (!this.everConnected)
|
|
77
80
|
this.closed = false;
|
|
78
81
|
const first = this.localOverride ?? this.cfg.relay;
|
|
79
|
-
|
|
80
|
-
|
|
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;
|
|
81
94
|
this.closed = false;
|
|
95
|
+
const kept = this.lastError;
|
|
82
96
|
try {
|
|
83
|
-
|
|
97
|
+
await this.attempt(first);
|
|
98
|
+
settleSystemFallback(first, true);
|
|
99
|
+
return;
|
|
84
100
|
}
|
|
85
101
|
catch (e2) {
|
|
86
|
-
|
|
102
|
+
const reached = answered(e2);
|
|
103
|
+
settleSystemFallback(first, reached);
|
|
104
|
+
if (reached)
|
|
105
|
+
e = e2;
|
|
106
|
+
else
|
|
107
|
+
this.lastError = kept;
|
|
87
108
|
}
|
|
88
109
|
}
|
|
89
110
|
return this.afterFailure(e);
|
|
90
111
|
}).catch((e) => {
|
|
91
|
-
if (!explainNetError(e))
|
|
112
|
+
if (!explainNetError(e, first))
|
|
92
113
|
throw e;
|
|
93
|
-
const out = new Error(describeNetError(e));
|
|
114
|
+
const out = new Error(describeNetError(e, first));
|
|
94
115
|
out.code = e.code;
|
|
95
116
|
throw out;
|
|
96
117
|
});
|
|
@@ -109,17 +130,39 @@ export class PantheonClient {
|
|
|
109
130
|
this.closed = false;
|
|
110
131
|
this.localOverride = local;
|
|
111
132
|
this.usingLoopback = true;
|
|
133
|
+
if (this.everConnected)
|
|
134
|
+
throw e;
|
|
112
135
|
return this.attempt(local);
|
|
113
136
|
}
|
|
114
137
|
}
|
|
115
138
|
attempt(address) {
|
|
116
139
|
return new Promise((resolve, reject) => {
|
|
117
140
|
let settled = false;
|
|
141
|
+
let heard = false;
|
|
142
|
+
const fail = reject;
|
|
143
|
+
reject = (err) => fail(heard ? Object.assign(err, { relayAnswered: true }) : err);
|
|
118
144
|
this.ws?.terminate();
|
|
145
|
+
if (this.watchdog) {
|
|
146
|
+
clearInterval(this.watchdog);
|
|
147
|
+
this.watchdog = null;
|
|
148
|
+
}
|
|
119
149
|
const ws = new WebSocket(address, { handshakeTimeout: 6000, ...wsOptions(address) });
|
|
120
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?.();
|
|
121
163
|
ws.on("open", () => { });
|
|
122
164
|
ws.on("message", (raw) => {
|
|
165
|
+
this.lastInbound = Date.now();
|
|
123
166
|
let msg;
|
|
124
167
|
try {
|
|
125
168
|
msg = JSON.parse(String(raw));
|
|
@@ -127,6 +170,8 @@ export class PantheonClient {
|
|
|
127
170
|
catch {
|
|
128
171
|
return;
|
|
129
172
|
}
|
|
173
|
+
if (typeof msg?.t === "string")
|
|
174
|
+
heard = true;
|
|
130
175
|
this.onEvent?.(msg);
|
|
131
176
|
switch (msg.t) {
|
|
132
177
|
case "challenge":
|
|
@@ -142,7 +187,7 @@ export class PantheonClient {
|
|
|
142
187
|
display: this.cfg.display ?? this.cfg.member,
|
|
143
188
|
agent: this.cfg.agent ?? "unknown",
|
|
144
189
|
pkg: VERSION,
|
|
145
|
-
from_seq: this.seq,
|
|
190
|
+
...(this.seq > 0 ? { from_seq: this.seq } : {}),
|
|
146
191
|
...(this.cfg.repo ? { repo: this.cfg.repo, ...(this.cfg.repoOrigin ? { repo_origin: this.cfg.repoOrigin } : {}) } : {}),
|
|
147
192
|
}));
|
|
148
193
|
break;
|
|
@@ -175,8 +220,13 @@ export class PantheonClient {
|
|
|
175
220
|
this.session = msg.session;
|
|
176
221
|
this.everConnected = true;
|
|
177
222
|
this.head = msg.seq;
|
|
178
|
-
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)
|
|
179
228
|
this.readSeq = msg.seq;
|
|
229
|
+
this.everReadMarked = true;
|
|
180
230
|
if (msg.token) {
|
|
181
231
|
this.enrolledToken = msg.token;
|
|
182
232
|
this.cfg.token = msg.token;
|
|
@@ -235,6 +285,8 @@ export class PantheonClient {
|
|
|
235
285
|
}
|
|
236
286
|
case "error": {
|
|
237
287
|
this.lastError = `${msg.code}: ${msg.message}`;
|
|
288
|
+
if (msg.code === "SPACE_DELETED")
|
|
289
|
+
this.closed = true;
|
|
238
290
|
const fn = msg.cid ? this.acks.get(msg.cid) : undefined;
|
|
239
291
|
if (fn && msg.cid) {
|
|
240
292
|
this.acks.delete(msg.cid);
|
|
@@ -274,13 +326,22 @@ export class PantheonClient {
|
|
|
274
326
|
reject(new Error(this.lastError ?? "relay connection failed"));
|
|
275
327
|
}
|
|
276
328
|
setTimeout(() => { if (!this.closed)
|
|
277
|
-
this.connect().catch(() => { }); }, this.backoff);
|
|
329
|
+
this.connect().catch(() => { }); }, Math.round(this.backoff * (0.5 + Math.random())));
|
|
278
330
|
this.backoff = Math.min(this.backoff * 2, 15_000);
|
|
279
331
|
};
|
|
280
332
|
ws.on("close", down);
|
|
281
333
|
ws.on("error", (e) => { this.lastError = String(e.message ?? e); down(); });
|
|
282
334
|
});
|
|
283
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
|
+
}
|
|
284
345
|
close() {
|
|
285
346
|
this.closed = true;
|
|
286
347
|
for (const w of this.waiters) {
|
|
@@ -288,7 +349,7 @@ export class PantheonClient {
|
|
|
288
349
|
w.resolve([]);
|
|
289
350
|
}
|
|
290
351
|
this.waiters = [];
|
|
291
|
-
this.ws?.close();
|
|
352
|
+
this.ws?.close(1000);
|
|
292
353
|
}
|
|
293
354
|
push(rec) {
|
|
294
355
|
for (let i = 0; i < this.waiters.length; i++) {
|
|
@@ -334,7 +395,7 @@ export class PantheonClient {
|
|
|
334
395
|
});
|
|
335
396
|
}
|
|
336
397
|
rpc(payload) {
|
|
337
|
-
if (!this.ws || this.ws.readyState !== 1) {
|
|
398
|
+
if (!this.ws || this.ws.readyState !== 1 || !this.connected) {
|
|
338
399
|
return Promise.reject(new Error(`not connected to relay (${this.lastError ?? "offline"})`));
|
|
339
400
|
}
|
|
340
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,12 +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,
|
|
23
|
+
import { currentPlan, explainNetError, routeFor, redactProxy } from "./network.js";
|
|
24
|
+
import { refusalOf } from "./refusal.js";
|
|
24
25
|
const HOSTED_RELAY = "relay.joinpantheon.network";
|
|
25
26
|
const git = (args, cwd) => {
|
|
26
27
|
try {
|
|
@@ -168,14 +169,16 @@ export async function doctor(projectDir) {
|
|
|
168
169
|
const line = describeChoice(choice);
|
|
169
170
|
const bad = choice.signIn && !choice.signIn.ok && !choice.signIn.unknown;
|
|
170
171
|
const fellBack = !!prof.agent.primary_surface && choice.backend.id !== prof.agent.primary_surface && agents.length > 0;
|
|
171
|
-
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)}` : ""}`;
|
|
172
173
|
const refused = unattendedRefusal(choice);
|
|
173
174
|
if (refused)
|
|
174
175
|
add("warn", "worker agent", line, `woken runs are refused here, not tried: ${refused}`, shareLine);
|
|
175
176
|
else if (bad)
|
|
176
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);
|
|
177
178
|
else if (fellBack)
|
|
178
|
-
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);
|
|
179
182
|
else
|
|
180
183
|
add("pass", "worker agent", line, undefined, shareLine);
|
|
181
184
|
}
|
|
@@ -224,7 +227,7 @@ export async function doctor(projectDir) {
|
|
|
224
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");
|
|
225
228
|
}
|
|
226
229
|
const plan = currentPlan();
|
|
227
|
-
const via =
|
|
230
|
+
const via = routeFor(cfg.relay, plan);
|
|
228
231
|
if (via) {
|
|
229
232
|
add("pass", "network", `through the proxy ${redactProxy(via)} (from ${plan.source})`, undefined, `through a proxy (${plan.source})`);
|
|
230
233
|
}
|
|
@@ -260,7 +263,7 @@ export async function doctor(projectDir) {
|
|
|
260
263
|
}
|
|
261
264
|
catch (e) {
|
|
262
265
|
const msg = e.message;
|
|
263
|
-
const nx = explainNetError(e);
|
|
266
|
+
const nx = explainNetError(e, health);
|
|
264
267
|
if (nx) {
|
|
265
268
|
add("fail", "relay", `cannot reach ${health} — ${nx.why}`, nx.fix, `unreachable (${where}): ${nx.share}`);
|
|
266
269
|
return out;
|
|
@@ -272,15 +275,15 @@ export async function doctor(projectDir) {
|
|
|
272
275
|
let joined = false;
|
|
273
276
|
await client.connect().then(() => { joined = true; }).catch(() => { });
|
|
274
277
|
if (!joined) {
|
|
275
|
-
const nx = client.pending ? null : explainNetError(new Error(client.lastError ?? ""));
|
|
278
|
+
const nx = client.pending ? null : explainNetError(new Error(client.lastError ?? ""), cfg.relay);
|
|
276
279
|
if (nx) {
|
|
277
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}`);
|
|
278
281
|
client.close();
|
|
279
282
|
return out;
|
|
280
283
|
}
|
|
281
284
|
client.pending
|
|
282
|
-
? add("fail", "membership", "registered but not admitted", `ask an admitted member to run: pantheon admit ${cfg.member}
|
|
283
|
-
: 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 ?? ""));
|
|
284
287
|
client.close();
|
|
285
288
|
return out;
|
|
286
289
|
}
|
|
@@ -295,7 +298,7 @@ export async function doctor(projectDir) {
|
|
|
295
298
|
.map((p) => `${p.member}[${online.has(p.member) ? "online" : "no session open"}]`).join(" "), undefined, `${admittedPeers.length} admitted, ${peersOnline} with a session open`)
|
|
296
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");
|
|
297
300
|
if (pending.length) {
|
|
298
|
-
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`);
|
|
299
302
|
}
|
|
300
303
|
const wakers = client.members.filter((m) => m.admitted && m.may_wake && m.member !== cfg.member);
|
|
301
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 {
|