@proagentstore/cli 0.4.156 → 0.4.158

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/bootstrap.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  payloadAt,
17
17
  payloadRoot,
18
18
  readTrial
19
- } from "./chunk-PKMXQJEJ.js";
19
+ } from "./chunk-7O4S5477.js";
20
20
 
21
21
  // src/bootstrap.ts
22
22
  import { existsSync } from "fs";
@@ -0,0 +1,84 @@
1
+ import { maskSecrets } from "../crash-reports.js";
2
+ import { tmuxExec } from "./tmux.js";
3
+ /** How long a delivered value is masked in captures. */
4
+ export const INJECTED_TTL_MS = 24 * 60 * 60_000;
5
+ const MAX_REMEMBERED = 50;
6
+ const injected = [];
7
+ /** Mask `value` in every capture from now on (for {@link INJECTED_TTL_MS}). Values under 4 characters are not maskable. */
8
+ export function rememberInjected(value, now = Date.now()) {
9
+ const v = value.trim();
10
+ if (v.length < 4)
11
+ return;
12
+ injected.push({ value: v, until: now + INJECTED_TTL_MS });
13
+ while (injected.length > MAX_REMEMBERED)
14
+ injected.shift();
15
+ }
16
+ /** For tests. */
17
+ export function forgetInjected() {
18
+ injected.length = 0;
19
+ }
20
+ /** Every remembered value in `text`, replaced character for character with `•` — across line wraps too. */
21
+ export function maskInjected(text, now = Date.now()) {
22
+ const live = injected.filter((e) => e.until > now);
23
+ if (!live.length || !text)
24
+ return text;
25
+ // Search the text with its newlines removed, then mask the ORIGINAL positions, so a value tmux wrapped
26
+ // onto the next row is still found and the pane keeps its shape.
27
+ const at = [];
28
+ let flat = "";
29
+ for (let i = 0; i < text.length; i++) {
30
+ if (text[i] === "\n" || text[i] === "\r")
31
+ continue;
32
+ at.push(i);
33
+ flat += text[i];
34
+ }
35
+ const chars = text.split("");
36
+ for (const { value } of live) {
37
+ for (let i = flat.indexOf(value); i !== -1; i = flat.indexOf(value, i + value.length)) {
38
+ for (let k = i; k < i + value.length; k++)
39
+ chars[at[k]] = "•";
40
+ }
41
+ }
42
+ return chars.join("");
43
+ }
44
+ /** A pane as it may leave this machine. */
45
+ export function redactPane(text, now = Date.now()) {
46
+ return maskInjected(maskSecrets(text), now);
47
+ }
48
+ const redacting = new WeakSet();
49
+ /** Mark this response's panes for redaction: a terminal route, unless the caller is inject confirming a delivery. */
50
+ export function redactPanesOf(res, path, body) {
51
+ if ((path.startsWith("/tmux/") || path.startsWith("/terminal/")) && body?.unmasked !== true)
52
+ redacting.add(res);
53
+ }
54
+ /** The body `json()` writes: its `pane`/`paneBefore` redacted when {@link redactPanesOf} marked the response. */
55
+ export function outgoing(res, body) {
56
+ if (!redacting.has(res) || !body || typeof body !== "object")
57
+ return body;
58
+ const out = { ...body };
59
+ for (const key of ["pane", "paneBefore"])
60
+ if (typeof out[key] === "string")
61
+ out[key] = redactPane(out[key]);
62
+ return out;
63
+ }
64
+ /**
65
+ * `POST /secure/scrub {target, value}`: after an echoed delivery, remember the value for masking and clear
66
+ * the tmux pane's scrollback. `cleared` is false for a non-tmux backend, whose history this cannot reach.
67
+ */
68
+ export function scrubDelivered(body) {
69
+ const value = typeof body.value === "string" ? body.value : "";
70
+ const target = typeof body.target === "string" ? body.target.trim() : "";
71
+ if (!value || !target)
72
+ return { remembered: false, cleared: false };
73
+ rememberInjected(value);
74
+ const tmuxTarget = /^tmux:/i.test(target) ? target.slice(5) : /^(kitty|iterm2):/i.test(target) ? "" : target;
75
+ if (!tmuxTarget)
76
+ return { remembered: true, cleared: false };
77
+ try {
78
+ tmuxExec(["clear-history", "-t", tmuxTarget]);
79
+ return { remembered: true, cleared: true };
80
+ }
81
+ catch {
82
+ return { remembered: true, cleared: false };
83
+ }
84
+ }
@@ -16,6 +16,13 @@ const MAX_PANE = 64 * 1024;
16
16
  export class CodingRuntime {
17
17
  reposBaseDir;
18
18
  sessions = new Map();
19
+ /**
20
+ * Which agent instance each session belongs to (#1164). One `pags up` serves every agent on the
21
+ * machine, and every relayed request names the instance whose socket it arrived on — so a session
22
+ * started by one agent must not be driven, read or ended through another's. Bound at `/coding/start`;
23
+ * a session with no binding (one started before this, or by a local caller) answers to anyone, as before.
24
+ */
25
+ owners = new Map();
19
26
  /**
20
27
  * Coding turns that are running here (#896, #1007).
21
28
  *
@@ -363,6 +370,22 @@ export class CodingRuntime {
363
370
  this.takeovers.delete(sessionId);
364
371
  return { ok: true, usage, acts, authResolved };
365
372
  }
373
+ /** Record the instance a session belongs to — the first one to start it (#1164). */
374
+ bindInstance(sessionId, instanceId) {
375
+ if (sessionId && instanceId && !this.owners.has(sessionId))
376
+ this.owners.set(sessionId, instanceId);
377
+ }
378
+ /** Why `instanceId` may not touch this session, or null (#1164). An unbound session, or no caller instance, is not refused. */
379
+ instanceRefusal(sessionId, instanceId) {
380
+ const owner = this.owners.get(sessionId);
381
+ if (!owner || !instanceId || owner === instanceId)
382
+ return null;
383
+ return `Coding session ${sessionId} belongs to another agent on this machine; a request from this agent cannot reach it.`;
384
+ }
385
+ /** Is this session visible to `instanceId` — its own, unbound, or no caller instance (#1164)? */
386
+ visibleTo(sessionId, instanceId) {
387
+ return this.instanceRefusal(sessionId, instanceId) === null;
388
+ }
366
389
  list() {
367
390
  return [...this.sessions.entries()].map(([sessionId, s]) => ({
368
391
  sessionId,
@@ -57,12 +57,19 @@ export const MAX_REPORTS = 100;
57
57
  export const MAX_QUARANTINED = 5;
58
58
  const MAX_MESSAGE = 1000;
59
59
  const MAX_STACK = 4000;
60
- /** Mask credential-shaped text: bearer tokens, API keys, JWTs, `token=…` pairs. */
60
+ /**
61
+ * Mask credential-shaped text: bearer tokens, API keys, JWTs, `token=…` pairs.
62
+ *
63
+ * THE redactor for everything this machine sends to the platform (#1166): crash reports, the update log
64
+ * (`bootstrap/update-log.ts`) and terminal panes (`coding/pane-redact.ts`). A PAGS session is TWO
65
+ * segments — `base64url(json).hmac` (`workers/api/src/lib/session.ts`) — so the token rule takes an
66
+ * optional third: a three-segment-only rule masked a bare session only when `Bearer`/`token=` preceded it.
67
+ */
61
68
  export function maskSecrets(text) {
62
69
  return text
63
70
  .replace(/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi, "$1 [redacted]")
64
71
  .replace(/\b(sk-[A-Za-z0-9_-]{8,}|ghp_[A-Za-z0-9]{8,}|gho_[A-Za-z0-9]{8,}|github_pat_[A-Za-z0-9_]{8,}|xox[abp]-[A-Za-z0-9-]{8,}|pags_runner_[A-Za-z0-9-]{8,})/g, "[redacted]")
65
- .replace(/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g, "[redacted-jwt]")
72
+ .replace(/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{16,}(?:\.[A-Za-z0-9_-]{8,})?/g, "[redacted-jwt]")
66
73
  .replace(/\b((?:api[_-]?key|token|secret|password|authorization)\s*[=:]\s*)("?)[^\s"&]{4,}\2/gi, "$1[redacted]");
67
74
  }
68
75
  /** A stable, non-identifying key for an account — the same as the runner lock's (`runner-lock.ts`). */
@@ -0,0 +1,42 @@
1
+ import { RunnerInputError } from "./errors.js";
2
+ /** The relayed instance id header, or undefined. */
3
+ export function instanceOf(req) {
4
+ const raw = req.headers["x-pags-instance-id"];
5
+ const id = (Array.isArray(raw) ? raw[0] : raw)?.trim();
6
+ return id || undefined;
7
+ }
8
+ /** The coding session a `/coding/*` request is about: its body's `sessionId`, or the takeover path's id. */
9
+ export function codingSessionIdOf(path, body) {
10
+ const m = path.match(/^\/coding\/takeover\/([^/]+)\//);
11
+ if (m)
12
+ return decodeURIComponent(m[1]);
13
+ return typeof body?.sessionId === "string" && body.sessionId ? body.sessionId : null;
14
+ }
15
+ /** Why `caller` may not reach the coding session this request names, or null. */
16
+ export async function codingSessionRefusal(coding, req, path, caller) {
17
+ if (!path.startsWith("/coding/"))
18
+ return null;
19
+ const sessionId = codingSessionIdOf(path, req.method === "POST" ? await readJson(req) : undefined);
20
+ return sessionId ? coding.instanceRefusal(sessionId, caller) : null;
21
+ }
22
+ /** A body read once and kept, so the session guard and the route handler both see it. */
23
+ const bodies = new WeakMap();
24
+ export async function readJson(req) {
25
+ if (bodies.has(req))
26
+ return bodies.get(req);
27
+ const chunks = [];
28
+ for await (const chunk of req)
29
+ chunks.push(Buffer.from(chunk));
30
+ const raw = Buffer.concat(chunks).toString("utf-8");
31
+ let parsed = {};
32
+ if (raw) {
33
+ try {
34
+ parsed = JSON.parse(raw);
35
+ }
36
+ catch {
37
+ throw new RunnerInputError("Request body must be valid JSON");
38
+ }
39
+ }
40
+ bodies.set(req, parsed);
41
+ return parsed;
42
+ }
@@ -1,3 +1,5 @@
1
+ import { outgoing, redactPanesOf, scrubDelivered } from "./coding/pane-redact.js";
2
+ import { codingSessionRefusal, instanceOf, readJson } from "./request-instance.js";
1
3
  import { createServer, } from "node:http";
2
4
  import { URL } from "node:url";
3
5
  import { recordRequestFailure } from "./request-failure.js";
@@ -75,6 +77,15 @@ export async function startRunnerServer(config) {
75
77
  async function route(runner, req, res) {
76
78
  const url = new URL(req.url || "/", `http://${req.headers.host || "127.0.0.1"}`);
77
79
  const path = url.pathname.replace(/\/$/, "") || "/";
80
+ // The agent this request came from, and whether it may touch the coding session it names (#1164, request-instance.ts).
81
+ const caller = instanceOf(req);
82
+ const refused = await codingSessionRefusal(runner.coding, req, path, caller);
83
+ if (refused)
84
+ return json(res, 403, { error: refused });
85
+ // Panes leave this machine redacted (#1166, coding/pane-redact.ts); a delivered secret is scrubbed.
86
+ redactPanesOf(res, path, req.method === "POST" && /^\/(tmux|terminal)\//.test(path) ? await readJson(req) : undefined);
87
+ if (req.method === "POST" && path === "/secure/scrub")
88
+ return json(res, 200, scrubDelivered(await readJson(req)));
78
89
  if ((req.method === "GET" || req.method === "POST") && path === "/health") {
79
90
  // #896: a health check that cannot say WHO answered is not enough to decide anything with.
80
91
  // A contender for the single-instance lock has a pid and a port from the lock file; what it
@@ -290,7 +301,9 @@ async function route(runner, req, res) {
290
301
  }
291
302
  if (req.method === "POST" && path === "/coding/start") {
292
303
  const b = await readJson(req);
293
- return json(res, 200, runner.coding.start(b));
304
+ const started = runner.coding.start(b);
305
+ runner.coding.bindInstance(b.sessionId, caller);
306
+ return json(res, 200, started);
294
307
  }
295
308
  if (req.method === "POST" && path === "/coding/capture") {
296
309
  // `drainUsage` opts this caller in to consuming the session's measured engine spend (#267).
@@ -308,7 +321,8 @@ async function route(runner, req, res) {
308
321
  return json(res, 200, runner.coding.end(b.sessionId));
309
322
  }
310
323
  if (req.method === "GET" && path === "/coding/sessions") {
311
- return json(res, 200, { sessions: runner.coding.list() });
324
+ // Each agent sees its own sessions (#1164); a local caller (the update controller) sees them all.
325
+ return json(res, 200, { sessions: runner.coding.list().filter((s) => runner.coding.visibleTo(s.sessionId, caller)) });
312
326
  }
313
327
  // Per-session CPU and memory, for the heartbeat (#924): which agent is loading this machine.
314
328
  if (req.method === "GET" && path === "/coding/resources") {
@@ -827,20 +841,6 @@ function authorize(req, config) {
827
841
  const headerToken = req.headers["x-pags-runner-token"];
828
842
  return auth === `Bearer ${token}` || headerToken === token;
829
843
  }
830
- async function readJson(req) {
831
- const chunks = [];
832
- for await (const chunk of req)
833
- chunks.push(Buffer.from(chunk));
834
- const raw = Buffer.concat(chunks).toString("utf-8");
835
- if (!raw)
836
- return {};
837
- try {
838
- return JSON.parse(raw);
839
- }
840
- catch {
841
- throw new RunnerInputError("Request body must be valid JSON");
842
- }
843
- }
844
844
  function clampLimit(value, fallback, max) {
845
845
  const parsed = Number(value || fallback);
846
846
  if (!Number.isFinite(parsed))
@@ -852,5 +852,5 @@ function json(res, status, body) {
852
852
  "Content-Type": "application/json; charset=utf-8",
853
853
  "X-Content-Type-Options": "nosniff",
854
854
  });
855
- res.end(JSON.stringify(body));
855
+ res.end(JSON.stringify(outgoing(res, body)));
856
856
  }
@@ -207,21 +207,193 @@ async function installPayload(version, root = payloadRoot(), install = npmInstal
207
207
  return payload;
208
208
  }
209
209
 
210
+ // src/crash-reports.ts
211
+ import { closeSync, copyFileSync, existsSync as existsSync2, fsyncSync, mkdirSync as mkdirSync3, openSync, readdirSync as readdirSync2, readFileSync as readFileSync3, renameSync as renameSync3, unlinkSync, writeSync } from "fs";
212
+ import { basename, dirname, join as join3 } from "path";
213
+ import { createHash, randomUUID } from "crypto";
214
+ var MAX_REPORTS = 100;
215
+ var MAX_QUARANTINED = 5;
216
+ var MAX_MESSAGE = 1e3;
217
+ var MAX_STACK = 4e3;
218
+ function maskSecrets(text) {
219
+ return text.replace(/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi, "$1 [redacted]").replace(/\b(sk-[A-Za-z0-9_-]{8,}|ghp_[A-Za-z0-9]{8,}|gho_[A-Za-z0-9]{8,}|github_pat_[A-Za-z0-9_]{8,}|xox[abp]-[A-Za-z0-9-]{8,}|pags_runner_[A-Za-z0-9-]{8,})/g, "[redacted]").replace(/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{16,}(?:\.[A-Za-z0-9_-]{8,})?/g, "[redacted-jwt]").replace(/\b((?:api[_-]?key|token|secret|password|authorization)\s*[=:]\s*)("?)[^\s"&]{4,}\2/gi, "$1[redacted]");
220
+ }
221
+ function crashAccountKey(login) {
222
+ return createHash("sha256").update(login.trim().toLowerCase()).digest("hex").substring(0, 12);
223
+ }
224
+ function clip(text, max) {
225
+ if (text.length <= max) return text;
226
+ return `${text.substring(0, max)}
227
+ [cut: showing the first ${max} of ${text.length} characters]`;
228
+ }
229
+ var clean = (value, max) => clip(maskSecrets(String(value ?? "")), max);
230
+ var shortId = (value) => typeof value === "string" && /^[A-Za-z0-9._:-]{1,128}$/.test(value) ? value : void 0;
231
+ function describeError(error) {
232
+ if (error instanceof Error) {
233
+ const cause = error.cause;
234
+ return {
235
+ message: clean(`${error.name}: ${error.message}`, MAX_MESSAGE),
236
+ stack: error.stack ? clean(error.stack, MAX_STACK) : void 0,
237
+ cause: cause === void 0 ? void 0 : clean(cause instanceof Error ? `${cause.name}: ${cause.message}` : cause, MAX_MESSAGE)
238
+ };
239
+ }
240
+ return { message: clean(error, MAX_MESSAGE) };
241
+ }
242
+ function crashReport(input) {
243
+ return {
244
+ id: shortId(input.id) ?? `crash_${randomUUID()}`,
245
+ at: input.at ?? (/* @__PURE__ */ new Date()).toISOString(),
246
+ source: input.source,
247
+ kind: input.kind,
248
+ message: clean(input.message, MAX_MESSAGE),
249
+ ...input.stack ? { stack: clean(input.stack, MAX_STACK) } : {},
250
+ ...input.cause ? { cause: clean(input.cause, MAX_MESSAGE) } : {},
251
+ ...input.exitCode !== void 0 ? { exitCode: input.exitCode } : {},
252
+ ...input.signal !== void 0 ? { signal: input.signal } : {},
253
+ ...shortId(input.build) ? { build: input.build } : {},
254
+ ...shortId(input.account) ? { account: input.account } : {},
255
+ ...shortId(input.instanceId) ? { instanceId: input.instanceId } : {},
256
+ ...shortId(input.sessionId) ? { sessionId: input.sessionId } : {},
257
+ ...shortId(input.taskId) ? { taskId: input.taskId } : {},
258
+ ...input.route ? { route: clean(input.route, 200) } : {},
259
+ ...input.tail?.length ? { tail: input.tail.slice(-20).map((line) => clean(line, 300)) } : {}
260
+ };
261
+ }
262
+ function writeJsonAtomic(path, data, mode = 384) {
263
+ mkdirSync3(dirname(path), { recursive: true });
264
+ const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
265
+ const fd = openSync(tmp, "w", mode);
266
+ try {
267
+ writeSync(fd, `${JSON.stringify(data, null, 2)}
268
+ `);
269
+ fsyncSync(fd);
270
+ } finally {
271
+ closeSync(fd);
272
+ }
273
+ try {
274
+ renameSync3(tmp, path);
275
+ } catch (error) {
276
+ try {
277
+ unlinkSync(tmp);
278
+ } catch {
279
+ }
280
+ throw error;
281
+ }
282
+ }
283
+ function removeStaleTemps(path) {
284
+ const dir = dirname(path);
285
+ const prefix = `${basename(path)}.`;
286
+ if (!existsSync2(dir)) return;
287
+ for (const name of readdirSync2(dir)) {
288
+ if (!name.startsWith(prefix) || !name.endsWith(".tmp")) continue;
289
+ try {
290
+ unlinkSync(join3(dir, name));
291
+ } catch {
292
+ }
293
+ }
294
+ }
295
+ function readJsonOrQuarantine(path) {
296
+ removeStaleTemps(path);
297
+ if (!existsSync2(path)) return { status: "missing" };
298
+ try {
299
+ return { status: "ok", value: JSON.parse(readFileSync3(path, "utf-8")) };
300
+ } catch (error) {
301
+ const quarantinedTo = `${path}.corrupt-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
302
+ try {
303
+ renameSync3(path, quarantinedTo);
304
+ } catch {
305
+ try {
306
+ copyFileSync(path, quarantinedTo);
307
+ } catch (copyError) {
308
+ return { status: "quarantined", quarantinedTo: "(not preserved)", error: `${describeError(error).message}; preserving it failed: ${describeError(copyError).message}` };
309
+ }
310
+ }
311
+ pruneQuarantine(path);
312
+ return { status: "quarantined", quarantinedTo, error: describeError(error).message };
313
+ }
314
+ }
315
+ function pruneQuarantine(path) {
316
+ const dir = dirname(path);
317
+ const prefix = `${basename(path)}.corrupt-`;
318
+ const copies = readdirSync2(dir).filter((name) => name.startsWith(prefix)).sort();
319
+ const excess = copies.length - MAX_QUARANTINED;
320
+ for (const name of excess > 0 ? copies.slice(0, excess) : []) {
321
+ try {
322
+ unlinkSync(join3(dir, name));
323
+ } catch {
324
+ }
325
+ }
326
+ }
327
+ var CrashLog = class {
328
+ /** `base` stamps the reports this log writes about ITSELF (a quarantined file). */
329
+ constructor(path, base = { source: "runtime" }) {
330
+ this.path = path;
331
+ this.base = base;
332
+ }
333
+ path;
334
+ base;
335
+ /** Everything still on disk, oldest first. A corrupt file is quarantined and reported, then read as a fresh file. */
336
+ read() {
337
+ const got = readJsonOrQuarantine(this.path);
338
+ if (got.status === "ok") {
339
+ const reports = Array.isArray(got.value.reports) ? got.value.reports.filter((r) => !!r && typeof r.id === "string") : [];
340
+ return { reports, dropped: Number(got.value.dropped) || 0 };
341
+ }
342
+ if (got.status === "missing") return { reports: [], dropped: 0 };
343
+ const file = {
344
+ reports: [crashReport({ ...this.base, kind: "store_corrupt", message: `crash-report file was unreadable and was moved to ${basename(got.quarantinedTo)}: ${got.error}` })],
345
+ dropped: 0
346
+ };
347
+ this.write(file);
348
+ return file;
349
+ }
350
+ /** Append a report (bounded). Never throws: a crash path must not crash on its own record. */
351
+ record(report) {
352
+ try {
353
+ const file = this.read();
354
+ if (file.reports.some((r) => r.id === report.id)) return true;
355
+ file.reports.push(report);
356
+ const over = file.reports.length - MAX_REPORTS;
357
+ if (over > 0) {
358
+ file.reports = file.reports.slice(over);
359
+ file.dropped += over;
360
+ }
361
+ this.write(file);
362
+ return true;
363
+ } catch (error) {
364
+ process.stderr.write(`[crash-reports] could not record ${report.kind}: ${describeError(error).message}
365
+ `);
366
+ return false;
367
+ }
368
+ }
369
+ /** Drop reports by id — only by this file's own writer. */
370
+ remove(ids) {
371
+ if (!ids.length) return;
372
+ const file = this.read();
373
+ const gone = new Set(ids);
374
+ file.reports = file.reports.filter((r) => !gone.has(r.id));
375
+ this.write(file);
376
+ }
377
+ write(file) {
378
+ writeJsonAtomic(this.path, file);
379
+ }
380
+ };
381
+
210
382
  // src/bootstrap/update-log.ts
211
- import { appendFileSync, existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync3, statSync, writeFileSync as writeFileSync2 } from "fs";
383
+ import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync4, readFileSync as readFileSync4, statSync, writeFileSync as writeFileSync2 } from "fs";
212
384
  import { homedir as homedir2 } from "os";
213
- import { dirname, join as join3 } from "path";
385
+ import { dirname as dirname2, join as join4 } from "path";
214
386
  var MAX_BYTES = 256 * 1024;
215
387
  var UPDATE_LOG_UPLOAD_CHARS = 4e3;
216
388
  function updateLogPath(env = process.env) {
217
- return env.PAGS_UPDATE_LOG?.trim() || join3(env.PAGS_CONFIG_DIR?.trim() || join3(homedir2(), ".config", "proagentstore"), "update.log");
389
+ return env.PAGS_UPDATE_LOG?.trim() || join4(env.PAGS_CONFIG_DIR?.trim() || join4(homedir2(), ".config", "proagentstore"), "update.log");
218
390
  }
219
391
  function logUpdateStep(step, env = process.env, now = /* @__PURE__ */ new Date()) {
220
392
  const path = updateLogPath(env);
221
393
  try {
222
- mkdirSync3(dirname(path), { recursive: true });
223
- if (existsSync2(path) && statSync(path).size > MAX_BYTES) {
224
- const text = readFileSync3(path, "utf8");
394
+ mkdirSync4(dirname2(path), { recursive: true });
395
+ if (existsSync3(path) && statSync(path).size > MAX_BYTES) {
396
+ const text = readFileSync4(path, "utf8");
225
397
  writeFileSync2(path, text.slice(text.indexOf("\n", text.length / 2) + 1));
226
398
  }
227
399
  appendFileSync(path, `${now.toISOString()} [${process.pid}] ${step.replace(/\s*\n\s*/g, " | ")}
@@ -231,7 +403,7 @@ function logUpdateStep(step, env = process.env, now = /* @__PURE__ */ new Date()
231
403
  }
232
404
  function updateLogTail(env = process.env, maxChars = UPDATE_LOG_UPLOAD_CHARS) {
233
405
  try {
234
- const text = readFileSync3(updateLogPath(env), "utf8").trimEnd();
406
+ const text = maskSecrets(readFileSync4(updateLogPath(env), "utf8").trimEnd());
235
407
  if (!text) return null;
236
408
  if (text.length <= maxChars) return text;
237
409
  const cut = text.slice(-maxChars);
@@ -276,6 +448,11 @@ export {
276
448
  latestPublishedRelease,
277
449
  latestPublishedVersion,
278
450
  installPayload,
451
+ crashAccountKey,
452
+ crashReport,
453
+ writeJsonAtomic,
454
+ readJsonOrQuarantine,
455
+ CrashLog,
279
456
  logUpdateStep,
280
457
  updateLogUpload
281
458
  };
package/dist/index.js CHANGED
@@ -2,8 +2,11 @@
2
2
  import {
3
3
  BOOTSTRAP_ENV,
4
4
  CLI_PACKAGE,
5
+ CrashLog,
5
6
  badVersions,
6
7
  beginTrial,
8
+ crashAccountKey,
9
+ crashReport,
7
10
  installPayload,
8
11
  isVersion,
9
12
  latestPublishedRelease,
@@ -11,10 +14,12 @@ import {
11
14
  markPayloadHealthy,
12
15
  olderThan,
13
16
  payloadRoot,
17
+ readJsonOrQuarantine,
14
18
  runNpm,
15
19
  takeRollbackNotice,
16
- updateLogUpload
17
- } from "./chunk-PKMXQJEJ.js";
20
+ updateLogUpload,
21
+ writeJsonAtomic
22
+ } from "./chunk-7O4S5477.js";
18
23
 
19
24
  // src/index.ts
20
25
  import { createRequire as createRequire3 } from "module";
@@ -1272,7 +1277,7 @@ var publishCommand = new Command7("publish").description("Publish an agent to Pr
1272
1277
 
1273
1278
  // src/commands/runner/command.ts
1274
1279
  import { spawn as spawn5 } from "child_process";
1275
- import { randomUUID as randomUUID4 } from "crypto";
1280
+ import { randomUUID as randomUUID3 } from "crypto";
1276
1281
  import { Command as Command8 } from "commander";
1277
1282
 
1278
1283
  // src/commands/runner/process.ts
@@ -1426,6 +1431,78 @@ function sampleResources(read = os, now = Date.now(), extra = {}) {
1426
1431
  };
1427
1432
  }
1428
1433
 
1434
+ // src/commands/runner/route-policy.ts
1435
+ var ALWAYS = ["/health", "/capabilities", "/events"];
1436
+ var READ_ALWAYS = ["/tasks"];
1437
+ var BROWSER = ["/tasks", "/browser/", "/takeover", "/sessions"];
1438
+ var BY_RUNTIME = {
1439
+ // The coding engine, the website runtime builder's task, and the engine's own SIGN-IN: the cloud relays the
1440
+ // login page through `/browser/act` + `/browser/handoff` and its card through `/takeover` (coding-sessions-open.ts).
1441
+ coding: ["/coding/", "/tasks", "/browser/act", "/browser/handoff", "/takeover"],
1442
+ browser: BROWSER,
1443
+ local_browser: ["/local-browser/"],
1444
+ local_artifact: ["/local-artifact/"],
1445
+ local_apply: ["/local-apply/", ...BROWSER],
1446
+ local_native: ["/native/"]
1447
+ };
1448
+ var TERMINAL = ["/tmux/", "/terminal/", "/secure/"];
1449
+ var REPO_READS = ["/coding/tree", "/coding/read-file", "/coding/git", "/coding/search", "/coding/git-remote", "/coding/repo-check"];
1450
+ var SIGN_IN_TMUX = /* @__PURE__ */ new Set(["/tmux/session", "/tmux/run", "/tmux/send", "/tmux/capture"]);
1451
+ var SIGN_IN_SESSION = /^pags-signin-[a-z0-9_-]+$/;
1452
+ function hasTerminal(caps) {
1453
+ const tools = caps.tools ?? [];
1454
+ return (caps.surfaces ?? []).includes("tmux") || tools.some((t) => t.startsWith("tmux_") || t.startsWith("terminal_"));
1455
+ }
1456
+ function allowedPrefixes(caps) {
1457
+ const out = [...ALWAYS];
1458
+ if (!caps) return out;
1459
+ if (caps.runtime && BY_RUNTIME[caps.runtime]) out.push(...BY_RUNTIME[caps.runtime]);
1460
+ const tools = caps.tools ?? [];
1461
+ if (hasTerminal(caps)) out.push(...TERMINAL);
1462
+ if (tools.some((t) => t.startsWith("browser_"))) out.push("/browser/");
1463
+ if (tools.some((t) => t.startsWith("repo_"))) out.push(...REPO_READS);
1464
+ return out;
1465
+ }
1466
+ function covers(prefix, path) {
1467
+ if (prefix.endsWith("/")) return path.startsWith(prefix);
1468
+ return path === prefix || path.startsWith(`${prefix}/`);
1469
+ }
1470
+ function routeRefusal(caps, path, method = "POST", body) {
1471
+ const clean3 = path.split("?")[0] ?? path;
1472
+ if (/(^|\/)\.\.(\/|$)/.test(clean3)) return `Refused ${clean3}: a relayed path may not climb out of its route.`;
1473
+ if (allowedPrefixes(caps).some((p) => covers(p, clean3))) return null;
1474
+ if ((method === "GET" || method === "HEAD") && READ_ALWAYS.some((p) => covers(p, clean3))) return null;
1475
+ if (caps?.runtime === "coding" && SIGN_IN_TMUX.has(clean3)) {
1476
+ const session = body?.session;
1477
+ if (typeof session === "string" && SIGN_IN_SESSION.test(session)) return null;
1478
+ }
1479
+ return `Refused ${clean3}: this agent's declared capabilities do not include it, so this machine will not run it for this agent.`;
1480
+ }
1481
+ var AgentRoutePolicy = class {
1482
+ constructor(fetchInstances) {
1483
+ this.fetchInstances = fetchInstances;
1484
+ }
1485
+ fetchInstances;
1486
+ declared = /* @__PURE__ */ new Map();
1487
+ refreshing = null;
1488
+ learn(instances) {
1489
+ for (const inst of instances ?? []) this.declared.set(inst.id, inst.capabilities ?? null);
1490
+ }
1491
+ refresh() {
1492
+ this.refreshing ??= this.fetchInstances().then((instances) => this.learn(instances)).catch(() => void 0).finally(() => {
1493
+ this.refreshing = null;
1494
+ });
1495
+ return this.refreshing;
1496
+ }
1497
+ refusalFor(id) {
1498
+ return (method, path, body) => {
1499
+ const caps = this.declared.get(id);
1500
+ if (caps === void 0) void this.refresh();
1501
+ return routeRefusal(caps, path, method, body);
1502
+ };
1503
+ }
1504
+ };
1505
+
1429
1506
  // src/commands/runner/self-update.ts
1430
1507
  import { spawn as spawn4 } from "child_process";
1431
1508
  var RUNNER_UPDATE_PATH = "/pags/runner/update";
@@ -2803,6 +2880,8 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2803
2880
  if (plan.target && !attached.has(plan.target)) forceNext.delete(plan.target);
2804
2881
  return { status: 200, result: { attached: [...attached.keys()], ...plan.target ? { target: plan.target, holding: attached.has(plan.target) } : {} } };
2805
2882
  };
2883
+ const routes = new AgentRoutePolicy(() => requestPags("GET", "/v1/instances/my/instances", { ...opts, pagsToken: pagsToken() }).then((r) => r.instances));
2884
+ await routes.refresh();
2806
2885
  const attach = (id, label = `${id.slice(0, 8)}\u2026`) => {
2807
2886
  if (attached.has(id)) return;
2808
2887
  const mintToken = () => requestPags("POST", `/v1/relay/${apiPathSegment(id)}/token`, { ...opts, pagsToken: pagsToken() }, {}).then((r) => r.token);
@@ -2838,7 +2917,8 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2838
2917
  runnerNode,
2839
2918
  // The final automatic-update drain is an admission gate, not merely another poll:
2840
2919
  // command work starts only when the old runner is still permitted to serve it.
2841
- () => relayMutationAdmission.begin()
2920
+ () => relayMutationAdmission.begin(),
2921
+ routes.refusalFor(id)
2842
2922
  )
2843
2923
  );
2844
2924
  if (label) writeLine(`Attached agent: ${label}`);
@@ -2934,6 +3014,7 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
2934
3014
  "/v1/instances/my/instances",
2935
3015
  { ...opts, pagsToken: pagsToken() }
2936
3016
  );
3017
+ routes.learn(res.instances);
2937
3018
  const { attach: toAttach, detach: toDetach } = diffMembership(
2938
3019
  attached.keys(),
2939
3020
  res.instances ?? [],
@@ -2983,7 +3064,7 @@ async function readSessionResources(localUrl, runnerToken) {
2983
3064
  }
2984
3065
  var RTT_PROBE_PREFIX = "rtt:";
2985
3066
  var RTT_ECHO_PREFIX = "rtt-echo:";
2986
- function openRelaySocket(instanceId, wsBase, mintToken, localUrl, runnerToken, force = false, onConflict, onOpen, onControl, runnerNode = hostname5(), beginMutation = () => () => void 0) {
3067
+ function openRelaySocket(instanceId, wsBase, mintToken, localUrl, runnerToken, force = false, onConflict, onOpen, onControl, runnerNode = hostname5(), beginMutation = () => () => void 0, routeRefusal2 = () => null) {
2987
3068
  let backoffMs = 1e3;
2988
3069
  let reconnecting = false;
2989
3070
  let closed = false;
@@ -3051,6 +3132,14 @@ function openRelaySocket(instanceId, wsBase, mintToken, localUrl, runnerToken, f
3051
3132
  return;
3052
3133
  }
3053
3134
  const method = (cmd.method || "POST").toUpperCase();
3135
+ const refused = routeRefusal2(method, cmd.path, cmd.body);
3136
+ if (refused) {
3137
+ try {
3138
+ ws.send(JSON.stringify({ id: cmd.id, status: 403, result: { error: refused } }));
3139
+ } catch {
3140
+ }
3141
+ return;
3142
+ }
3054
3143
  const releaseMutation = method !== "GET" && method !== "HEAD" ? beginMutation() : null;
3055
3144
  if (method !== "GET" && method !== "HEAD" && !releaseMutation) {
3056
3145
  try {
@@ -3154,200 +3243,28 @@ function openRelaySocket(instanceId, wsBase, mintToken, localUrl, runnerToken, f
3154
3243
  };
3155
3244
  }
3156
3245
 
3157
- // src/crash-reports.ts
3158
- import { closeSync as closeSync2, copyFileSync, existsSync as existsSync8, fsyncSync, mkdirSync as mkdirSync7, openSync as openSync2, readdirSync, readFileSync as readFileSync8, renameSync as renameSync3, unlinkSync as unlinkSync3, writeSync as writeSync2 } from "fs";
3159
- import { basename, dirname as dirname4, join as join9 } from "path";
3160
- import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
3161
- var MAX_REPORTS = 100;
3162
- var MAX_QUARANTINED = 5;
3163
- var MAX_MESSAGE = 1e3;
3164
- var MAX_STACK = 4e3;
3165
- function maskSecrets(text) {
3166
- return text.replace(/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi, "$1 [redacted]").replace(/\b(sk-[A-Za-z0-9_-]{8,}|ghp_[A-Za-z0-9]{8,}|gho_[A-Za-z0-9]{8,}|github_pat_[A-Za-z0-9_]{8,}|xox[abp]-[A-Za-z0-9-]{8,}|pags_runner_[A-Za-z0-9-]{8,})/g, "[redacted]").replace(/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g, "[redacted-jwt]").replace(/\b((?:api[_-]?key|token|secret|password|authorization)\s*[=:]\s*)("?)[^\s"&]{4,}\2/gi, "$1[redacted]");
3167
- }
3168
- function crashAccountKey(login) {
3169
- return createHash2("sha256").update(login.trim().toLowerCase()).digest("hex").substring(0, 12);
3170
- }
3171
- function clip(text, max) {
3172
- if (text.length <= max) return text;
3173
- return `${text.substring(0, max)}
3174
- [cut: showing the first ${max} of ${text.length} characters]`;
3175
- }
3176
- var clean3 = (value, max) => clip(maskSecrets(String(value ?? "")), max);
3177
- var shortId = (value) => typeof value === "string" && /^[A-Za-z0-9._:-]{1,128}$/.test(value) ? value : void 0;
3178
- function describeError(error) {
3179
- if (error instanceof Error) {
3180
- const cause = error.cause;
3181
- return {
3182
- message: clean3(`${error.name}: ${error.message}`, MAX_MESSAGE),
3183
- stack: error.stack ? clean3(error.stack, MAX_STACK) : void 0,
3184
- cause: cause === void 0 ? void 0 : clean3(cause instanceof Error ? `${cause.name}: ${cause.message}` : cause, MAX_MESSAGE)
3185
- };
3186
- }
3187
- return { message: clean3(error, MAX_MESSAGE) };
3188
- }
3189
- function crashReport(input) {
3190
- return {
3191
- id: shortId(input.id) ?? `crash_${randomUUID3()}`,
3192
- at: input.at ?? (/* @__PURE__ */ new Date()).toISOString(),
3193
- source: input.source,
3194
- kind: input.kind,
3195
- message: clean3(input.message, MAX_MESSAGE),
3196
- ...input.stack ? { stack: clean3(input.stack, MAX_STACK) } : {},
3197
- ...input.cause ? { cause: clean3(input.cause, MAX_MESSAGE) } : {},
3198
- ...input.exitCode !== void 0 ? { exitCode: input.exitCode } : {},
3199
- ...input.signal !== void 0 ? { signal: input.signal } : {},
3200
- ...shortId(input.build) ? { build: input.build } : {},
3201
- ...shortId(input.account) ? { account: input.account } : {},
3202
- ...shortId(input.instanceId) ? { instanceId: input.instanceId } : {},
3203
- ...shortId(input.sessionId) ? { sessionId: input.sessionId } : {},
3204
- ...shortId(input.taskId) ? { taskId: input.taskId } : {},
3205
- ...input.route ? { route: clean3(input.route, 200) } : {},
3206
- ...input.tail?.length ? { tail: input.tail.slice(-20).map((line) => clean3(line, 300)) } : {}
3207
- };
3208
- }
3209
- function writeJsonAtomic(path, data, mode = 384) {
3210
- mkdirSync7(dirname4(path), { recursive: true });
3211
- const tmp = `${path}.${process.pid}.${randomUUID3()}.tmp`;
3212
- const fd = openSync2(tmp, "w", mode);
3213
- try {
3214
- writeSync2(fd, `${JSON.stringify(data, null, 2)}
3215
- `);
3216
- fsyncSync(fd);
3217
- } finally {
3218
- closeSync2(fd);
3219
- }
3220
- try {
3221
- renameSync3(tmp, path);
3222
- } catch (error) {
3223
- try {
3224
- unlinkSync3(tmp);
3225
- } catch {
3226
- }
3227
- throw error;
3228
- }
3229
- }
3230
- function removeStaleTemps(path) {
3231
- const dir = dirname4(path);
3232
- const prefix = `${basename(path)}.`;
3233
- if (!existsSync8(dir)) return;
3234
- for (const name of readdirSync(dir)) {
3235
- if (!name.startsWith(prefix) || !name.endsWith(".tmp")) continue;
3236
- try {
3237
- unlinkSync3(join9(dir, name));
3238
- } catch {
3239
- }
3240
- }
3241
- }
3242
- function readJsonOrQuarantine(path) {
3243
- removeStaleTemps(path);
3244
- if (!existsSync8(path)) return { status: "missing" };
3245
- try {
3246
- return { status: "ok", value: JSON.parse(readFileSync8(path, "utf-8")) };
3247
- } catch (error) {
3248
- const quarantinedTo = `${path}.corrupt-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
3249
- try {
3250
- renameSync3(path, quarantinedTo);
3251
- } catch {
3252
- try {
3253
- copyFileSync(path, quarantinedTo);
3254
- } catch (copyError) {
3255
- return { status: "quarantined", quarantinedTo: "(not preserved)", error: `${describeError(error).message}; preserving it failed: ${describeError(copyError).message}` };
3256
- }
3257
- }
3258
- pruneQuarantine(path);
3259
- return { status: "quarantined", quarantinedTo, error: describeError(error).message };
3260
- }
3261
- }
3262
- function pruneQuarantine(path) {
3263
- const dir = dirname4(path);
3264
- const prefix = `${basename(path)}.corrupt-`;
3265
- const copies = readdirSync(dir).filter((name) => name.startsWith(prefix)).sort();
3266
- const excess = copies.length - MAX_QUARANTINED;
3267
- for (const name of excess > 0 ? copies.slice(0, excess) : []) {
3268
- try {
3269
- unlinkSync3(join9(dir, name));
3270
- } catch {
3271
- }
3272
- }
3273
- }
3274
- var CrashLog = class {
3275
- /** `base` stamps the reports this log writes about ITSELF (a quarantined file). */
3276
- constructor(path, base = { source: "runtime" }) {
3277
- this.path = path;
3278
- this.base = base;
3279
- }
3280
- path;
3281
- base;
3282
- /** Everything still on disk, oldest first. A corrupt file is quarantined and reported, then read as a fresh file. */
3283
- read() {
3284
- const got = readJsonOrQuarantine(this.path);
3285
- if (got.status === "ok") {
3286
- const reports = Array.isArray(got.value.reports) ? got.value.reports.filter((r) => !!r && typeof r.id === "string") : [];
3287
- return { reports, dropped: Number(got.value.dropped) || 0 };
3288
- }
3289
- if (got.status === "missing") return { reports: [], dropped: 0 };
3290
- const file = {
3291
- reports: [crashReport({ ...this.base, kind: "store_corrupt", message: `crash-report file was unreadable and was moved to ${basename(got.quarantinedTo)}: ${got.error}` })],
3292
- dropped: 0
3293
- };
3294
- this.write(file);
3295
- return file;
3296
- }
3297
- /** Append a report (bounded). Never throws: a crash path must not crash on its own record. */
3298
- record(report) {
3299
- try {
3300
- const file = this.read();
3301
- if (file.reports.some((r) => r.id === report.id)) return true;
3302
- file.reports.push(report);
3303
- const over = file.reports.length - MAX_REPORTS;
3304
- if (over > 0) {
3305
- file.reports = file.reports.slice(over);
3306
- file.dropped += over;
3307
- }
3308
- this.write(file);
3309
- return true;
3310
- } catch (error) {
3311
- process.stderr.write(`[crash-reports] could not record ${report.kind}: ${describeError(error).message}
3312
- `);
3313
- return false;
3314
- }
3315
- }
3316
- /** Drop reports by id — only by this file's own writer. */
3317
- remove(ids) {
3318
- if (!ids.length) return;
3319
- const file = this.read();
3320
- const gone = new Set(ids);
3321
- file.reports = file.reports.filter((r) => !gone.has(r.id));
3322
- this.write(file);
3323
- }
3324
- write(file) {
3325
- writeJsonAtomic(this.path, file);
3326
- }
3327
- };
3328
-
3329
3246
  // src/crash-delivery.ts
3330
- import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
3247
+ import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
3331
3248
  import { homedir as homedir4 } from "os";
3332
- import { join as join10 } from "path";
3249
+ import { join as join9 } from "path";
3333
3250
  var BATCH = 50;
3334
3251
  var LEDGER_MAX = 1e3;
3335
3252
  var TIMEOUT_MS = 1e4;
3336
3253
  function runnerDataDir(dataDir, env = process.env) {
3337
- return dataDir || env.PAGS_RUNNER_DATA_DIR || join10(homedir4(), ".config", "proagentstore", "browser-runner");
3254
+ return dataDir || env.PAGS_RUNNER_DATA_DIR || join9(homedir4(), ".config", "proagentstore", "browser-runner");
3338
3255
  }
3339
3256
  function crashPaths(configDir2, account, dataDir) {
3340
3257
  const key = crashAccountKey(account);
3341
3258
  return {
3342
- runtimeFile: join10(runnerDataDir(dataDir), "crash-reports.json"),
3343
- cliFile: join10(configDir2, `runner-crashes-${key}.json`),
3344
- ledgerFile: join10(configDir2, `crash-delivered-${key}.json`)
3259
+ runtimeFile: join9(runnerDataDir(dataDir), "crash-reports.json"),
3260
+ cliFile: join9(configDir2, `runner-crashes-${key}.json`),
3261
+ ledgerFile: join9(configDir2, `crash-delivered-${key}.json`)
3345
3262
  };
3346
3263
  }
3347
3264
  function readRuntimeReports(path) {
3348
- if (!existsSync9(path)) return [];
3265
+ if (!existsSync8(path)) return [];
3349
3266
  try {
3350
- const parsed = JSON.parse(readFileSync9(path, "utf-8"));
3267
+ const parsed = JSON.parse(readFileSync8(path, "utf-8"));
3351
3268
  return Array.isArray(parsed.reports) ? parsed.reports.filter((r) => r && typeof r.id === "string") : [];
3352
3269
  } catch {
3353
3270
  return [];
@@ -3478,7 +3395,7 @@ function createRunnerCommand() {
3478
3395
  });
3479
3396
  command.command("connect <instanceIds...>").description("Start ONE local runtime, connect via WebSocket relay, and register it for every given PAGS instance").option("--host <host>", "Host to bind", "127.0.0.1").option("--port <port>", "Port to bind", "49171").option("--data-dir <path>", "Runner data directory").option("--token <token>", "Runner bearer token. Defaults to PAGS_RUNNER_TOKEN or a generated token").option("--headless", "Run Playwright headless").option("--api-base <url>", "PAGS API base URL").option("--pags-token <token>", "PAGS session token. Defaults to PAGS_TOKEN").option("--runner-version <version>", "Runner version").option("--force", "Take over from another connected machine").option("--watch-instances", "Attach newly eligible agents while running, without a restart").action(async (instanceIds, opts) => {
3480
3397
  takeSessionTokenFromEnv();
3481
- const runnerToken = clean2(opts.token) || clean2(process.env.PAGS_RUNNER_TOKEN) || `pags_runner_${randomUUID4()}`;
3398
+ const runnerToken = clean2(opts.token) || clean2(process.env.PAGS_RUNNER_TOKEN) || `pags_runner_${randomUUID3()}`;
3482
3399
  const host = clean2(opts.host) || "127.0.0.1";
3483
3400
  const port = clean2(opts.port) || String(await findFreePort2(49171));
3484
3401
  const lockAccount = clean2(process.env.PAGS_LOCK_ACCOUNT) || "";
@@ -3697,27 +3614,27 @@ function createRunnerCommand() {
3697
3614
  var runnerCommand = createRunnerCommand();
3698
3615
 
3699
3616
  // src/commands/status.ts
3700
- import { readFileSync as readFileSync11, statSync as statSync2, watchFile } from "fs";
3617
+ import { readFileSync as readFileSync10, statSync as statSync2, watchFile } from "fs";
3701
3618
  import { Command as Command9 } from "commander";
3702
3619
 
3703
3620
  // src/background.ts
3704
3621
  import { spawn as nodeSpawn2 } from "child_process";
3705
- import { closeSync as closeSync3, existsSync as existsSync10, mkdirSync as mkdirSync8, openSync as openSync3, readFileSync as readFileSync10, renameSync as renameSync4, statSync, writeFileSync as writeFileSync6 } from "fs";
3706
- import { dirname as dirname5, join as join11 } from "path";
3622
+ import { closeSync as closeSync2, existsSync as existsSync9, mkdirSync as mkdirSync7, openSync as openSync2, readFileSync as readFileSync9, renameSync as renameSync3, statSync, writeFileSync as writeFileSync6 } from "fs";
3623
+ import { dirname as dirname4, join as join10 } from "path";
3707
3624
  var BACKGROUND_ENV = "PAGS_BACKGROUND";
3708
3625
  var BACKGROUND_LOG_ENV = "PAGS_BACKGROUND_LOG";
3709
3626
  var BACKGROUND_RESTART_ENV = "PAGS_BACKGROUND_RESTART";
3710
3627
  var LOG_ROTATE_BYTES = 5 * 1024 * 1024;
3711
- var backgroundLogPath = (account, env = process.env) => join11(configDir(env), "logs", `runner-${accountKey(account)}.log`);
3712
- var backgroundStatePath = (account, env = process.env) => join11(configDir(env), `background-${accountKey(account)}.json`);
3628
+ var backgroundLogPath = (account, env = process.env) => join10(configDir(env), "logs", `runner-${accountKey(account)}.log`);
3629
+ var backgroundStatePath = (account, env = process.env) => join10(configDir(env), `background-${accountKey(account)}.json`);
3713
3630
  function writeBackgroundState(path, state) {
3714
- mkdirSync8(dirname5(path), { recursive: true });
3631
+ mkdirSync7(dirname4(path), { recursive: true });
3715
3632
  writeFileSync6(path, `${JSON.stringify(state)}
3716
3633
  `, { mode: 384 });
3717
3634
  }
3718
3635
  function readBackgroundState(path) {
3719
3636
  try {
3720
- const parsed = JSON.parse(readFileSync10(path, "utf-8"));
3637
+ const parsed = JSON.parse(readFileSync9(path, "utf-8"));
3721
3638
  return parsed?.v === 1 && Number.isInteger(parsed.pid) ? parsed : null;
3722
3639
  } catch {
3723
3640
  return null;
@@ -3729,7 +3646,7 @@ function rotateLog(path, maxBytes = LOG_ROTATE_BYTES) {
3729
3646
  } catch {
3730
3647
  return false;
3731
3648
  }
3732
- renameSync4(path, `${path}.1`);
3649
+ renameSync3(path, `${path}.1`);
3733
3650
  return true;
3734
3651
  }
3735
3652
  function shellQuote(arg, platform = process.platform) {
@@ -3741,16 +3658,16 @@ function backgroundRestartCommand(execPath, script, upArgs, platform = process.p
3741
3658
  return platform === "win32" ? `set ${BACKGROUND_RESTART_ENV}=1&& ${argv}` : `${BACKGROUND_RESTART_ENV}=1 ${argv}`;
3742
3659
  }
3743
3660
  function spawnDetached(command, args, env, logPath, spawn6 = nodeSpawn2) {
3744
- mkdirSync8(dirname5(logPath), { recursive: true });
3661
+ mkdirSync7(dirname4(logPath), { recursive: true });
3745
3662
  rotateLog(logPath);
3746
- const fd = openSync3(logPath, "a", 384);
3663
+ const fd = openSync2(logPath, "a", 384);
3747
3664
  try {
3748
3665
  const child = spawn6(command, args, { detached: true, stdio: ["ignore", fd, fd], env, windowsHide: true });
3749
3666
  child.unref();
3750
3667
  if (!child.pid) throw new Error("the runner process did not start");
3751
3668
  return child.pid;
3752
3669
  } finally {
3753
- closeSync3(fd);
3670
+ closeSync2(fd);
3754
3671
  }
3755
3672
  }
3756
3673
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -3774,8 +3691,8 @@ async function waitUntilTakeable(read, opts = {}) {
3774
3691
  }
3775
3692
  }
3776
3693
  function tailLines(path, n) {
3777
- if (!existsSync10(path)) return null;
3778
- const lines = readFileSync10(path, "utf-8").split("\n");
3694
+ if (!existsSync9(path)) return null;
3695
+ const lines = readFileSync9(path, "utf-8").split("\n");
3779
3696
  if (lines.at(-1) === "") lines.pop();
3780
3697
  return lines.slice(Math.max(0, lines.length - n));
3781
3698
  }
@@ -3830,7 +3747,7 @@ var logsCommand = new Command9("logs").description("Show the background runner's
3830
3747
  watchFile(path, { interval: 1e3 }, (cur) => {
3831
3748
  if (cur.size < offset) offset = 0;
3832
3749
  if (cur.size === offset) return;
3833
- process.stdout.write(readFileSync11(path).subarray(offset, cur.size));
3750
+ process.stdout.write(readFileSync10(path).subarray(offset, cur.size));
3834
3751
  offset = cur.size;
3835
3752
  });
3836
3753
  });
@@ -4100,7 +4017,7 @@ async function startInBackground(start, deps = processDeps()) {
4100
4017
 
4101
4018
  // src/service-install.ts
4102
4019
  import { execFileSync as execFileSync3 } from "child_process";
4103
- import { mkdirSync as mkdirSync9, rmSync, writeFileSync as writeFileSync7 } from "fs";
4020
+ import { mkdirSync as mkdirSync8, rmSync, writeFileSync as writeFileSync7 } from "fs";
4104
4021
  import { posix, win32 as win322 } from "path";
4105
4022
  var SERVICE_LABEL = "online.proagentstore.pags-up";
4106
4023
  var SYSTEMD_UNIT = "pags-up.service";
@@ -4293,7 +4210,7 @@ function serviceRemovalPlan(ctx) {
4293
4210
  };
4294
4211
  }
4295
4212
  var defaultWrite = (path, content) => {
4296
- mkdirSync9(path.replace(/[\\/][^\\/]*$/, ""), { recursive: true });
4213
+ mkdirSync8(path.replace(/[\\/][^\\/]*$/, ""), { recursive: true });
4297
4214
  writeFileSync7(path, content);
4298
4215
  };
4299
4216
  var defaultRun = (file, args) => void execFileSync3(file, args, { stdio: "pipe", timeout: 6e4 });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.4.156",
3
+ "version": "0.4.158",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",