@join-pantheon/cli 0.28.0 → 0.29.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
- return this.attempt(first).catch(async (e) => {
80
- if (!this.localOverride && trySystemFallback(first, e)) {
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
- return await this.attempt(first);
97
+ await this.attempt(first);
98
+ settleSystemFallback(first, true);
99
+ return;
84
100
  }
85
101
  catch (e2) {
86
- e = e2;
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 (this.readSeq < msg.seq)
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, proxyFor, redactProxy } from "./network.js";
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.skipped.length} skipped (not signed in)` : ""}`;
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, `you chose ${prof.agent.primary_surface} at setup; runs use ${choice.backend.label} until it is signed in again`, shareLine);
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 = proxyFor(cfg.relay, plan);
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} --may-wake (confirm fingerprint ${fingerprint(cfg.publicKey)} with them by voice first)`, "registered, not admitted")
283
- : add("fail", "membership", `refused: ${client.lastError ?? "unknown"}`, "if this is KEY_MISMATCH, someone else already registered your member name — pick another with --me, or have them revoke it", `refused: ${String(client.lastError ?? "unknown").split(/[\s:]/)[0]}`);
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],
@@ -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
- let cgroup = "";
27
- try {
28
- cgroup = exists("/proc/1/cgroup") ? read("/proc/1/cgroup") : "";
29
- }
30
- catch { }
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 {