@jabezpauls/agentbox 0.1.8 → 0.1.10

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.
Files changed (2) hide show
  1. package/dist/agentbox.mjs +3501 -154
  2. package/package.json +3 -1
package/dist/agentbox.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // agentbox-cli v0.1.8
2
+ // agentbox-cli v0.1.10
3
3
  import { createRequire as __agentboxCreateRequire } from "node:module";
4
4
  const require = __agentboxCreateRequire(import.meta.url);
5
5
  var __create = Object.create;
@@ -1397,7 +1397,7 @@ var require_receiver = __commonJS({
1397
1397
  var require_sender = __commonJS({
1398
1398
  "../node_modules/ws/lib/sender.js"(exports, module) {
1399
1399
  "use strict";
1400
- var { Duplex: Duplex2 } = __require("stream");
1400
+ var { Duplex: Duplex3 } = __require("stream");
1401
1401
  var { randomFillSync } = __require("crypto");
1402
1402
  var {
1403
1403
  types: { isUint8Array }
@@ -2124,7 +2124,7 @@ var require_extension = __commonJS({
2124
2124
  if (dest[name] === void 0) dest[name] = [elem];
2125
2125
  else dest[name].push(elem);
2126
2126
  }
2127
- function parse(header) {
2127
+ function parse2(header) {
2128
2128
  const offers = /* @__PURE__ */ Object.create(null);
2129
2129
  let params = /* @__PURE__ */ Object.create(null);
2130
2130
  let mustUnescape = false;
@@ -2264,7 +2264,7 @@ var require_extension = __commonJS({
2264
2264
  }).join(", ");
2265
2265
  }).join(", ");
2266
2266
  }
2267
- module.exports = { format, parse };
2267
+ module.exports = { format, parse: parse2 };
2268
2268
  }
2269
2269
  });
2270
2270
 
@@ -2272,13 +2272,13 @@ var require_extension = __commonJS({
2272
2272
  var require_websocket = __commonJS({
2273
2273
  "../node_modules/ws/lib/websocket.js"(exports, module) {
2274
2274
  "use strict";
2275
- var EventEmitter2 = __require("events");
2275
+ var EventEmitter4 = __require("events");
2276
2276
  var https2 = __require("https");
2277
- var http3 = __require("http");
2278
- var net3 = __require("net");
2277
+ var http4 = __require("http");
2278
+ var net5 = __require("net");
2279
2279
  var tls = __require("tls");
2280
- var { randomBytes: randomBytes5, createHash: createHash3 } = __require("crypto");
2281
- var { Duplex: Duplex2, Readable: Readable2 } = __require("stream");
2280
+ var { randomBytes: randomBytes10, createHash: createHash5 } = __require("crypto");
2281
+ var { Duplex: Duplex3, Readable: Readable2 } = __require("stream");
2282
2282
  var { URL: URL2 } = __require("url");
2283
2283
  var PerMessageDeflate2 = require_permessage_deflate();
2284
2284
  var Receiver2 = require_receiver();
@@ -2298,13 +2298,13 @@ var require_websocket = __commonJS({
2298
2298
  var {
2299
2299
  EventTarget: { addEventListener, removeEventListener }
2300
2300
  } = require_event_target();
2301
- var { format, parse } = require_extension();
2301
+ var { format, parse: parse2 } = require_extension();
2302
2302
  var { toBuffer } = require_buffer_util();
2303
2303
  var kAborted = /* @__PURE__ */ Symbol("kAborted");
2304
2304
  var protocolVersions = [8, 13];
2305
2305
  var readyStates = ["CONNECTING", "OPEN", "CLOSING", "CLOSED"];
2306
2306
  var subprotocolRegex = /^[!#$%&'*+\-.0-9A-Z^_`|a-z~]+$/;
2307
- var WebSocket2 = class _WebSocket extends EventEmitter2 {
2307
+ var WebSocket2 = class _WebSocket extends EventEmitter4 {
2308
2308
  /**
2309
2309
  * Create a new `WebSocket`.
2310
2310
  *
@@ -2815,8 +2815,8 @@ var require_websocket = __commonJS({
2815
2815
  }
2816
2816
  }
2817
2817
  const defaultPort = isSecure ? 443 : 80;
2818
- const key = randomBytes5(16).toString("base64");
2819
- const request = isSecure ? https2.request : http3.request;
2818
+ const key = randomBytes10(16).toString("base64");
2819
+ const request2 = isSecure ? https2.request : http4.request;
2820
2820
  const protocolSet = /* @__PURE__ */ new Set();
2821
2821
  let perMessageDeflate;
2822
2822
  opts.createConnection = opts.createConnection || (isSecure ? tlsConnect : netConnect);
@@ -2893,12 +2893,12 @@ var require_websocket = __commonJS({
2893
2893
  if (opts.auth && !options.headers.authorization) {
2894
2894
  options.headers.authorization = "Basic " + Buffer.from(opts.auth).toString("base64");
2895
2895
  }
2896
- req = websocket._req = request(opts);
2896
+ req = websocket._req = request2(opts);
2897
2897
  if (websocket._redirects) {
2898
2898
  websocket.emit("redirect", websocket.url, req);
2899
2899
  }
2900
2900
  } else {
2901
- req = websocket._req = request(opts);
2901
+ req = websocket._req = request2(opts);
2902
2902
  }
2903
2903
  if (opts.timeout) {
2904
2904
  req.on("timeout", () => {
@@ -2945,7 +2945,7 @@ var require_websocket = __commonJS({
2945
2945
  abortHandshake(websocket, socket2, "Invalid Upgrade header");
2946
2946
  return;
2947
2947
  }
2948
- const digest = createHash3("sha1").update(key + GUID).digest("base64");
2948
+ const digest = createHash5("sha1").update(key + GUID).digest("base64");
2949
2949
  if (res.headers["sec-websocket-accept"] !== digest) {
2950
2950
  abortHandshake(websocket, socket2, "Invalid Sec-WebSocket-Accept header");
2951
2951
  return;
@@ -2975,7 +2975,7 @@ var require_websocket = __commonJS({
2975
2975
  }
2976
2976
  let extensions;
2977
2977
  try {
2978
- extensions = parse(secWebSocketExtensions);
2978
+ extensions = parse2(secWebSocketExtensions);
2979
2979
  } catch (err) {
2980
2980
  const message = "Invalid Sec-WebSocket-Extensions header";
2981
2981
  abortHandshake(websocket, socket2, message);
@@ -3019,12 +3019,12 @@ var require_websocket = __commonJS({
3019
3019
  }
3020
3020
  function netConnect(options) {
3021
3021
  options.path = options.socketPath;
3022
- return net3.connect(options);
3022
+ return net5.connect(options);
3023
3023
  }
3024
3024
  function tlsConnect(options) {
3025
3025
  options.path = void 0;
3026
3026
  if (!options.servername && options.servername !== "") {
3027
- options.servername = net3.isIP(options.host) ? "" : options.host;
3027
+ options.servername = net5.isIP(options.host) ? "" : options.host;
3028
3028
  }
3029
3029
  return tls.connect(options);
3030
3030
  }
@@ -3065,7 +3065,7 @@ var require_websocket = __commonJS({
3065
3065
  websocket._closeCode = code2;
3066
3066
  if (websocket._socket[kWebSocket] === void 0) return;
3067
3067
  websocket._socket.removeListener("data", socketOnData);
3068
- process.nextTick(resume, websocket._socket);
3068
+ process.nextTick(resume2, websocket._socket);
3069
3069
  if (code2 === 1005) websocket.close();
3070
3070
  else websocket.close(code2, reason);
3071
3071
  }
@@ -3077,7 +3077,7 @@ var require_websocket = __commonJS({
3077
3077
  const websocket = this[kWebSocket];
3078
3078
  if (websocket._socket[kWebSocket] !== void 0) {
3079
3079
  websocket._socket.removeListener("data", socketOnData);
3080
- process.nextTick(resume, websocket._socket);
3080
+ process.nextTick(resume2, websocket._socket);
3081
3081
  websocket.close(err[kStatusCode]);
3082
3082
  }
3083
3083
  if (!websocket._errorEmitted) {
@@ -3099,7 +3099,7 @@ var require_websocket = __commonJS({
3099
3099
  function receiverOnPong(data) {
3100
3100
  this[kWebSocket].emit("pong", data);
3101
3101
  }
3102
- function resume(stream) {
3102
+ function resume2(stream) {
3103
3103
  stream.resume();
3104
3104
  }
3105
3105
  function senderOnError(err) {
@@ -3169,7 +3169,7 @@ var require_stream = __commonJS({
3169
3169
  "../node_modules/ws/lib/stream.js"(exports, module) {
3170
3170
  "use strict";
3171
3171
  var WebSocket2 = require_websocket();
3172
- var { Duplex: Duplex2 } = __require("stream");
3172
+ var { Duplex: Duplex3 } = __require("stream");
3173
3173
  function emitClose(stream) {
3174
3174
  stream.emit("close");
3175
3175
  }
@@ -3187,7 +3187,7 @@ var require_stream = __commonJS({
3187
3187
  }
3188
3188
  function createWebSocketStream2(ws, options) {
3189
3189
  let terminateOnDestroy = true;
3190
- const duplex = new Duplex2({
3190
+ const duplex = new Duplex3({
3191
3191
  ...options,
3192
3192
  autoDestroy: false,
3193
3193
  emitClose: false,
@@ -3267,7 +3267,7 @@ var require_subprotocol = __commonJS({
3267
3267
  "../node_modules/ws/lib/subprotocol.js"(exports, module) {
3268
3268
  "use strict";
3269
3269
  var { tokenChars } = require_validation();
3270
- function parse(header) {
3270
+ function parse2(header) {
3271
3271
  const protocols = /* @__PURE__ */ new Set();
3272
3272
  let start = -1;
3273
3273
  let end = -1;
@@ -3303,7 +3303,7 @@ var require_subprotocol = __commonJS({
3303
3303
  protocols.add(protocol);
3304
3304
  return protocols;
3305
3305
  }
3306
- module.exports = { parse };
3306
+ module.exports = { parse: parse2 };
3307
3307
  }
3308
3308
  });
3309
3309
 
@@ -3311,10 +3311,10 @@ var require_subprotocol = __commonJS({
3311
3311
  var require_websocket_server = __commonJS({
3312
3312
  "../node_modules/ws/lib/websocket-server.js"(exports, module) {
3313
3313
  "use strict";
3314
- var EventEmitter2 = __require("events");
3315
- var http3 = __require("http");
3316
- var { Duplex: Duplex2 } = __require("stream");
3317
- var { createHash: createHash3 } = __require("crypto");
3314
+ var EventEmitter4 = __require("events");
3315
+ var http4 = __require("http");
3316
+ var { Duplex: Duplex3 } = __require("stream");
3317
+ var { createHash: createHash5 } = __require("crypto");
3318
3318
  var extension2 = require_extension();
3319
3319
  var PerMessageDeflate2 = require_permessage_deflate();
3320
3320
  var subprotocol2 = require_subprotocol();
@@ -3324,7 +3324,7 @@ var require_websocket_server = __commonJS({
3324
3324
  var RUNNING = 0;
3325
3325
  var CLOSING = 1;
3326
3326
  var CLOSED = 2;
3327
- var WebSocketServer2 = class extends EventEmitter2 {
3327
+ var WebSocketServer2 = class extends EventEmitter4 {
3328
3328
  /**
3329
3329
  * Create a `WebSocketServer` instance.
3330
3330
  *
@@ -3393,8 +3393,8 @@ var require_websocket_server = __commonJS({
3393
3393
  );
3394
3394
  }
3395
3395
  if (options.port != null) {
3396
- this._server = http3.createServer((req, res) => {
3397
- const body = http3.STATUS_CODES[426];
3396
+ this._server = http4.createServer((req, res) => {
3397
+ const body = http4.STATUS_CODES[426];
3398
3398
  res.writeHead(426, {
3399
3399
  "Content-Length": body.length,
3400
3400
  "Content-Type": "text/plain"
@@ -3621,7 +3621,7 @@ var require_websocket_server = __commonJS({
3621
3621
  );
3622
3622
  }
3623
3623
  if (this._state > RUNNING) return abortHandshake(socket2, 503);
3624
- const digest = createHash3("sha1").update(key + GUID).digest("base64");
3624
+ const digest = createHash5("sha1").update(key + GUID).digest("base64");
3625
3625
  const headers = [
3626
3626
  "HTTP/1.1 101 Switching Protocols",
3627
3627
  "Upgrade: websocket",
@@ -3683,7 +3683,7 @@ var require_websocket_server = __commonJS({
3683
3683
  this.destroy();
3684
3684
  }
3685
3685
  function abortHandshake(socket2, code2, message, headers) {
3686
- message = message || http3.STATUS_CODES[code2];
3686
+ message = message || http4.STATUS_CODES[code2];
3687
3687
  headers = {
3688
3688
  Connection: "close",
3689
3689
  "Content-Type": "text/html",
@@ -3692,7 +3692,7 @@ var require_websocket_server = __commonJS({
3692
3692
  };
3693
3693
  socket2.once("finish", socket2.destroy);
3694
3694
  socket2.end(
3695
- `HTTP/1.1 ${code2} ${http3.STATUS_CODES[code2]}\r
3695
+ `HTTP/1.1 ${code2} ${http4.STATUS_CODES[code2]}\r
3696
3696
  ` + Object.keys(headers).map((h) => `${h}: ${headers[h]}`).join("\r\n") + "\r\n\r\n" + message
3697
3697
  );
3698
3698
  }
@@ -3739,9 +3739,9 @@ var UsageError = class extends CliError {
3739
3739
  }
3740
3740
  };
3741
3741
  var ApiError = class extends CliError {
3742
- constructor(status2, message, code2 = null) {
3743
- super(message, status2 === 401 ? EXIT.AUTH : status2 === 404 ? EXIT.NOT_FOUND : EXIT.FAILURE);
3744
- this.status = status2;
3742
+ constructor(status3, message, code2 = null) {
3743
+ super(message, status3 === 401 ? EXIT.AUTH : status3 === 404 ? EXIT.NOT_FOUND : EXIT.FAILURE);
3744
+ this.status = status3;
3745
3745
  this.code = code2;
3746
3746
  this.name = "ApiError";
3747
3747
  }
@@ -3868,7 +3868,8 @@ function parseSize(raw) {
3868
3868
  }
3869
3869
 
3870
3870
  // src/version.ts
3871
- var VERSION = true ? "v0.1.8" : "0.0.0-dev";
3871
+ var VERSION = true ? "v0.1.10" : "0.0.0-dev";
3872
+ var STANDALONE = typeof __AGENTBOX_STANDALONE__ === "boolean" && __AGENTBOX_STANDALONE__;
3872
3873
  var MIN_NODE_MAJOR = 20;
3873
3874
  function nodeMajor(version = process.versions.node) {
3874
3875
  return Number(version.split(".")[0]);
@@ -4082,8 +4083,8 @@ var status = command({
4082
4083
  else if (list.length === 0) row("apps", "none");
4083
4084
  else {
4084
4085
  const isShared = (a) => Boolean(a.visibility?.mode && a.visibility.mode !== "private" && (a.visibility.expiresAt == null || a.visibility.expiresAt > ctx.now()));
4085
- const shared = list.filter(isShared).length;
4086
- row("apps", `${list.length}${shared ? ` (${shared} shared)` : ""}`);
4086
+ const shared2 = list.filter(isShared).length;
4087
+ row("apps", `${list.length}${shared2 ? ` (${shared2} shared)` : ""}`);
4087
4088
  const lines = table(
4088
4089
  ["NAME", "PORT", "STATE", "SHARING"],
4089
4090
  list.map((a) => [
@@ -4265,13 +4266,13 @@ var TunnelStream = class extends Duplex {
4265
4266
  this.ws = ws;
4266
4267
  ws.on("message", (data, isBinary) => {
4267
4268
  if (!isBinary) {
4268
- let control = null;
4269
+ let control2 = null;
4269
4270
  try {
4270
- control = JSON.parse(data.toString("utf8"));
4271
+ control2 = JSON.parse(data.toString("utf8"));
4271
4272
  } catch {
4272
4273
  }
4273
- if (control?.type === "error") {
4274
- const err = new TunnelError(control.message ?? "the tunnel failed");
4274
+ if (control2?.type === "error") {
4275
+ const err = new TunnelError(control2.message ?? "the tunnel failed");
4275
4276
  setImmediate(() => this.destroy(err));
4276
4277
  }
4277
4278
  return;
@@ -4316,8 +4317,8 @@ function openTunnel(client, target, opts = {}) {
4316
4317
  const early = (data, isBinary) => {
4317
4318
  if (isBinary) return;
4318
4319
  try {
4319
- const control = JSON.parse(data.toString("utf8"));
4320
- if (control.type === "error") fail(new TunnelError(control.message ?? "the tunnel failed"));
4320
+ const control2 = JSON.parse(data.toString("utf8"));
4321
+ if (control2.type === "error") fail(new TunnelError(control2.message ?? "the tunnel failed"));
4321
4322
  } catch {
4322
4323
  }
4323
4324
  };
@@ -4335,10 +4336,10 @@ function openTunnel(client, target, opts = {}) {
4335
4336
  });
4336
4337
  ws.on("message", early);
4337
4338
  ws.once("unexpected-response", (_req, res) => {
4338
- const status2 = res.statusCode ?? 0;
4339
+ const status3 = res.statusCode ?? 0;
4339
4340
  res.resume();
4340
4341
  fail(
4341
- status2 === 401 ? new CliError("the box did not accept this device's sign-in; run `agentbox login` again", EXIT.AUTH) : status2 === 404 ? new CliError(`${client.origin} has no tunnel endpoint yet; update the box`, EXIT.NOT_FOUND) : new TunnelError(`the box refused the tunnel (HTTP ${status2})`)
4342
+ status3 === 401 ? new CliError("the box did not accept this device's sign-in; run `agentbox login` again", EXIT.AUTH) : status3 === 404 ? new CliError(`${client.origin} has no tunnel endpoint yet; update the box`, EXIT.NOT_FOUND) : new TunnelError(`the box refused the tunnel (HTTP ${status3})`)
4342
4343
  );
4343
4344
  });
4344
4345
  ws.once("error", (err) => fail(new CliError(`could not open a tunnel to ${client.origin}: ${err.message}`, EXIT.UNREACHABLE)));
@@ -4528,20 +4529,20 @@ var share = command({
4528
4529
  const body = { mode: passcode ? "passcode" : "link" };
4529
4530
  if (expires !== void 0) body.expiresIn = parseExpiry(expires);
4530
4531
  if (chosen !== void 0) body.passcode = chosen;
4531
- const shared = await client.json("PUT", `/_gate/apps/${encodeURIComponent(app.id)}/visibility`, {
4532
+ const shared2 = await client.json("PUT", `/_gate/apps/${encodeURIComponent(app.id)}/visibility`, {
4532
4533
  body: { json: body },
4533
4534
  what: `sharing ${app.name}`
4534
4535
  });
4535
4536
  const url = `${box.url}/a/${app.id}/`;
4536
4537
  if (ctx.json) {
4537
- ctx.printJson({ ...shared, url });
4538
+ ctx.printJson({ ...shared2, url });
4538
4539
  return;
4539
4540
  }
4540
4541
  ctx.out(`${url}
4541
4542
  `);
4542
- if (shared.passcode) ctx.out(`passcode ${shared.passcode}
4543
+ if (shared2.passcode) ctx.out(`passcode ${shared2.passcode}
4543
4544
  `);
4544
- ctx.err(`${safeText(app.name)} is ${safeText(sharing(shared, ctx.now()))}${shared.passcode ? "; the passcode is shown only this once" : ""}.
4545
+ ctx.err(`${safeText(app.name)} is ${safeText(sharing(shared2, ctx.now()))}${shared2.passcode ? "; the passcode is shown only this once" : ""}.
4545
4546
  `);
4546
4547
  }
4547
4548
  });
@@ -4676,10 +4677,10 @@ function hasBox(boxes2, name) {
4676
4677
  function emptyConfig() {
4677
4678
  return { version: 1, current: null, boxes: boxTable() };
4678
4679
  }
4679
- function configDir(env = process.env, home = os.homedir(), platform = process.platform) {
4680
+ function configDir(env = process.env, home2 = os.homedir(), platform = process.platform) {
4680
4681
  if (platform === "win32" && env.APPDATA && path3.win32.isAbsolute(env.APPDATA)) return path3.win32.join(env.APPDATA, "agentbox");
4681
4682
  const xdg = env.XDG_CONFIG_HOME;
4682
- const base = xdg && path3.isAbsolute(xdg) ? xdg : path3.join(home, ".config");
4683
+ const base = xdg && path3.isAbsolute(xdg) ? xdg : path3.join(home2, ".config");
4683
4684
  return path3.join(base, "agentbox");
4684
4685
  }
4685
4686
  var BOX_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
@@ -4914,7 +4915,7 @@ async function pollForToken(client, start, deps = {}) {
4914
4915
  await wait(delay, deps.signal);
4915
4916
  delay = interval;
4916
4917
  if (now() > deadline) throw new CliError("the code expired before it was approved; run `agentbox login` again");
4917
- let status2;
4918
+ let status3;
4918
4919
  let body;
4919
4920
  let retryAfter;
4920
4921
  try {
@@ -4924,7 +4925,7 @@ async function pollForToken(client, start, deps = {}) {
4924
4925
  headers: { accept: "application/json" },
4925
4926
  ...deps.signal ? { signal: deps.signal } : {}
4926
4927
  });
4927
- status2 = res.status;
4928
+ status3 = res.status;
4928
4929
  retryAfter = typeof res.headers["retry-after"] === "string" ? res.headers["retry-after"] : void 0;
4929
4930
  const text = await res.text();
4930
4931
  try {
@@ -4939,17 +4940,17 @@ async function pollForToken(client, start, deps = {}) {
4939
4940
  }
4940
4941
  throw err;
4941
4942
  }
4942
- if (status2 >= 500) {
4943
- if (++failures > maxFailures) throw new CliError(`the box kept failing to answer the sign-in (HTTP ${status2}); try again later`);
4943
+ if (status3 >= 500) {
4944
+ if (++failures > maxFailures) throw new CliError(`the box kept failing to answer the sign-in (HTTP ${status3}); try again later`);
4944
4945
  delay = backoff(interval, failures);
4945
4946
  continue;
4946
4947
  }
4947
4948
  failures = 0;
4948
- if (status2 === 200) {
4949
+ if (status3 === 200) {
4949
4950
  if (typeof body.token === "string" && TOKEN_SHAPE.test(body.token)) return body.token;
4950
4951
  throw new CliError("the box answered with something that is not a device token");
4951
4952
  }
4952
- if (status2 === 429) {
4953
+ if (status3 === 429) {
4953
4954
  const s = Number(retryAfter);
4954
4955
  if (Number.isFinite(s) && s > 0) await wait(s * 1e3, deps.signal);
4955
4956
  continue;
@@ -4966,7 +4967,7 @@ async function pollForToken(client, start, deps = {}) {
4966
4967
  case "expired_token":
4967
4968
  throw new CliError("the code expired, or was already used; run `agentbox login` again");
4968
4969
  default:
4969
- throw new CliError(`the box refused the sign-in (HTTP ${status2}${typeof body.error === "string" ? `: ${body.error}` : ""})`);
4970
+ throw new CliError(`the box refused the sign-in (HTTP ${status3}${typeof body.error === "string" ? `: ${body.error}` : ""})`);
4970
4971
  }
4971
4972
  }
4972
4973
  }
@@ -4990,9 +4991,9 @@ import os3 from "node:os";
4990
4991
  import path5 from "node:path";
4991
4992
 
4992
4993
  // ../shared/src/index.ts
4993
- function encodePathParam(path12) {
4994
+ function encodePathParam(path22) {
4994
4995
  let out = "";
4995
- for (const ch of path12) {
4996
+ for (const ch of path22) {
4996
4997
  const cp = ch.codePointAt(0) ?? 0;
4997
4998
  out += cp >= 56448 && cp <= 56575 ? `%${(cp - 56320).toString(16).toUpperCase()}` : encodeURIComponent(ch);
4998
4999
  }
@@ -5064,8 +5065,8 @@ var FilesApi = class {
5064
5065
  copy(from, to, overwrite) {
5065
5066
  return this.client.json("POST", "/api/files/copy", { body: { json: { from, to, overwrite } }, what: `copying ${from}`, idleMs: 6e5 });
5066
5067
  }
5067
- trash(paths) {
5068
- return this.client.json("POST", "/api/files/trash", { body: { json: { paths } }, what: "moving to the trash" });
5068
+ trash(paths2) {
5069
+ return this.client.json("POST", "/api/files/trash", { body: { json: { paths: paths2 } }, what: "moving to the trash" });
5069
5070
  }
5070
5071
  startUpload(p, size, overwrite) {
5071
5072
  return this.client.json("POST", "/api/files/uploads", { body: { json: { path: p, size, overwrite } }, what: `uploading to ${p}` });
@@ -5401,8 +5402,8 @@ function readIfThere(file) {
5401
5402
  }
5402
5403
  }
5403
5404
  function tilde(ctx, p) {
5404
- const home = homeDir(ctx);
5405
- return p.startsWith(home + path5.sep) ? `~${p.slice(home.length)}` : p;
5405
+ const home2 = homeDir(ctx);
5406
+ return p.startsWith(home2 + path5.sep) ? `~${p.slice(home2.length)}` : p;
5406
5407
  }
5407
5408
  function sshConfigArgs(ctx) {
5408
5409
  const mine = path5.join(sshDir(ctx), "config");
@@ -6404,7 +6405,7 @@ var put = command({
6404
6405
  const force = bool(p.options, "force");
6405
6406
  const chunkRaw = typeof p.options["chunk-size"] === "string" ? parseSize(p.options["chunk-size"]) : MAX_CHUNK;
6406
6407
  if (chunkRaw < 1024 || chunkRaw > MAX_CHUNK) throw new UsageError(`--chunk-size must be from 1K to ${MAX_CHUNK / 1024 / 1024}M`);
6407
- const resume = new ResumeStore(ctx.config.dir);
6408
+ const resume2 = new ResumeStore(ctx.config.dir);
6408
6409
  let target = await files.stat(remote);
6409
6410
  if (!target && remote.endsWith("/")) target = await files.mkdir(remote);
6410
6411
  const intoDir = isDir(target);
@@ -6419,7 +6420,7 @@ var put = command({
6419
6420
  remote: dest,
6420
6421
  overwrite: force,
6421
6422
  chunkSize: chunkRaw,
6422
- resume,
6423
+ resume: resume2,
6423
6424
  signal: ctx.abort.signal,
6424
6425
  onProgress: (sent) => bar.update(sent),
6425
6426
  onResume: (received, total) => ctx.err(`${file}: resuming at ${formatBytes(received)} of ${formatBytes(total)}
@@ -6491,7 +6492,7 @@ var rm = command({
6491
6492
  details: "Nothing is deleted outright: restore from Files \u2192 Trash in the app.",
6492
6493
  async run(ctx, p) {
6493
6494
  const files = await filesApi(ctx);
6494
- const paths = [];
6495
+ const paths2 = [];
6495
6496
  for (const target of p.operands) {
6496
6497
  const e = await files.stat(target);
6497
6498
  if (!e) {
@@ -6499,10 +6500,10 @@ var rm = command({
6499
6500
  throw new CliError(`${target}: no such file or folder on the box`, EXIT.NOT_FOUND);
6500
6501
  }
6501
6502
  if (e.type === "dir" && !bool(p.options, "recursive")) throw new CliError(`${e.path} is a folder; add -r to trash it and everything in it`);
6502
- paths.push(e.path);
6503
+ paths2.push(e.path);
6503
6504
  }
6504
- if (paths.length === 0) return;
6505
- const result = await files.trash(paths);
6505
+ if (paths2.length === 0) return;
6506
+ const result = await files.trash(paths2);
6506
6507
  for (const t2 of result.trashed) ctx.err(`trashed ${safeText(t2.originalPath)}
6507
6508
  `);
6508
6509
  }
@@ -6577,8 +6578,8 @@ function editorCommand(editor, file, platform, env = process.env) {
6577
6578
  if (!CMD_SAFE.test(file)) {
6578
6579
  throw new CliError(`the temporary file's path (${file}) has characters cmd.exe would act on; set TEMP to a plain folder, or use an editor that is an .exe`);
6579
6580
  }
6580
- const quote = (w) => `"${w.replace(/"/g, "")}"`;
6581
- return { command: [resolved, ...rest, file].map(quote).join(" "), args: [], shell: true };
6581
+ const quote2 = (w) => `"${w.replace(/"/g, "")}"`;
6582
+ return { command: [resolved, ...rest, file].map(quote2).join(" "), args: [], shell: true };
6582
6583
  }
6583
6584
  function runEditor(editor, file, platform = process.platform, env = process.env) {
6584
6585
  return new Promise((resolve, reject) => {
@@ -6805,14 +6806,3335 @@ var add = command({
6805
6806
  });
6806
6807
  var HERDR_COMMANDS = [add, call, socket];
6807
6808
 
6809
+ // src/commands/node.ts
6810
+ import { spawn as spawn7 } from "node:child_process";
6811
+ import fs20 from "node:fs";
6812
+ import os11 from "node:os";
6813
+ import path18 from "node:path";
6814
+
6815
+ // src/node/control.ts
6816
+ import { randomBytes as randomBytes4, timingSafeEqual } from "node:crypto";
6817
+ import fs9 from "node:fs";
6818
+ import net3 from "node:net";
6819
+
6820
+ // src/node/paths.ts
6821
+ import { randomBytes as randomBytes3 } from "node:crypto";
6822
+ import fs8 from "node:fs";
6823
+ import os7 from "node:os";
6824
+ import path9 from "node:path";
6825
+ function nodeDir(env = process.env, home2 = os7.homedir(), platform = process.platform) {
6826
+ if (platform === "win32") {
6827
+ const local = env.LOCALAPPDATA && path9.win32.isAbsolute(env.LOCALAPPDATA) ? env.LOCALAPPDATA : path9.win32.join(home2, "AppData", "Local");
6828
+ return path9.win32.join(local, "agentbox", "node");
6829
+ }
6830
+ const xdg = env.XDG_DATA_HOME;
6831
+ const base = xdg && path9.isAbsolute(xdg) ? xdg : path9.join(home2, ".local", "share");
6832
+ return path9.join(base, "agentbox", "node");
6833
+ }
6834
+ function nodePaths(dir) {
6835
+ return {
6836
+ dir,
6837
+ policy: path9.join(dir, "policy.json"),
6838
+ credentials: path9.join(dir, "credentials.json"),
6839
+ secrets: path9.join(dir, "secrets"),
6840
+ bin: path9.join(dir, "bin"),
6841
+ state: path9.join(dir, "state"),
6842
+ control: path9.join(dir, "control.sock"),
6843
+ controlSecret: path9.join(dir, "control.secret")
6844
+ };
6845
+ }
6846
+ function ensurePrivateDir(dir) {
6847
+ fs8.mkdirSync(dir, { recursive: true, mode: 448 });
6848
+ if (process.platform !== "win32") fs8.chmodSync(dir, 448);
6849
+ }
6850
+ function writePrivate2(file, contents, mode = 384) {
6851
+ ensurePrivateDir(path9.dirname(file));
6852
+ const tmp = path9.join(path9.dirname(file), `.${path9.basename(file)}.${randomBytes3(6).toString("hex")}.tmp`);
6853
+ const fd = fs8.openSync(tmp, "wx", mode);
6854
+ try {
6855
+ fs8.writeFileSync(fd, contents);
6856
+ if (process.platform !== "win32") fs8.fchmodSync(fd, mode);
6857
+ fs8.fsyncSync(fd);
6858
+ } catch (err) {
6859
+ fs8.closeSync(fd);
6860
+ fs8.rmSync(tmp, { force: true });
6861
+ throw err;
6862
+ }
6863
+ fs8.closeSync(fd);
6864
+ try {
6865
+ fs8.renameSync(tmp, file);
6866
+ } catch (err) {
6867
+ fs8.rmSync(tmp, { force: true });
6868
+ throw err;
6869
+ }
6870
+ }
6871
+
6872
+ // src/node/control.ts
6873
+ var COMMANDS = /* @__PURE__ */ new Set(["status", "pause", "resume", "leave"]);
6874
+ async function startControl(socketPath, secretFile, handle) {
6875
+ const secret = randomBytes4(32).toString("base64url");
6876
+ writePrivate2(secretFile, secret);
6877
+ const want = Buffer.from(secret);
6878
+ fs9.rmSync(socketPath, { force: true });
6879
+ const server = net3.createServer((sock) => {
6880
+ let buf = "";
6881
+ sock.setTimeout(1e4, () => sock.destroy());
6882
+ sock.on("error", () => sock.destroy());
6883
+ sock.on("data", (c) => {
6884
+ buf += c.toString("utf8");
6885
+ if (buf.length > 16 * 1024) return sock.destroy();
6886
+ const nl = buf.indexOf("\n");
6887
+ if (nl < 0) return;
6888
+ const reply = (o) => {
6889
+ sock.end(`${JSON.stringify(o)}
6890
+ `);
6891
+ };
6892
+ let req;
6893
+ try {
6894
+ req = JSON.parse(buf.slice(0, nl));
6895
+ } catch {
6896
+ return reply({ ok: false, error: "bad request" });
6897
+ }
6898
+ const got = Buffer.from(typeof req.secret === "string" ? req.secret : "");
6899
+ if (got.length !== want.length || !timingSafeEqual(got, want)) return reply({ ok: false, error: "refused" });
6900
+ if (typeof req.command !== "string" || !COMMANDS.has(req.command)) return reply({ ok: false, error: "unknown command" });
6901
+ handle(req.command).then(
6902
+ (out) => reply({ ok: true, ...out }),
6903
+ (err) => reply({ ok: false, error: err.message })
6904
+ );
6905
+ });
6906
+ });
6907
+ await new Promise((resolve, reject) => {
6908
+ server.once("error", reject);
6909
+ server.listen(socketPath, () => {
6910
+ server.off("error", reject);
6911
+ resolve();
6912
+ });
6913
+ });
6914
+ if (process.platform !== "win32") fs9.chmodSync(socketPath, 384);
6915
+ return {
6916
+ close: () => {
6917
+ server.close();
6918
+ fs9.rmSync(socketPath, { force: true });
6919
+ }
6920
+ };
6921
+ }
6922
+ function callControl(socketPath, secretFile, command2, timeoutMs = 1e4) {
6923
+ let secret;
6924
+ try {
6925
+ secret = fs9.readFileSync(secretFile, "utf8").trim();
6926
+ } catch {
6927
+ return Promise.resolve(null);
6928
+ }
6929
+ return new Promise((resolve) => {
6930
+ const sock = net3.connect(socketPath);
6931
+ let buf = "";
6932
+ const timer = setTimeout(() => {
6933
+ sock.destroy();
6934
+ resolve(null);
6935
+ }, timeoutMs);
6936
+ sock.on("connect", () => sock.write(`${JSON.stringify({ secret, command: command2 })}
6937
+ `));
6938
+ sock.on("data", (c) => buf += c.toString("utf8"));
6939
+ sock.on("end", () => {
6940
+ clearTimeout(timer);
6941
+ try {
6942
+ resolve(JSON.parse(buf));
6943
+ } catch {
6944
+ resolve(null);
6945
+ }
6946
+ });
6947
+ sock.on("error", () => {
6948
+ clearTimeout(timer);
6949
+ resolve(null);
6950
+ });
6951
+ });
6952
+ }
6953
+
6954
+ // src/node/credentials.ts
6955
+ import fs10 from "node:fs";
6956
+
6957
+ // ../hostcore/src/link/frame.ts
6958
+ var FrameType = {
6959
+ open: 1,
6960
+ data: 2,
6961
+ close: 3,
6962
+ window: 4,
6963
+ control: 5
6964
+ };
6965
+ var TYPES = new Set(Object.values(FrameType));
6966
+ var HEADER_BYTES = 5;
6967
+ var CONTROL_MAX_BYTES = 64 * 1024;
6968
+ var HELLO_MAX_BYTES = 16 * 1024;
6969
+ var MAX_STREAM_ID = 4294967295;
6970
+ var FrameError = class extends Error {
6971
+ };
6972
+ function encodeFrame(type, stream, payload = Buffer.alloc(0)) {
6973
+ if (!TYPES.has(type)) throw new FrameError(`unknown frame type ${type}`);
6974
+ if (!Number.isInteger(stream) || stream < 0 || stream > MAX_STREAM_ID) throw new FrameError(`bad stream id ${stream}`);
6975
+ if (type === FrameType.control && stream !== 0) throw new FrameError("control frames are on stream 0");
6976
+ if (type !== FrameType.control && stream === 0) throw new FrameError("stream 0 is the link's own");
6977
+ const out = Buffer.allocUnsafe(HEADER_BYTES + payload.length);
6978
+ out.writeUInt8(type, 0);
6979
+ out.writeUInt32BE(stream, 1);
6980
+ out.set(payload, HEADER_BYTES);
6981
+ return out;
6982
+ }
6983
+ function decodeFrame(buf) {
6984
+ if (buf.length < HEADER_BYTES) throw new FrameError("frame too short");
6985
+ const type = buf.readUInt8(0);
6986
+ if (!TYPES.has(type)) throw new FrameError(`unknown frame type ${type}`);
6987
+ const stream = buf.readUInt32BE(1);
6988
+ const payload = buf.subarray(HEADER_BYTES);
6989
+ if (type === FrameType.control) {
6990
+ if (stream !== 0) throw new FrameError("control frames are on stream 0");
6991
+ if (payload.length > CONTROL_MAX_BYTES) throw new FrameError("control message too large");
6992
+ } else if (stream === 0) {
6993
+ throw new FrameError("stream 0 is the link's own");
6994
+ }
6995
+ return { type, stream, payload };
6996
+ }
6997
+ function encodeControl(message) {
6998
+ const json2 = Buffer.from(JSON.stringify(message), "utf8");
6999
+ if (json2.length > CONTROL_MAX_BYTES) throw new FrameError("control message too large");
7000
+ return encodeFrame(FrameType.control, 0, json2);
7001
+ }
7002
+ function decodeControl(payload) {
7003
+ let value;
7004
+ try {
7005
+ value = JSON.parse(payload.toString("utf8"));
7006
+ } catch {
7007
+ throw new FrameError("control message is not JSON");
7008
+ }
7009
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new FrameError("control message is not an object");
7010
+ const type = value.type;
7011
+ if (typeof type !== "string" || type === "") throw new FrameError("control message has no type");
7012
+ return value;
7013
+ }
7014
+
7015
+ // ../hostcore/src/link/protocol.ts
7016
+ import { createHash as createHash3, createHmac, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
7017
+ var PROTOCOL_VERSION = 1;
7018
+ var LINK_PATH = "/_gate/node";
7019
+ var DUPLICATE_BACKOFF_MS = 5 * 6e4;
7020
+ var NODE_TOKEN_SHAPE = /^abn_[A-Za-z0-9_-]{43}$/;
7021
+ function tokenDigest(token) {
7022
+ return createHash3("sha256").update(token).digest("hex");
7023
+ }
7024
+ function revokeMac(tokenDigestHex, machineId, nonce) {
7025
+ return createHmac("sha256", tokenDigestHex).update(`agentbox-revoked
7026
+ ${machineId}
7027
+ ${nonce}`).digest("hex");
7028
+ }
7029
+ function verifyRevoke(token, machineId, nonce, mac) {
7030
+ if (typeof mac !== "string" || !/^[0-9a-f]{64}$/.test(mac)) return false;
7031
+ const want = Buffer.from(revokeMac(tokenDigest(token), machineId, nonce), "hex");
7032
+ return timingSafeEqual2(want, Buffer.from(mac, "hex"));
7033
+ }
7034
+
7035
+ // ../hostcore/src/link/stream.ts
7036
+ import { Duplex as Duplex2 } from "node:stream";
7037
+ var STREAM_WINDOW = 256 * 1024;
7038
+ var DATA_CHUNK = 64 * 1024;
7039
+ var STREAM_BUDGET = 64 * 1024 * 1024;
7040
+ function decodeOpen(payload) {
7041
+ let v = {};
7042
+ try {
7043
+ v = JSON.parse(payload.toString("utf8"));
7044
+ } catch {
7045
+ }
7046
+ const id = (s) => typeof s === "string" && /^[A-Za-z0-9_-]{1,128}$/.test(s) ? s : null;
7047
+ return { kind: v.kind === "upgrade" ? "upgrade" : "http", session: id(v.session), grant: id(v.grant) };
7048
+ }
7049
+ function windowPayload(n) {
7050
+ const b = Buffer.allocUnsafe(4);
7051
+ b.writeUInt32BE(n, 0);
7052
+ return b;
7053
+ }
7054
+ var LinkStream = class extends Duplex2 {
7055
+ constructor(id, sendFrame, window = STREAM_WINDOW) {
7056
+ super({ allowHalfOpen: false });
7057
+ this.id = id;
7058
+ this.sendFrame = sendFrame;
7059
+ this.window = window;
7060
+ this.credit = window;
7061
+ this.allowance = window;
7062
+ }
7063
+ id;
7064
+ sendFrame;
7065
+ window;
7066
+ /** Bytes the peer will still take from us. */
7067
+ credit;
7068
+ /** Bytes the peer may still send us before it must wait for a window frame. */
7069
+ allowance;
7070
+ /** Bytes received and pushed to our reader, not yet credited back. */
7071
+ unacked = 0;
7072
+ pending = null;
7073
+ remoteClosed = false;
7074
+ closeSent = false;
7075
+ // What net.Socket has and Node's HTTP server, its client and `ws` call.
7076
+ setTimeout(_ms, cb) {
7077
+ if (cb) this.once("timeout", cb);
7078
+ return this;
7079
+ }
7080
+ setNoDelay() {
7081
+ return this;
7082
+ }
7083
+ setKeepAlive() {
7084
+ return this;
7085
+ }
7086
+ ref() {
7087
+ return this;
7088
+ }
7089
+ unref() {
7090
+ return this;
7091
+ }
7092
+ /** A frame for this stream, from the link. */
7093
+ receive(frame) {
7094
+ if (this.destroyed) return;
7095
+ switch (frame.type) {
7096
+ case FrameType.data: {
7097
+ if (this.remoteClosed) return;
7098
+ const n = frame.payload.length;
7099
+ if (n === 0) return;
7100
+ this.allowance -= n;
7101
+ if (this.allowance < 0) {
7102
+ this.destroy(new Error("the far end sent past its window"));
7103
+ return;
7104
+ }
7105
+ this.unacked += n;
7106
+ if (this.push(Buffer.from(frame.payload)) && this.unacked >= this.window / 4) this.grant();
7107
+ return;
7108
+ }
7109
+ case FrameType.window: {
7110
+ if (frame.payload.length !== 4) {
7111
+ this.destroy(new Error("bad window frame"));
7112
+ return;
7113
+ }
7114
+ this.credit += frame.payload.readUInt32BE(0);
7115
+ if (this.credit > this.window * 4) {
7116
+ this.destroy(new Error("the far end widened the window past any it was given"));
7117
+ return;
7118
+ }
7119
+ this.flush();
7120
+ return;
7121
+ }
7122
+ case FrameType.close:
7123
+ this.remoteEnd();
7124
+ return;
7125
+ default:
7126
+ return;
7127
+ }
7128
+ }
7129
+ /** The far end is gone (its close, or the link's end). */
7130
+ remoteEnd() {
7131
+ if (this.remoteClosed) return;
7132
+ this.remoteClosed = true;
7133
+ this.closeSent = true;
7134
+ const p = this.pending;
7135
+ this.pending = null;
7136
+ p?.cb();
7137
+ this.push(null);
7138
+ if (this.readableLength === 0) setImmediate(() => this.destroy());
7139
+ else this.once("end", () => this.destroy());
7140
+ }
7141
+ grant() {
7142
+ if (this.unacked <= 0 || this.remoteClosed || this.closeSent) return;
7143
+ const n = this.unacked;
7144
+ this.unacked = 0;
7145
+ this.allowance += n;
7146
+ this.sendFrame(encodeFrame(FrameType.window, this.id, windowPayload(n)));
7147
+ }
7148
+ _read() {
7149
+ this.grant();
7150
+ }
7151
+ flush() {
7152
+ const p = this.pending;
7153
+ if (!p) return;
7154
+ this.pending = null;
7155
+ this.sendChunks(p.chunk, p.cb);
7156
+ }
7157
+ sendChunks(chunk, cb) {
7158
+ let off = 0;
7159
+ while (off < chunk.length) {
7160
+ if (this.remoteClosed) return cb();
7161
+ if (this.credit <= 0) {
7162
+ this.pending = { chunk: chunk.subarray(off), cb };
7163
+ return;
7164
+ }
7165
+ const n = Math.min(chunk.length - off, this.credit, DATA_CHUNK);
7166
+ this.sendFrame(encodeFrame(FrameType.data, this.id, chunk.subarray(off, off + n)));
7167
+ this.credit -= n;
7168
+ off += n;
7169
+ }
7170
+ cb();
7171
+ }
7172
+ _write(chunk, _enc, cb) {
7173
+ if (this.remoteClosed) return cb();
7174
+ this.sendChunks(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk), cb);
7175
+ }
7176
+ _final(cb) {
7177
+ this.sendClose();
7178
+ cb();
7179
+ if (!this.remoteClosed) {
7180
+ this.remoteClosed = true;
7181
+ this.push(null);
7182
+ if (this.readableLength === 0) setImmediate(() => this.destroy());
7183
+ else this.once("end", () => this.destroy());
7184
+ }
7185
+ }
7186
+ _destroy(err, cb) {
7187
+ this.sendClose();
7188
+ const p = this.pending;
7189
+ this.pending = null;
7190
+ p?.cb(err ?? new Error("stream closed"));
7191
+ cb(err);
7192
+ }
7193
+ sendClose() {
7194
+ if (this.closeSent) return;
7195
+ this.closeSent = true;
7196
+ try {
7197
+ this.sendFrame(encodeFrame(FrameType.close, this.id));
7198
+ } catch {
7199
+ }
7200
+ }
7201
+ };
7202
+ var StreamTable = class {
7203
+ constructor(sendFrame, window = STREAM_WINDOW) {
7204
+ this.sendFrame = sendFrame;
7205
+ this.window = window;
7206
+ }
7207
+ sendFrame;
7208
+ window;
7209
+ streams = /* @__PURE__ */ new Map();
7210
+ get size() {
7211
+ return this.streams.size;
7212
+ }
7213
+ has(id) {
7214
+ return this.streams.has(id);
7215
+ }
7216
+ open(id) {
7217
+ if (this.streams.has(id)) throw new Error(`stream ${id} is open already`);
7218
+ const s = new LinkStream(id, this.sendFrame, this.window);
7219
+ this.streams.set(id, s);
7220
+ s.once("close", () => {
7221
+ if (this.streams.get(id) === s) this.streams.delete(id);
7222
+ });
7223
+ s.on("error", () => {
7224
+ });
7225
+ return s;
7226
+ }
7227
+ /** Route a data, window or close frame; false when no such stream is open. */
7228
+ receive(frame) {
7229
+ const s = this.streams.get(frame.stream);
7230
+ if (!s) return false;
7231
+ s.receive(frame);
7232
+ return true;
7233
+ }
7234
+ /** The link ended: every stream with it. */
7235
+ endAll(reason) {
7236
+ for (const s of [...this.streams.values()]) {
7237
+ s.remoteEnd();
7238
+ s.destroy(new Error(reason));
7239
+ }
7240
+ this.streams.clear();
7241
+ }
7242
+ };
7243
+
7244
+ // src/node/credentials.ts
7245
+ function readCredentials(file) {
7246
+ let raw;
7247
+ try {
7248
+ raw = fs10.readFileSync(file, "utf8");
7249
+ } catch (err) {
7250
+ if (err.code === "ENOENT") return null;
7251
+ throw err;
7252
+ }
7253
+ let c;
7254
+ try {
7255
+ c = JSON.parse(raw);
7256
+ } catch {
7257
+ throw new CliError(`${file} is not valid JSON; this machine cannot link until it is fixed or re-joined`);
7258
+ }
7259
+ const p = c.presence;
7260
+ if (c.version !== 1 || typeof c.box !== "string" || typeof c.machineId !== "string" || typeof c.name !== "string" || typeof c.token !== "string" || !NODE_TOKEN_SHAPE.test(c.token) || !p || typeof p.salt !== "string" || typeof p.key !== "string" || typeof p.iterations !== "number") {
7261
+ throw new CliError(`${file} is not a node's credentials; re-join this machine`);
7262
+ }
7263
+ return c;
7264
+ }
7265
+ function writeCredentials(file, c) {
7266
+ writePrivate2(file, `${JSON.stringify(c, null, 2)}
7267
+ `);
7268
+ }
7269
+
7270
+ // src/node/claude.ts
7271
+ import fs12 from "node:fs";
7272
+ import path11 from "node:path";
7273
+
7274
+ // ../../images/machine/agents.md
7275
+ var agents_default = "# You are working on {{machine}}, through agentbox\n\n{{machine}} is one of the owner's own computers, joined to their agentbox. You\nrun here as the owner, with their files, their tools and their network. The\nowner watches your terminal from their browser, on agentbox's machines page.\n\n- **To show the owner an app** you started (a dev server, say), tell them the\n port it listens on; they open it themselves.\n- **Never make anything public.** Do not use tunnels (ngrok, cloudflared,\n localtunnel), external hosting, claude.ai Artifacts or similar to show a web\n app unless the owner explicitly asks for that.\n- **Docker and ports are this machine's own.** Containers you start and ports\n you listen on are on {{machine}}, not in an agentbox sandbox; clean up what\n you start.\n";
7276
+
7277
+ // src/node/update.ts
7278
+ import { spawnSync as spawnSync2 } from "node:child_process";
7279
+ import crypto2 from "node:crypto";
7280
+ import fs11 from "node:fs";
7281
+ import path10 from "node:path";
7282
+
7283
+ // src/node/release-keys.ts
7284
+ var RELEASE_PUBLIC_KEYS = [
7285
+ // current
7286
+ `-----BEGIN PUBLIC KEY-----
7287
+ MCowBQYDK2VwAyEAQP6eEvWwcx5ptR56iFAJvDISpDYvy2oWOYNTH9KMUzw=
7288
+ -----END PUBLIC KEY-----`,
7289
+ // next (rotation)
7290
+ `-----BEGIN PUBLIC KEY-----
7291
+ MCowBQYDK2VwAyEANhmTV3Z/SVIUjCBjFrK+nZpl3Y2ck7vRex7l5CixSDU=
7292
+ -----END PUBLIC KEY-----`
7293
+ ];
7294
+
7295
+ // src/node/update-verify.ts
7296
+ import crypto from "node:crypto";
7297
+ var MACHINE_PLATFORMS = ["linux-x64", "linux-arm64", "win-x64"];
7298
+ var UpdateRefused = class extends Error {
7299
+ constructor(message) {
7300
+ super(message);
7301
+ this.name = "UpdateRefused";
7302
+ }
7303
+ };
7304
+ function machinePlatform(platform = process.platform, arch = process.arch) {
7305
+ const k = `${platform === "win32" ? "win" : platform}-${arch}`;
7306
+ return MACHINE_PLATFORMS.includes(k) ? k : null;
7307
+ }
7308
+ function parseVersion(v) {
7309
+ const m = /^v?(\d{1,9})\.(\d{1,9})\.(\d{1,9})$/.exec(v);
7310
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
7311
+ }
7312
+ function compareVersions(a, b) {
7313
+ return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
7314
+ }
7315
+ function verifyManifest(bytes, signature, keys) {
7316
+ if (keys.length === 0) {
7317
+ throw new UpdateRefused(
7318
+ "this build of agentbox has no release keys built in, so it cannot check that an update is genuine; it does not update itself (install a release build from https://github.com/jabezpauls/agentbox/releases)"
7319
+ );
7320
+ }
7321
+ const text = (Buffer.isBuffer(signature) ? signature.toString("utf8") : signature ?? "").trim();
7322
+ const sig = /^[A-Za-z0-9+/]{86}==$/.test(text) ? Buffer.from(text, "base64") : null;
7323
+ if (!sig || sig.length !== 64) throw new UpdateRefused("the release manifest is not signed (no valid signature beside it); not updating");
7324
+ const trusted = keys.some((pem) => {
7325
+ try {
7326
+ const key = crypto.createPublicKey(pem);
7327
+ return key.asymmetricKeyType === "ed25519" && crypto.verify(null, bytes, key, sig);
7328
+ } catch {
7329
+ return false;
7330
+ }
7331
+ });
7332
+ if (!trusted) throw new UpdateRefused("the release manifest is not signed by a release key this node trusts; not updating");
7333
+ return parseManifest(bytes);
7334
+ }
7335
+ var HEX64 = /^[0-9a-f]{64}$/;
7336
+ function asset(v, where, versioned) {
7337
+ const a = v;
7338
+ if (!a || typeof a !== "object") throw new UpdateRefused(`the release manifest has no ${where}`);
7339
+ if (typeof a.url !== "string" || !/^https:\/\//.test(a.url)) throw new UpdateRefused(`the release manifest's ${where} has no https URL`);
7340
+ if (typeof a.sha256 !== "string" || !HEX64.test(a.sha256)) throw new UpdateRefused(`the release manifest's ${where} has no SHA-256`);
7341
+ const out = { url: a.url, sha256: a.sha256 };
7342
+ if (typeof a.size === "number" && Number.isSafeInteger(a.size) && a.size > 0) out.size = a.size;
7343
+ if (!versioned) return out;
7344
+ if (typeof a.version !== "string" || !/^[0-9A-Za-z.+-]{1,64}$/.test(a.version)) throw new UpdateRefused(`the release manifest's ${where} has no version`);
7345
+ const entry = { ...out, version: a.version };
7346
+ if (a.files !== void 0) {
7347
+ const f = a.files;
7348
+ if (!f || typeof f !== "object" || !Object.values(f).every((d) => typeof d === "string" && HEX64.test(d))) {
7349
+ throw new UpdateRefused(`the release manifest's ${where} lists files without a SHA-256 each`);
7350
+ }
7351
+ entry.files = f;
7352
+ }
7353
+ return entry;
7354
+ }
7355
+ function parseManifest(bytes) {
7356
+ let raw;
7357
+ try {
7358
+ raw = JSON.parse(bytes.toString("utf8"));
7359
+ } catch {
7360
+ throw new UpdateRefused("the release manifest is not JSON");
7361
+ }
7362
+ if (!raw || typeof raw !== "object" || raw.schema !== 1) throw new UpdateRefused("the release manifest is of a kind this node does not know; update it by hand");
7363
+ if (typeof raw.version !== "string" || !parseVersion(raw.version)) throw new UpdateRefused("the release manifest has no version");
7364
+ const platforms = {};
7365
+ const given = raw.platforms ?? {};
7366
+ for (const p of MACHINE_PLATFORMS) {
7367
+ const e = given[p];
7368
+ if (!e) continue;
7369
+ const entry = {
7370
+ node: asset(e.node, `${p} node`, false),
7371
+ herdr: asset(e.herdr, `${p} herdr`, true),
7372
+ claude: asset(e.claude, `${p} Claude Code`, true)
7373
+ };
7374
+ if (e.nodeGui !== void 0) entry.nodeGui = asset(e.nodeGui, `${p} windowless node`, false);
7375
+ platforms[p] = entry;
7376
+ }
7377
+ return { schema: 1, version: raw.version, platforms };
7378
+ }
7379
+
7380
+ // src/node/update.ts
7381
+ var RELEASES_URL = "https://github.com/jabezpauls/agentbox/releases/latest/download";
7382
+ var MANIFEST_FILE = "machines-manifest.json";
7383
+ var SIGNATURE_FILE = "machines-manifest.json.sig";
7384
+ var MAX_MANIFEST = 256 * 1024;
7385
+ var MAX_SIGNATURE = 4 * 1024;
7386
+ var MAX_ASSET = 1024 * 1024 * 1024;
7387
+ function describeReport(r) {
7388
+ const tools = (t2) => [t2.herdr === "deferred" || t2.claude === "deferred" ? "herdr and Claude Code are updated once no agent is running" : "", t2.herdr === "updated" ? "herdr updated" : "", t2.claude === "updated" ? "Claude Code updated" : ""].filter(Boolean).join("; ");
7389
+ switch (r.state) {
7390
+ case "off":
7391
+ return "updates are off (policy.json)";
7392
+ case "current": {
7393
+ const t2 = tools(r.tools);
7394
+ return `up to date (agentbox ${r.version})${t2 ? `; ${t2}` : ""}`;
7395
+ }
7396
+ case "available":
7397
+ return `agentbox ${r.version} is available (this is ${r.from}); updates are set to notify, so run it by hand or set "updates": "auto"`;
7398
+ case "updated": {
7399
+ const t2 = tools(r.tools);
7400
+ return `updated agentbox ${r.from} \u2192 ${r.version}${r.restart ? ", restarting" : ""}${t2 ? `; ${t2}` : ""}`;
7401
+ }
7402
+ }
7403
+ }
7404
+ async function fetchLimited(fetcher, url, max, what) {
7405
+ const res = await fetcher(url, { redirect: "follow" });
7406
+ if (res.status === 404) return null;
7407
+ if (!res.ok || !res.body) throw new Error(`${what}: HTTP ${res.status} from ${url}`);
7408
+ const chunks = [];
7409
+ let size = 0;
7410
+ for await (const c of res.body) {
7411
+ size += c.length;
7412
+ if (size > max) throw new UpdateRefused(`${what} is larger than any real one; not updating`);
7413
+ chunks.push(Buffer.from(c));
7414
+ }
7415
+ return Buffer.concat(chunks);
7416
+ }
7417
+ async function fetchManifest(o = {}) {
7418
+ const keys = o.keys ?? RELEASE_PUBLIC_KEYS;
7419
+ if (keys.length === 0) verifyManifest(Buffer.alloc(0), null, keys);
7420
+ const fetcher = o.fetch ?? globalThis.fetch;
7421
+ const base = o.releasesUrl ?? RELEASES_URL;
7422
+ const bytes = await fetchLimited(fetcher, `${base}/${MANIFEST_FILE}`, MAX_MANIFEST, "the release manifest");
7423
+ if (!bytes) throw new UpdateRefused("the latest release has no machines manifest");
7424
+ const sig = await fetchLimited(fetcher, `${base}/${SIGNATURE_FILE}`, MAX_SIGNATURE, "the manifest's signature");
7425
+ return verifyManifest(bytes, sig, keys);
7426
+ }
7427
+ async function downloadVerified(fetcher, asset2, dest, what) {
7428
+ const dir = path10.dirname(dest);
7429
+ fs11.mkdirSync(dir, { recursive: true });
7430
+ const tmp = path10.join(dir, `.${path10.basename(dest)}.update-${crypto2.randomBytes(4).toString("hex")}`);
7431
+ const hash = crypto2.createHash("sha256");
7432
+ let fd = null;
7433
+ try {
7434
+ const res = await fetcher(asset2.url, { redirect: "follow" });
7435
+ if (!res.ok || !res.body) throw new Error(`${what}: HTTP ${res.status} from ${asset2.url}`);
7436
+ fd = fs11.openSync(tmp, "wx", 448);
7437
+ let size = 0;
7438
+ for await (const c of res.body) {
7439
+ size += c.length;
7440
+ if (size > (asset2.size ?? MAX_ASSET) || size > MAX_ASSET) throw new UpdateRefused(`${what} is larger than the manifest says; not installing it`);
7441
+ hash.update(c);
7442
+ fs11.writeSync(fd, c);
7443
+ }
7444
+ fs11.closeSync(fd);
7445
+ fd = null;
7446
+ const got = hash.digest("hex");
7447
+ if (got !== asset2.sha256) throw new UpdateRefused(`${what} does not match the signed manifest (SHA-256 ${got}, expected ${asset2.sha256}); not installing it`);
7448
+ fs11.chmodSync(tmp, 493);
7449
+ return tmp;
7450
+ } catch (err) {
7451
+ if (fd !== null) fs11.closeSync(fd);
7452
+ fs11.rmSync(tmp, { force: true });
7453
+ throw err;
7454
+ }
7455
+ }
7456
+ function replaceFile(tmp, dest, platform = process.platform) {
7457
+ try {
7458
+ if (platform === "win32" && fs11.existsSync(dest)) {
7459
+ const old = `${dest}.old`;
7460
+ fs11.rmSync(old, { force: true });
7461
+ fs11.renameSync(dest, old);
7462
+ }
7463
+ fs11.renameSync(tmp, dest);
7464
+ } catch (err) {
7465
+ fs11.rmSync(tmp, { force: true });
7466
+ throw err;
7467
+ }
7468
+ }
7469
+ function defaultCheckRuns(file, version) {
7470
+ const out = spawnSync2(file, ["--version"], { encoding: "utf8", timeout: 3e4, windowsHide: true });
7471
+ const said = (out.stdout ?? "").trim();
7472
+ if (out.status !== 0 || said !== `agentbox ${version}`) {
7473
+ throw new UpdateRefused(`the downloaded agentbox does not run here, or is not ${version} (it said ${JSON.stringify(said.slice(0, 100))}); not installing it`);
7474
+ }
7475
+ }
7476
+ async function updateTool(o, fetcher, platform, name, installed, asset2, idle) {
7477
+ if (!installed) return "none";
7478
+ if (installed.version === asset2.version) return "current";
7479
+ if (asset2.files) return "unsupported";
7480
+ if (!idle) return "deferred";
7481
+ const tmp = await downloadVerified(fetcher, asset2, installed.path, `${name} ${asset2.version} for ${platform}`);
7482
+ replaceFile(tmp, installed.path, o.platform ?? process.platform);
7483
+ if (installed.versionFile) fs11.writeFileSync(installed.versionFile, `${asset2.version}
7484
+ `, { mode: 384 });
7485
+ return "updated";
7486
+ }
7487
+ async function checkForUpdate(o) {
7488
+ if (o.policy === "off") return { state: "off" };
7489
+ const running = parseVersion(o.running);
7490
+ if (!running) throw new UpdateRefused(`this node's version (${o.running}) is not a release's, so it cannot tell what is newer; install a release build`);
7491
+ const platform = machinePlatform(o.platform ?? process.platform, o.arch ?? process.arch);
7492
+ if (!platform) throw new UpdateRefused(`no agentbox release is built for ${o.platform ?? process.platform}-${o.arch ?? process.arch}`);
7493
+ const fetcher = o.fetch ?? globalThis.fetch;
7494
+ const manifest = await fetchManifest(o);
7495
+ const order = compareVersions(parseVersion(manifest.version), running);
7496
+ if (order < 0) throw new UpdateRefused(`the latest release (${manifest.version}) is older than this node (${o.running}); not going back`);
7497
+ const assets = manifest.platforms[platform];
7498
+ if (!assets) throw new UpdateRefused(`the release ${manifest.version} has nothing for ${platform}`);
7499
+ const newer = order > 0;
7500
+ if (newer && o.policy === "notify") return { state: "available", version: manifest.version, from: o.running };
7501
+ const tools = async () => {
7502
+ const needed = [o.herdr && o.herdr.version !== assets.herdr.version, o.claude && o.claude.version !== assets.claude.version].some(Boolean);
7503
+ const idle = needed && o.policy === "auto" && o.idle ? await o.idle() : false;
7504
+ return {
7505
+ herdr: await updateTool(o, fetcher, platform, "herdr", o.herdr, assets.herdr, idle),
7506
+ claude: await updateTool(o, fetcher, platform, "Claude Code", o.claude, assets.claude, idle)
7507
+ };
7508
+ };
7509
+ if (!newer) return { state: "current", version: o.running, tools: o.policy === "auto" ? await tools() : { herdr: "none", claude: "none" } };
7510
+ if (o.nodePath === null) return { state: "available", version: manifest.version, from: o.running };
7511
+ const node = await downloadVerified(fetcher, assets.node, o.nodePath, `agentbox ${manifest.version} for ${platform}`);
7512
+ let gui = null;
7513
+ try {
7514
+ (o.checkRuns ?? defaultCheckRuns)(node, manifest.version);
7515
+ if (o.nodeGuiPath && assets.nodeGui) gui = await downloadVerified(fetcher, assets.nodeGui, o.nodeGuiPath, `agentbox-node.exe ${manifest.version}`);
7516
+ } catch (err) {
7517
+ fs11.rmSync(node, { force: true });
7518
+ throw err;
7519
+ }
7520
+ replaceFile(node, o.nodePath, o.platform ?? process.platform);
7521
+ if (gui && o.nodeGuiPath) replaceFile(gui, o.nodeGuiPath, o.platform ?? process.platform);
7522
+ return { state: "updated", version: manifest.version, from: o.running, restart: true, tools: await tools() };
7523
+ }
7524
+
7525
+ // src/node/claude.ts
7526
+ var CLAUDE_VERSION = "2.1.311";
7527
+ var CLAUDE_PINS = {
7528
+ "linux-x64": { path: "linux-x64/claude", sha256: "86dc1fcc69498159eaf15f446613e847010f9036bfbbdffd4c49350be8489ada", size: 293776752 },
7529
+ "linux-arm64": { path: "linux-arm64/claude", sha256: "2e2496c9a8065df93acb0fca2d341f59a6b8777649b2e025ab8c9bcd5eef61f3", size: 293658808 },
7530
+ "win32-x64": { path: "win32-x64/claude.exe", sha256: "966f4b2ce962298f5dee6a942e8c47c8f97392e69e728e622319a98b259c0680", size: 298870432 }
7531
+ };
7532
+ var CLAUDE_RELEASES = "https://downloads.claude.ai/claude-code-releases";
7533
+ function claudePaths(p, platform = process.platform) {
7534
+ const exe = platform === "win32" ? "claude.exe" : "claude";
7535
+ return {
7536
+ bin: path11.join(p.bin, exe),
7537
+ versionFile: path11.join(p.bin, "claude.version"),
7538
+ config: path11.join(p.dir, "claude"),
7539
+ shims: path11.join(p.dir, "shims"),
7540
+ shim: path11.join(p.dir, "shims", "claude"),
7541
+ agents: path11.join(p.dir, "agents.md"),
7542
+ token: path11.join(p.secrets, "claude-token")
7543
+ };
7544
+ }
7545
+ function writeIfAbsent(file, contents) {
7546
+ if (fs12.existsSync(file)) return false;
7547
+ writePrivate2(file, contents);
7548
+ return true;
7549
+ }
7550
+ function seedClaudeConfig(c, home2) {
7551
+ ensurePrivateDir(c.config);
7552
+ writeIfAbsent(path11.join(c.config, ".claude.json"), `${JSON.stringify({ hasCompletedOnboarding: true, projects: { [home2]: { hasTrustDialogAccepted: true } } }, null, 2)}
7553
+ `);
7554
+ writeIfAbsent(path11.join(c.config, "settings.json"), `${JSON.stringify({ disableDeepLinkRegistration: "disable" }, null, 2)}
7555
+ `);
7556
+ }
7557
+ function agentsText(machine) {
7558
+ return agents_default.replaceAll("{{machine}}", machine.replace(/[^\w.-]/g, ""));
7559
+ }
7560
+ function shQuote(s) {
7561
+ return `'${s.replace(/'/g, "'\\''")}'`;
7562
+ }
7563
+ var PLAIN = ["mcp", "config", "doctor", "update", "install", "setup-token", "migrate-installer", "plugin", "-v", "--version", "-h", "--help"];
7564
+ function shimText(c) {
7565
+ return `#!/bin/sh
7566
+ # agentbox: Claude Code on this machine. Written by the node; rewritten at
7567
+ # every start. Reads the shared sign-in (when this machine was given it) at
7568
+ # each launch, for this process only.
7569
+ CLAUDE_CONFIG_DIR=${shQuote(c.config)}
7570
+ DISABLE_UPDATES=1
7571
+ export CLAUDE_CONFIG_DIR DISABLE_UPDATES
7572
+ if [ -f ${shQuote(c.token)} ]; then
7573
+ CLAUDE_CODE_OAUTH_TOKEN="$(cat ${shQuote(c.token)})"
7574
+ export CLAUDE_CODE_OAUTH_TOKEN
7575
+ fi
7576
+ if [ ! -x ${shQuote(c.bin)} ]; then
7577
+ echo "agentbox: Claude Code is not installed on this machine yet (it is installed by \`agentbox node join\`)." >&2
7578
+ exit 127
7579
+ fi
7580
+ case "\${1:-}" in
7581
+ ${PLAIN.join("|")}) exec ${shQuote(c.bin)} "$@" ;;
7582
+ esac
7583
+ exec ${shQuote(c.bin)} --append-system-prompt-file ${shQuote(c.agents)} "$@"
7584
+ `;
7585
+ }
7586
+ function prepareClaude(c, o) {
7587
+ seedClaudeConfig(c, o.home);
7588
+ writePrivate2(c.agents, agentsText(o.machine));
7589
+ ensurePrivateDir(c.shims);
7590
+ writePrivate2(c.shim, shimText(c), 448);
7591
+ }
7592
+ function holdToken(c, token) {
7593
+ if (token === null) {
7594
+ fs12.rmSync(c.token, { force: true });
7595
+ return;
7596
+ }
7597
+ if (!/^[A-Za-z0-9_\-.~+/=]{20,512}$/.test(token)) return;
7598
+ writePrivate2(c.token, token);
7599
+ }
7600
+ function installedClaude(c) {
7601
+ if (!fs12.existsSync(c.bin)) return null;
7602
+ try {
7603
+ const v = fs12.readFileSync(c.versionFile, "utf8").trim();
7604
+ return parseVersion(v) ? v : null;
7605
+ } catch {
7606
+ return null;
7607
+ }
7608
+ }
7609
+ async function installClaude(c, deps) {
7610
+ const have = installedClaude(c);
7611
+ const want = parseVersion(CLAUDE_VERSION);
7612
+ if (have && compareVersions(parseVersion(have), want) >= 0) return have;
7613
+ const key = `${deps.platform}-${deps.arch}`;
7614
+ const pin = CLAUDE_PINS[key];
7615
+ if (!pin) throw new CliError(`Claude Code ${CLAUDE_VERSION} has no build for ${deps.platform}/${deps.arch} that this node knows`);
7616
+ deps.log?.(`Installing Claude Code ${CLAUDE_VERSION} (about ${Math.round(pin.size / 2 ** 20)} MiB)\u2026
7617
+ `);
7618
+ const tmp = await downloadVerified(
7619
+ deps.fetch ?? globalThis.fetch,
7620
+ { url: `${CLAUDE_RELEASES}/${CLAUDE_VERSION}/${pin.path}`, sha256: pin.sha256, size: pin.size },
7621
+ c.bin,
7622
+ `Claude Code ${CLAUDE_VERSION}`
7623
+ );
7624
+ replaceFile(tmp, c.bin);
7625
+ writePrivate2(c.versionFile, `${CLAUDE_VERSION}
7626
+ `);
7627
+ return CLAUDE_VERSION;
7628
+ }
7629
+
7630
+ // src/node/install.ts
7631
+ import { createHash as createHash4 } from "node:crypto";
7632
+ import fs13 from "node:fs";
7633
+ import path12 from "node:path";
7634
+ var HERDR_VERSION = "0.9.3";
7635
+ var HERDR_PINS = {
7636
+ "linux-x64": { asset: "herdr-linux-x86_64", sha256: "18a8dc65f1c2fa485884344356dea1cfd911c6f06cf46fa78e193f4087f4dba7" },
7637
+ "linux-arm64": { asset: "herdr-linux-aarch64", sha256: "4de7aa3e25678812e92960de64f7c2aaa1bca1f0f80a3c5e559837e231e1f5c0" }
7638
+ };
7639
+ function sha256(bytes) {
7640
+ return createHash4("sha256").update(bytes).digest("hex");
7641
+ }
7642
+ async function fetchBytes(url) {
7643
+ let res;
7644
+ try {
7645
+ res = await fetch(url, { redirect: "follow" });
7646
+ } catch (err) {
7647
+ throw new CliError(`could not download ${url}: ${err.message}`);
7648
+ }
7649
+ if (!res.ok) throw new CliError(`could not download ${url}: HTTP ${res.status}`);
7650
+ return new Uint8Array(await res.arrayBuffer());
7651
+ }
7652
+ async function installHerdr(binDir, deps) {
7653
+ const pin = HERDR_PINS[`${deps.platform}-${deps.arch}`];
7654
+ if (!pin) throw new CliError(`herdr ${HERDR_VERSION} has no build for ${deps.platform}/${deps.arch} that this node knows`);
7655
+ const target = path12.join(binDir, "herdr");
7656
+ try {
7657
+ if (sha256(fs13.readFileSync(target)) === pin.sha256) return target;
7658
+ } catch {
7659
+ }
7660
+ const url = `https://github.com/herdrdev/herdr/releases/download/v${HERDR_VERSION}/${pin.asset}`;
7661
+ deps.log?.(`Installing herdr ${HERDR_VERSION}\u2026
7662
+ `);
7663
+ const bytes = deps.from ? new Uint8Array(fs13.readFileSync(deps.from)) : await (deps.download ?? fetchBytes)(url);
7664
+ const got = sha256(bytes);
7665
+ if (got !== pin.sha256) throw new CliError(`herdr ${HERDR_VERSION} failed verification (SHA-256 ${got}, expected ${pin.sha256}); nothing was installed`);
7666
+ writePrivate2(target, bytes, 448);
7667
+ return target;
7668
+ }
7669
+
7670
+ // src/node/join.ts
7671
+ import fs15 from "node:fs";
7672
+
7673
+ // src/node/policy.ts
7674
+ import fs14 from "node:fs";
7675
+ import path13 from "node:path";
7676
+ var DENY_ALL = { terminals: false, files: [], ports: false, share: false, updates: "auto", presenceHours: 12, paused: false };
7677
+ var KEYS = /* @__PURE__ */ new Set(["terminals", "files", "ports", "share", "updates", "presenceHours", "paused"]);
7678
+ function broken(problem) {
7679
+ return { policy: { ...DENY_ALL, files: [] }, problem };
7680
+ }
7681
+ function expand(p, home2) {
7682
+ if (p === "~") return home2;
7683
+ if (p.startsWith("~/")) return path13.join(home2, p.slice(2));
7684
+ return p;
7685
+ }
7686
+ function parsePolicy(text, home2) {
7687
+ let raw;
7688
+ try {
7689
+ raw = JSON.parse(text);
7690
+ } catch {
7691
+ return broken("policy.json is not valid JSON");
7692
+ }
7693
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return broken("policy.json is not an object");
7694
+ const o = raw;
7695
+ for (const k of Object.keys(o)) if (!KEYS.has(k)) return broken(`policy.json has "${k}", which this node does not know`);
7696
+ const policy = { ...DENY_ALL, files: [] };
7697
+ for (const k of ["terminals", "ports", "share", "paused"]) {
7698
+ if (o[k] === void 0) continue;
7699
+ if (typeof o[k] !== "boolean") return broken(`policy.json: "${k}" must be true or false`);
7700
+ policy[k] = o[k];
7701
+ }
7702
+ if (o.updates !== void 0) {
7703
+ if (o.updates !== "auto" && o.updates !== "notify" && o.updates !== "off") return broken('policy.json: "updates" must be "auto", "notify" or "off"');
7704
+ policy.updates = o.updates;
7705
+ }
7706
+ if (o.presenceHours !== void 0) {
7707
+ const h = o.presenceHours;
7708
+ if (typeof h !== "number" || !Number.isInteger(h) || h < 1 || h > 24) return broken('policy.json: "presenceHours" must be a whole number from 1 to 24');
7709
+ policy.presenceHours = h;
7710
+ }
7711
+ if (o.files !== void 0) {
7712
+ if (!Array.isArray(o.files)) return broken('policy.json: "files" must be a list');
7713
+ for (const f of o.files) {
7714
+ const e = f ?? {};
7715
+ if (typeof e.path !== "string" || e.path === "") return broken('policy.json: every "files" entry needs a "path"');
7716
+ if (e.mode !== "read" && e.mode !== "write") return broken(`policy.json: "${e.path}" needs a "mode" of "read" or "write"`);
7717
+ const full = expand(e.path, home2);
7718
+ if (!path13.isAbsolute(full)) return broken(`policy.json: "${e.path}" is not an absolute folder (or ~/\u2026)`);
7719
+ policy.files.push({ path: path13.normalize(full), mode: e.mode });
7720
+ }
7721
+ }
7722
+ return { policy, problem: null };
7723
+ }
7724
+ function loadPolicy(file, home2) {
7725
+ let text;
7726
+ try {
7727
+ text = fs14.readFileSync(file, "utf8");
7728
+ } catch (err) {
7729
+ if (err.code === "ENOENT") return broken("there is no policy.json");
7730
+ return broken(`policy.json cannot be read (${err.code ?? "error"})`);
7731
+ }
7732
+ return parsePolicy(text, home2);
7733
+ }
7734
+ function summarize(l) {
7735
+ return { ...l.policy, files: l.policy.files.map((f) => ({ ...f })), problem: l.problem };
7736
+ }
7737
+ function watchPolicy(file, home2, onChange, intervalMs = 1e3) {
7738
+ let last = JSON.stringify(loadPolicy(file, home2));
7739
+ const check = () => {
7740
+ const now = loadPolicy(file, home2);
7741
+ const s = JSON.stringify(now);
7742
+ if (s === last) return;
7743
+ last = s;
7744
+ onChange(now);
7745
+ };
7746
+ const timer = setInterval(check, intervalMs);
7747
+ timer.unref();
7748
+ return () => clearInterval(timer);
7749
+ }
7750
+ function writeInitialPolicy(file, folders) {
7751
+ if (fs14.existsSync(file)) return false;
7752
+ const policy = { ...DENY_ALL, terminals: true, files: folders };
7753
+ writePrivate2(file, `${JSON.stringify(policy, null, 2)}
7754
+ `);
7755
+ return true;
7756
+ }
7757
+ function setPaused(file, paused) {
7758
+ let o;
7759
+ try {
7760
+ o = JSON.parse(fs14.readFileSync(file, "utf8"));
7761
+ } catch (err) {
7762
+ if (err.code === "ENOENT") o = { ...DENY_ALL };
7763
+ else throw new CliError(`${file} is not valid JSON; fix it first (everything is denied meanwhile)`);
7764
+ }
7765
+ if (typeof o !== "object" || o === null || Array.isArray(o)) throw new CliError(`${file} is not a policy; fix it first`);
7766
+ o.paused = paused;
7767
+ writePrivate2(file, `${JSON.stringify(o, null, 2)}
7768
+ `);
7769
+ }
7770
+
7771
+ // src/node/join.ts
7772
+ var MIN_FREE_BYTES = 512 * 1024 * 1024;
7773
+ var SUPPORTED_PLATFORMS = /* @__PURE__ */ new Set(["linux"]);
7774
+ async function joinMachine(o, s) {
7775
+ const existing = readCredentials(s.paths.credentials);
7776
+ if (existing && existing.box !== o.box && existing.revokedAt === void 0) {
7777
+ throw new CliError(
7778
+ `this machine is joined to ${existing.box} already; a machine joins one box only (run \`agentbox node leave\` first)`
7779
+ );
7780
+ }
7781
+ const rejoin = existing && existing.box === o.box && existing.revokedAt === void 0 ? existing : null;
7782
+ if (!SUPPORTED_PLATFORMS.has(s.platform)) throw new CliError(`machines on ${s.platform} are not supported yet (Linux only, for now)`);
7783
+ try {
7784
+ ensurePrivateDir(s.paths.dir);
7785
+ fs15.accessSync(s.paths.dir, fs15.constants.W_OK);
7786
+ } catch (err) {
7787
+ throw new CliError(`the node folder ${s.paths.dir} is not writable: ${err.message}`);
7788
+ }
7789
+ const free = s.freeBytes(s.paths.dir);
7790
+ if (free !== null && free < MIN_FREE_BYTES) {
7791
+ throw new CliError(`${s.paths.dir} has ${Math.floor(free / 2 ** 20)} MiB free; the node needs ${MIN_FREE_BYTES / 2 ** 20} MiB`);
7792
+ }
7793
+ await s.install();
7794
+ const presence = await s.enrol(await s.passphrase());
7795
+ let started;
7796
+ try {
7797
+ started = await s.client.json("POST", "/_gate/node/join/start", {
7798
+ auth: false,
7799
+ body: {
7800
+ json: {
7801
+ name: o.name,
7802
+ platform: s.platform,
7803
+ arch: s.arch,
7804
+ hostname: s.hostname,
7805
+ ...rejoin ? { rejoin: { machineId: rejoin.machineId, token: rejoin.token } } : {}
7806
+ }
7807
+ },
7808
+ what: "asking the box to join"
7809
+ });
7810
+ } catch (err) {
7811
+ if (err.status === 404) throw new CliError(`${o.box} does not have machines turned on (or is not an agentbox)`, EXIT.NOT_FOUND);
7812
+ throw err;
7813
+ }
7814
+ s.err(`
7815
+ ${rejoin ? "Re-joining" : "Joining"} as "${started.name}". On the box's machines Admin page${started.approveAt ? ` (${started.approveAt})` : ""},
7816
+ `);
7817
+ s.err(`check the name, platform and address, and type this code:
7818
+
7819
+ ${started.userCode}
7820
+
7821
+ `);
7822
+ s.err(`Waiting for approval (the code lasts ${Math.round(started.expiresIn / 60)} minutes)\u2026
7823
+ `);
7824
+ const deadline = s.now() + started.expiresIn * 1e3;
7825
+ let got = null;
7826
+ while (got === null) {
7827
+ await s.sleep(Math.max(1, started.interval) * 1e3);
7828
+ if (s.now() > deadline) throw new CliError("the code expired before it was approved; run `agentbox node join` again");
7829
+ let p;
7830
+ try {
7831
+ p = await s.client.json("POST", "/_gate/node/join/poll", { auth: false, body: { json: { joinCode: started.joinCode } }, what: "waiting for approval" });
7832
+ } catch (err) {
7833
+ if (err instanceof CliError && (err.exitCode === EXIT.UNREACHABLE || err.status === 429 || (err.status ?? 0) >= 500)) continue;
7834
+ throw err;
7835
+ }
7836
+ if (p.status === "pending") continue;
7837
+ if (p.status === "denied") throw new CliError("the join was denied on the box");
7838
+ if (p.status === "expired") throw new CliError("the code expired, or was already used; run `agentbox node join` again");
7839
+ got = p;
7840
+ }
7841
+ const credentials = {
7842
+ version: 1,
7843
+ box: o.box,
7844
+ machineId: got.machineId,
7845
+ name: got.name,
7846
+ token: got.token,
7847
+ presence,
7848
+ joinedAt: s.now()
7849
+ };
7850
+ writeCredentials(s.paths.credentials, credentials);
7851
+ s.err(`Joined as "${got.name}" (machine ${got.machineId}).
7852
+ `);
7853
+ if (fs15.existsSync(s.paths.policy)) {
7854
+ s.err(`Keeping the existing ${s.paths.policy}:
7855
+ `);
7856
+ } else {
7857
+ const folders = o.folders ?? await s.askFolders();
7858
+ writeInitialPolicy(s.paths.policy, folders);
7859
+ s.err(`Wrote ${s.paths.policy}:
7860
+ `);
7861
+ }
7862
+ s.printPolicy();
7863
+ if (s.service) {
7864
+ await s.service();
7865
+ s.err("Waiting for the machine to show online\u2026\n");
7866
+ if (await s.waitOnline()) s.err("Online.\n");
7867
+ else s.err("It has not come online yet; see `agentbox node status` and `agentbox node logs`.\n");
7868
+ } else {
7869
+ s.err("Start it with `agentbox node run` (no service was registered).\n");
7870
+ }
7871
+ return credentials;
7872
+ }
7873
+
7874
+ // src/node/link.ts
7875
+ import { randomBytes as randomBytes5 } from "node:crypto";
7876
+ import { EventEmitter } from "node:events";
7877
+ function backoffMs(n, min, max, random = Math.random) {
7878
+ const base = Math.min(max, min * 2 ** Math.max(0, n - 1));
7879
+ return Math.round(base / 2 + base / 2 * random());
7880
+ }
7881
+ var NodeLink = class extends EventEmitter {
7882
+ ws = null;
7883
+ stateNow = "stopped";
7884
+ running = false;
7885
+ paused = false;
7886
+ failures = 0;
7887
+ failingSince = null;
7888
+ retryTimer = null;
7889
+ wakeTimer = null;
7890
+ lastTick = 0;
7891
+ nonce = "";
7892
+ nextDelay = null;
7893
+ o;
7894
+ constructor(opts) {
7895
+ super();
7896
+ this.o = {
7897
+ now: Date.now,
7898
+ random: Math.random,
7899
+ pingMs: PING_MS,
7900
+ minBackoffMs: 1e3,
7901
+ maxBackoffMs: 6e4,
7902
+ slowAfterMs: 60 * 6e4,
7903
+ slowRetryMs: 60 * 6e4,
7904
+ duplicateBackoffMs: DUPLICATE_BACKOFF_MS,
7905
+ wakeCheckMs: 5e3,
7906
+ wakeJumpMs: 15e3,
7907
+ ...opts
7908
+ };
7909
+ }
7910
+ get state() {
7911
+ return this.stateNow;
7912
+ }
7913
+ set(state) {
7914
+ if (state === this.stateNow) return;
7915
+ this.stateNow = state;
7916
+ this.emit("state", state);
7917
+ }
7918
+ log(line) {
7919
+ this.emit("log", line);
7920
+ }
7921
+ start() {
7922
+ if (this.running) return;
7923
+ this.running = true;
7924
+ this.lastTick = this.o.now();
7925
+ this.wakeTimer = setInterval(() => this.tick(), this.o.wakeCheckMs);
7926
+ this.wakeTimer.unref();
7927
+ if (!this.paused) this.connect();
7928
+ else this.set("paused");
7929
+ }
7930
+ stop(state = "stopped") {
7931
+ this.running = false;
7932
+ if (this.wakeTimer) clearInterval(this.wakeTimer);
7933
+ this.wakeTimer = null;
7934
+ this.clearRetry();
7935
+ const ws = this.ws;
7936
+ this.ws = null;
7937
+ ws?.close(1e3, "stopping");
7938
+ this.set(state);
7939
+ }
7940
+ /** `agentbox node pause`: say so, close, and stay closed until resume. */
7941
+ pause() {
7942
+ this.paused = true;
7943
+ this.clearRetry();
7944
+ const ws = this.ws;
7945
+ this.ws = null;
7946
+ if (ws && ws.readyState === import_websocket.default.OPEN) {
7947
+ ws.send(encodeControl({ type: "pause" }));
7948
+ ws.close(1e3, "paused");
7949
+ } else {
7950
+ ws?.terminate();
7951
+ }
7952
+ if (this.running) this.set("paused");
7953
+ }
7954
+ resume() {
7955
+ if (!this.paused) return;
7956
+ this.paused = false;
7957
+ this.failures = 0;
7958
+ this.failingSince = null;
7959
+ if (this.running && !this.ws) this.connect();
7960
+ }
7961
+ get isPaused() {
7962
+ return this.paused;
7963
+ }
7964
+ sendPolicy(policy) {
7965
+ if (this.stateNow === "online" && this.ws?.readyState === import_websocket.default.OPEN) this.ws.send(encodeControl({ type: "policy", policy }));
7966
+ }
7967
+ /** Tell the box this machine is leaving; resolves whether it was told. */
7968
+ leave(timeoutMs = 5e3) {
7969
+ const ws = this.ws;
7970
+ if (!ws || this.stateNow !== "online" || ws.readyState !== import_websocket.default.OPEN) return Promise.resolve(false);
7971
+ return new Promise((resolve) => {
7972
+ const timer = setTimeout(() => resolve(false), timeoutMs);
7973
+ ws.once("close", (code2) => {
7974
+ clearTimeout(timer);
7975
+ resolve(code2 === 4005);
7976
+ });
7977
+ ws.send(encodeControl({ type: "leave" }));
7978
+ });
7979
+ }
7980
+ clearRetry() {
7981
+ if (this.retryTimer) clearTimeout(this.retryTimer);
7982
+ this.retryTimer = null;
7983
+ }
7984
+ /** The wall clock jumped: the machine slept. Whatever was waiting, go now. */
7985
+ tick() {
7986
+ const t2 = this.o.now();
7987
+ const late = t2 - this.lastTick - this.o.wakeCheckMs;
7988
+ this.lastTick = t2;
7989
+ if (late < this.o.wakeJumpMs || !this.running || this.paused) return;
7990
+ if (this.stateNow === "revoked" || this.stateNow === "left") return;
7991
+ this.log(`woke after ${Math.round(late / 1e3)} s; reconnecting now`);
7992
+ this.failures = 0;
7993
+ if (this.ws) {
7994
+ const ws = this.ws;
7995
+ this.ws = null;
7996
+ ws.terminate();
7997
+ }
7998
+ this.clearRetry();
7999
+ this.connect();
8000
+ }
8001
+ retry() {
8002
+ if (!this.running || this.paused) return;
8003
+ const t2 = this.o.now();
8004
+ let delay;
8005
+ if (this.nextDelay !== null) {
8006
+ delay = this.nextDelay;
8007
+ this.nextDelay = null;
8008
+ } else {
8009
+ this.failures += 1;
8010
+ this.failingSince ??= t2;
8011
+ delay = t2 - this.failingSince >= this.o.slowAfterMs ? this.o.slowRetryMs : backoffMs(this.failures, this.o.minBackoffMs, this.o.maxBackoffMs, this.o.random);
8012
+ }
8013
+ this.clearRetry();
8014
+ this.retryTimer = setTimeout(() => {
8015
+ this.retryTimer = null;
8016
+ this.connect();
8017
+ }, delay);
8018
+ this.retryTimer.unref();
8019
+ }
8020
+ connect() {
8021
+ if (!this.running || this.paused || this.ws) return;
8022
+ this.set("connecting");
8023
+ const url = `${this.o.box.replace(/^http/, "ws")}${LINK_PATH}`;
8024
+ const ws = new import_websocket.default(url, { headers: { authorization: `Bearer ${this.o.token}` }, perMessageDeflate: false, handshakeTimeout: 2e4, followRedirects: false });
8025
+ this.ws = ws;
8026
+ let welcomed = false;
8027
+ let stopKeepAlive = () => {
8028
+ };
8029
+ const streams = new StreamTable((f) => {
8030
+ if (ws.readyState === import_websocket.default.OPEN) ws.send(f);
8031
+ }, STREAM_WINDOW);
8032
+ ws.on("unexpected-response", (_req, res) => {
8033
+ this.log(`the box answered the link with HTTP ${res.statusCode}; retrying`);
8034
+ res.resume();
8035
+ ws.terminate();
8036
+ });
8037
+ ws.on("error", (err) => this.log(`link error: ${err.message}`));
8038
+ ws.on("open", () => {
8039
+ stopKeepAlive = keepAlive(
8040
+ ws,
8041
+ () => {
8042
+ this.log("the box stopped answering pings; reconnecting");
8043
+ ws.terminate();
8044
+ },
8045
+ this.o.pingMs
8046
+ );
8047
+ this.nonce = randomBytes5(32).toString("base64url");
8048
+ void this.o.describe().then(
8049
+ (d) => {
8050
+ if (ws.readyState !== import_websocket.default.OPEN) return;
8051
+ const hello = { type: "hello", protocol: PROTOCOL_VERSION, version: this.o.version, nonce: this.nonce, ...d };
8052
+ ws.send(encodeControl(hello));
8053
+ },
8054
+ (err) => {
8055
+ this.log(`could not describe this machine: ${err.message}`);
8056
+ ws.terminate();
8057
+ }
8058
+ );
8059
+ });
8060
+ ws.on("message", (data, isBinary) => {
8061
+ if (!isBinary) return;
8062
+ let frame;
8063
+ try {
8064
+ frame = decodeFrame(data);
8065
+ } catch {
8066
+ this.log("the box sent a malformed frame; reconnecting");
8067
+ ws.terminate();
8068
+ return;
8069
+ }
8070
+ if (frame.type === FrameType.control) {
8071
+ let m;
8072
+ try {
8073
+ m = decodeControl(frame.payload);
8074
+ } catch {
8075
+ return;
8076
+ }
8077
+ if (m.type === "ok") {
8078
+ welcomed = true;
8079
+ this.failures = 0;
8080
+ this.failingSince = null;
8081
+ this.set("online");
8082
+ this.emit("welcome", { machineId: m.machineId, name: m.name });
8083
+ } else if (m.type === "revoked") {
8084
+ if (verifyRevoke(this.o.token, this.o.machineId, this.nonce, m.mac)) {
8085
+ this.log("the box revoked this machine");
8086
+ this.emit("revoked");
8087
+ this.stop("revoked");
8088
+ } else {
8089
+ this.log("ignored a revoked message that was not signed for this machine and link");
8090
+ ws.terminate();
8091
+ }
8092
+ } else if (m.type === "duplicate") {
8093
+ this.log("the box refused this link: this machine is already linked from elsewhere; waiting 5 minutes");
8094
+ this.set("duplicate");
8095
+ this.nextDelay = this.o.duplicateBackoffMs;
8096
+ } else if (m.type === "upgrade") {
8097
+ this.log(`the box needs a newer node (protocol ${String(m.minimum)}); waiting an hour`);
8098
+ this.set("upgrade");
8099
+ this.nextDelay = this.o.slowRetryMs;
8100
+ this.emit("upgrade", { minimum: m.minimum });
8101
+ } else if (welcomed) {
8102
+ this.emit("control", m);
8103
+ }
8104
+ return;
8105
+ }
8106
+ if (frame.type === FrameType.open) {
8107
+ if (!welcomed || this.listenerCount("stream") === 0 || streams.has(frame.stream)) {
8108
+ ws.send(encodeFrame(FrameType.close, frame.stream));
8109
+ return;
8110
+ }
8111
+ this.emit("stream", streams.open(frame.stream), decodeOpen(frame.payload));
8112
+ return;
8113
+ }
8114
+ if (!streams.receive(frame) && frame.type !== FrameType.close) ws.send(encodeFrame(FrameType.close, frame.stream));
8115
+ });
8116
+ ws.on("close", (code2) => {
8117
+ stopKeepAlive();
8118
+ streams.endAll("the link closed");
8119
+ if (this.ws !== ws) return;
8120
+ this.ws = null;
8121
+ if (!this.running || this.stateNow === "revoked") return;
8122
+ if (code2 === 4005) {
8123
+ this.stop("left");
8124
+ return;
8125
+ }
8126
+ if (welcomed) {
8127
+ this.failures = 0;
8128
+ this.failingSince = null;
8129
+ }
8130
+ if (this.stateNow !== "duplicate" && this.stateNow !== "upgrade") this.set("offline");
8131
+ this.retry();
8132
+ });
8133
+ }
8134
+ };
8135
+
8136
+ // src/node/presence.ts
8137
+ import { createHmac as createHmac2, pbkdf2, randomBytes as randomBytes6, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
8138
+ var PRESENCE_ITERATIONS = 6e5;
8139
+ var MIN_PASSPHRASE_LENGTH = 12;
8140
+ function derive(passphrase2, salt, iterations = PRESENCE_ITERATIONS) {
8141
+ return new Promise((resolve, reject) => pbkdf2(passphrase2, salt, iterations, 32, "sha256", (err, key) => err ? reject(err) : resolve(key)));
8142
+ }
8143
+ async function enrol(passphrase2, now, iterations = PRESENCE_ITERATIONS) {
8144
+ const salt = randomBytes6(16);
8145
+ const key = await derive(passphrase2, salt, iterations);
8146
+ return { algorithm: "PBKDF2-SHA256", iterations, salt: salt.toString("base64url"), key: key.toString("base64url"), createdAt: now };
8147
+ }
8148
+ async function askPassphrase(prompt, err) {
8149
+ err("The machines passphrase (the one set on the box's host with `agentbox machines enable`).\n");
8150
+ err("This machine keeps only a key derived from it; the box never sees it from here.\n");
8151
+ const pass = await prompt.secret("Machines passphrase: ");
8152
+ if (pass.length < MIN_PASSPHRASE_LENGTH) throw new CliError(`the machines passphrase has at least ${MIN_PASSPHRASE_LENGTH} characters`);
8153
+ const again = await prompt.secret("Again: ");
8154
+ if (again !== pass) throw new CliError("the passphrases did not match");
8155
+ return pass;
8156
+ }
8157
+ var CHALLENGE_MS = 6e4;
8158
+ var GRANT_IDLE_MS = 30 * 6e4;
8159
+ var LOCK_FAILURES = 5;
8160
+ var LOCK_WINDOW_MS = 10 * 6e4;
8161
+ var LOCK_MS = 15 * 6e4;
8162
+ var MAX_CHALLENGES = 64;
8163
+ var MAX_GRANTS = 32;
8164
+ function presenceMessage(challenge, sessionId, machineId) {
8165
+ return Buffer.concat([challenge, Buffer.from(sessionId, "utf8"), Buffer.from(machineId, "utf8")]);
8166
+ }
8167
+ function presenceAnswer(key, challenge, sessionId, machineId) {
8168
+ return createHmac2("sha256", key).update(presenceMessage(challenge, sessionId, machineId)).digest("base64url");
8169
+ }
8170
+ var PresenceGate = class {
8171
+ constructor(o) {
8172
+ this.o = o;
8173
+ this.key = Buffer.from(o.presence.key, "base64url");
8174
+ this.now = o.now ?? Date.now;
8175
+ }
8176
+ o;
8177
+ key;
8178
+ challenges = /* @__PURE__ */ new Map();
8179
+ grants = /* @__PURE__ */ new Map();
8180
+ failures = [];
8181
+ lockedUntil = 0;
8182
+ now;
8183
+ /** Locked until this time, or `null`. */
8184
+ locked() {
8185
+ return this.lockedUntil > this.now() ? this.lockedUntil : null;
8186
+ }
8187
+ challenge(session) {
8188
+ if (session === null) return null;
8189
+ const until = this.locked();
8190
+ if (until !== null) return { locked: true, until };
8191
+ const t2 = this.now();
8192
+ for (const [c, v] of this.challenges) if (v.expiresAt <= t2) this.challenges.delete(c);
8193
+ while (this.challenges.size >= MAX_CHALLENGES) this.challenges.delete(this.challenges.keys().next().value);
8194
+ const challenge = randomBytes6(32).toString("base64url");
8195
+ const expiresAt = t2 + CHALLENGE_MS;
8196
+ this.challenges.set(challenge, { session, expiresAt });
8197
+ return { challenge, salt: this.o.presence.salt, iterations: this.o.presence.iterations, expiresAt };
8198
+ }
8199
+ answer(session, challenge, answer) {
8200
+ if (session === null) return { ok: false, error: "no_session" };
8201
+ const until = this.locked();
8202
+ if (until !== null) return { ok: false, error: "locked", until };
8203
+ const t2 = this.now();
8204
+ const c = this.challenges.get(challenge);
8205
+ this.challenges.delete(challenge);
8206
+ if (!c || c.expiresAt <= t2 || c.session !== session) return { ok: false, error: "expired" };
8207
+ const want = Buffer.from(presenceAnswer(this.key, Buffer.from(challenge, "base64url"), session, this.o.machineId), "base64url");
8208
+ const got = Buffer.from(answer, "base64url");
8209
+ if (got.length !== want.length || !timingSafeEqual3(got, want)) {
8210
+ this.failures = this.failures.filter((f) => f > t2 - LOCK_WINDOW_MS);
8211
+ this.failures.push(t2);
8212
+ if (this.failures.length >= LOCK_FAILURES) {
8213
+ this.lockedUntil = t2 + LOCK_MS;
8214
+ this.failures = [];
8215
+ this.challenges.clear();
8216
+ this.o.log?.(`presence: ${LOCK_FAILURES} wrong machines passphrases in ${LOCK_WINDOW_MS / 6e4} minutes; locked for ${LOCK_MS / 6e4} minutes`);
8217
+ return { ok: false, error: "locked", until: this.lockedUntil };
8218
+ }
8219
+ return { ok: false, error: "wrong", failures: this.failures.length };
8220
+ }
8221
+ this.failures = [];
8222
+ while (this.grants.size >= MAX_GRANTS) this.grants.delete(this.grants.keys().next().value);
8223
+ const grant = `pg_${randomBytes6(32).toString("base64url")}`;
8224
+ const g = { session, issuedAt: t2, lastActive: t2, open: 0 };
8225
+ this.grants.set(grant, g);
8226
+ return { ok: true, grant, expiresAt: this.expiry(g) };
8227
+ }
8228
+ expiry(g) {
8229
+ return g.issuedAt + this.o.hours() * 60 * 6e4;
8230
+ }
8231
+ /** Whether `grant` is live for `session` now. */
8232
+ check(grant, session) {
8233
+ if (grant === null || session === null) return false;
8234
+ const g = this.grants.get(grant);
8235
+ if (!g || g.session !== session) return false;
8236
+ const t2 = this.now();
8237
+ if (t2 >= this.expiry(g) || g.open === 0 && t2 - g.lastActive >= GRANT_IDLE_MS) {
8238
+ this.grants.delete(grant);
8239
+ return false;
8240
+ }
8241
+ return true;
8242
+ }
8243
+ /** A channel opened on `grant`: it does not idle out while open. Returns its release. */
8244
+ hold(grant) {
8245
+ const g = this.grants.get(grant);
8246
+ if (!g) return () => {
8247
+ };
8248
+ g.open += 1;
8249
+ g.lastActive = this.now();
8250
+ let done = false;
8251
+ return () => {
8252
+ if (done) return;
8253
+ done = true;
8254
+ g.open -= 1;
8255
+ g.lastActive = this.now();
8256
+ };
8257
+ }
8258
+ };
8259
+
8260
+ // src/node/prompt.ts
8261
+ var Prompter = class {
8262
+ constructor(stdin, out) {
8263
+ this.stdin = stdin;
8264
+ this.out = out;
8265
+ }
8266
+ stdin;
8267
+ out;
8268
+ buffer = "";
8269
+ ended = false;
8270
+ waiting = null;
8271
+ listening = false;
8272
+ listen() {
8273
+ if (this.listening) return;
8274
+ this.listening = true;
8275
+ this.stdin.on("data", (c) => {
8276
+ this.buffer += c.toString();
8277
+ this.waiting?.();
8278
+ });
8279
+ this.stdin.on("end", () => {
8280
+ this.ended = true;
8281
+ this.waiting?.();
8282
+ });
8283
+ this.stdin.resume();
8284
+ }
8285
+ async nextLine() {
8286
+ this.listen();
8287
+ for (; ; ) {
8288
+ const i = this.buffer.search(/\r?\n|\r/);
8289
+ if (i >= 0) {
8290
+ const line = this.buffer.slice(0, i);
8291
+ const nl = /^\r?\n|^\r/.exec(this.buffer.slice(i))?.[0].length ?? 1;
8292
+ this.buffer = this.buffer.slice(i + nl);
8293
+ return line;
8294
+ }
8295
+ if (this.ended) {
8296
+ if (this.buffer === "") throw new CliError("no answer on stdin", EXIT.USAGE);
8297
+ const line = this.buffer;
8298
+ this.buffer = "";
8299
+ return line;
8300
+ }
8301
+ await new Promise((r) => this.waiting = r);
8302
+ this.waiting = null;
8303
+ }
8304
+ }
8305
+ async line(question) {
8306
+ this.out.write(question);
8307
+ return (await this.nextLine()).trim();
8308
+ }
8309
+ /** A secret: on a terminal, read in raw mode and never echoed. */
8310
+ async secret(question) {
8311
+ this.out.write(question);
8312
+ if (!this.stdin.isTTY || !this.stdin.setRawMode) return this.nextLine();
8313
+ this.listen();
8314
+ this.stdin.setRawMode(true);
8315
+ try {
8316
+ let value = "";
8317
+ for (; ; ) {
8318
+ while (this.buffer.length > 0) {
8319
+ const ch = this.buffer[0];
8320
+ this.buffer = this.buffer.slice(1);
8321
+ if (ch === "\r" || ch === "\n") {
8322
+ this.out.write("\n");
8323
+ return value;
8324
+ }
8325
+ if (ch === "") throw new CliError("interrupted", EXIT.INTERRUPTED);
8326
+ if (ch === "\x7F" || ch === "\b") value = value.slice(0, -1);
8327
+ else if (ch >= " ") value += ch;
8328
+ }
8329
+ if (this.ended) throw new CliError("no answer", EXIT.USAGE);
8330
+ await new Promise((r) => this.waiting = r);
8331
+ this.waiting = null;
8332
+ }
8333
+ } finally {
8334
+ this.stdin.setRawMode(false);
8335
+ }
8336
+ }
8337
+ close() {
8338
+ this.stdin.pause();
8339
+ }
8340
+ };
8341
+
8342
+ // src/node/run.ts
8343
+ import fs19 from "node:fs";
8344
+ import path17 from "node:path";
8345
+
8346
+ // ../hostcore/src/herdr/socket.ts
8347
+ import net4 from "node:net";
8348
+ import readline from "node:readline";
8349
+ var HerdrError = class extends Error {
8350
+ code;
8351
+ constructor(code2, message) {
8352
+ super(message);
8353
+ this.code = code2;
8354
+ this.name = "HerdrError";
8355
+ }
8356
+ };
8357
+ var seq = 0;
8358
+ function request(socketPath, method, params = {}, opts = {}) {
8359
+ return new Promise((resolve, reject) => {
8360
+ const id = `wb_${++seq}`;
8361
+ const sock = net4.connect(socketPath);
8362
+ const rl = readline.createInterface({ input: sock });
8363
+ rl.on("error", () => {
8364
+ });
8365
+ const timer = setTimeout(() => {
8366
+ rl.close();
8367
+ sock.destroy();
8368
+ reject(new HerdrError("timeout", `${method} timed out`));
8369
+ }, opts.timeoutMs ?? 1e4);
8370
+ sock.once("error", (err) => {
8371
+ clearTimeout(timer);
8372
+ rl.close();
8373
+ reject(err);
8374
+ });
8375
+ sock.once("connect", () => {
8376
+ sock.write(JSON.stringify({ id, method, params }) + "\n");
8377
+ });
8378
+ rl.once("line", (line) => {
8379
+ clearTimeout(timer);
8380
+ rl.close();
8381
+ sock.end();
8382
+ let msg;
8383
+ try {
8384
+ msg = JSON.parse(line);
8385
+ } catch {
8386
+ reject(new HerdrError("bad_json", line.slice(0, 200)));
8387
+ return;
8388
+ }
8389
+ if (msg.error) {
8390
+ reject(new HerdrError(msg.error.code ?? "error", msg.error.message ?? "herdr error"));
8391
+ return;
8392
+ }
8393
+ resolve(msg.result);
8394
+ });
8395
+ });
8396
+ }
8397
+ function subscribe(socketPath, subscriptions, onEvent, onClose) {
8398
+ return new Promise((resolve, reject) => {
8399
+ const sock = net4.connect(socketPath);
8400
+ let started = false;
8401
+ let closing = false;
8402
+ const rl = readline.createInterface({ input: sock });
8403
+ rl.on("error", () => {
8404
+ });
8405
+ sock.once("connect", () => {
8406
+ sock.write(JSON.stringify({ id: "sub", method: "events.subscribe", params: { subscriptions } }) + "\n");
8407
+ });
8408
+ sock.on("error", (err) => {
8409
+ if (!started) {
8410
+ reject(err);
8411
+ } else if (!closing) {
8412
+ onClose(err);
8413
+ }
8414
+ });
8415
+ sock.on("close", () => {
8416
+ if (started && !closing) onClose();
8417
+ });
8418
+ rl.on("line", (line) => {
8419
+ let msg;
8420
+ try {
8421
+ msg = JSON.parse(line);
8422
+ } catch {
8423
+ return;
8424
+ }
8425
+ if (!started) {
8426
+ if (msg.error) {
8427
+ sock.destroy();
8428
+ reject(new HerdrError(msg.error.code ?? "error", msg.error.message ?? "herdr error"));
8429
+ return;
8430
+ }
8431
+ started = true;
8432
+ resolve({
8433
+ close: () => {
8434
+ closing = true;
8435
+ sock.destroy();
8436
+ }
8437
+ });
8438
+ return;
8439
+ }
8440
+ if (msg.event) onEvent(msg);
8441
+ });
8442
+ });
8443
+ }
8444
+
8445
+ // ../hostcore/src/herdr/session.ts
8446
+ var LIFECYCLE_TYPES = [
8447
+ "workspace.created",
8448
+ "workspace.updated",
8449
+ "workspace.metadata_updated",
8450
+ "workspace.renamed",
8451
+ "workspace.moved",
8452
+ "workspace.reordered",
8453
+ "workspace.closed",
8454
+ "workspace.focused",
8455
+ "worktree.created",
8456
+ "worktree.opened",
8457
+ "worktree.removed",
8458
+ "tab.created",
8459
+ "tab.closed",
8460
+ "tab.focused",
8461
+ "tab.renamed",
8462
+ "tab.moved",
8463
+ "pane.created",
8464
+ "pane.closed",
8465
+ "pane.updated",
8466
+ "pane.focused",
8467
+ "pane.moved",
8468
+ "pane.exited",
8469
+ "pane.agent_detected",
8470
+ "layout.updated"
8471
+ ];
8472
+ var SessionHub = class {
8473
+ socketPath;
8474
+ lifecycleSub = null;
8475
+ agentSub = null;
8476
+ panes = /* @__PURE__ */ new Set();
8477
+ listeners = /* @__PURE__ */ new Set();
8478
+ _connected = false;
8479
+ _version = null;
8480
+ _protocol = null;
8481
+ stopped = false;
8482
+ reconnecting = false;
8483
+ reconnectDelay = 250;
8484
+ reconnectTimer = null;
8485
+ agentDebounce = null;
8486
+ constructor(socketPath) {
8487
+ this.socketPath = socketPath;
8488
+ }
8489
+ get connected() {
8490
+ return this._connected;
8491
+ }
8492
+ get version() {
8493
+ return this._version;
8494
+ }
8495
+ get protocol() {
8496
+ return this._protocol;
8497
+ }
8498
+ paneIds() {
8499
+ return [...this.panes];
8500
+ }
8501
+ on(listener) {
8502
+ this.listeners.add(listener);
8503
+ return () => {
8504
+ this.listeners.delete(listener);
8505
+ };
8506
+ }
8507
+ emit(m) {
8508
+ for (const listener of this.listeners) listener(m);
8509
+ }
8510
+ async start() {
8511
+ await this.connect();
8512
+ }
8513
+ async snapshot() {
8514
+ const res = await request(this.socketPath, "session.snapshot");
8515
+ return res.snapshot;
8516
+ }
8517
+ stop() {
8518
+ this.stopped = true;
8519
+ if (this.reconnectTimer) {
8520
+ clearTimeout(this.reconnectTimer);
8521
+ this.reconnectTimer = null;
8522
+ }
8523
+ if (this.agentDebounce) {
8524
+ clearTimeout(this.agentDebounce);
8525
+ this.agentDebounce = null;
8526
+ }
8527
+ this.closeSubs();
8528
+ this._connected = false;
8529
+ this.listeners.clear();
8530
+ }
8531
+ closeSubs() {
8532
+ if (this.lifecycleSub) {
8533
+ this.lifecycleSub.close();
8534
+ this.lifecycleSub = null;
8535
+ }
8536
+ if (this.agentSub) {
8537
+ this.agentSub.close();
8538
+ this.agentSub = null;
8539
+ }
8540
+ }
8541
+ /** Open lifecycle subscription, load the snapshot, then open agent-status. */
8542
+ async connect() {
8543
+ if (this.agentDebounce) {
8544
+ clearTimeout(this.agentDebounce);
8545
+ this.agentDebounce = null;
8546
+ }
8547
+ this.closeSubs();
8548
+ this.lifecycleSub = await subscribe(
8549
+ this.socketPath,
8550
+ LIFECYCLE_TYPES.map((type) => ({ type })),
8551
+ (e) => this.onLifecycle(e),
8552
+ () => this.onDrop()
8553
+ );
8554
+ const snap = await this.snapshot();
8555
+ this._version = snap.version;
8556
+ this._protocol = snap.protocol;
8557
+ this.panes.clear();
8558
+ for (const p of snap.panes) this.panes.add(p.pane_id);
8559
+ await this.openAgentSub();
8560
+ this._connected = true;
8561
+ }
8562
+ async openAgentSub() {
8563
+ const subs = this.paneIds().map((pane_id) => ({
8564
+ type: "pane.agent_status_changed",
8565
+ pane_id
8566
+ }));
8567
+ const next = await subscribe(
8568
+ this.socketPath,
8569
+ subs,
8570
+ (e) => this.onAgentEvent(e),
8571
+ () => this.onDrop()
8572
+ );
8573
+ const previous = this.agentSub;
8574
+ this.agentSub = next;
8575
+ if (previous) previous.close();
8576
+ }
8577
+ onLifecycle(e) {
8578
+ this.emit({ kind: "event", event: e.event, data: e.data });
8579
+ let changed = false;
8580
+ if (e.event === "pane_created" || e.event === "pane_moved") {
8581
+ const paneId = e.data.pane?.pane_id;
8582
+ if (paneId && !this.panes.has(paneId)) {
8583
+ this.panes.add(paneId);
8584
+ changed = true;
8585
+ }
8586
+ } else if (e.event === "pane_closed") {
8587
+ const paneId = e.data.pane_id;
8588
+ if (paneId && this.panes.delete(paneId)) changed = true;
8589
+ }
8590
+ if (changed) this.scheduleAgentReopen();
8591
+ }
8592
+ onAgentEvent(e) {
8593
+ this.emit({ kind: "event", event: "pane_agent_status_changed", data: e.data });
8594
+ }
8595
+ scheduleAgentReopen() {
8596
+ if (this.agentDebounce) clearTimeout(this.agentDebounce);
8597
+ this.agentDebounce = setTimeout(() => {
8598
+ this.agentDebounce = null;
8599
+ this.openAgentSub().catch(() => this.onDrop());
8600
+ }, 50);
8601
+ }
8602
+ onDrop() {
8603
+ if (this.stopped || this.reconnecting) return;
8604
+ this.reconnecting = true;
8605
+ this._connected = false;
8606
+ this.scheduleReconnect();
8607
+ }
8608
+ scheduleReconnect() {
8609
+ this.reconnectTimer = setTimeout(() => {
8610
+ this.reconnectTimer = null;
8611
+ if (this.stopped) return;
8612
+ this.connect().then(
8613
+ () => {
8614
+ this.reconnectDelay = 250;
8615
+ this.reconnecting = false;
8616
+ this.emit({ kind: "reset", reason: "herdr reconnected" });
8617
+ },
8618
+ () => {
8619
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, 5e3);
8620
+ this.scheduleReconnect();
8621
+ }
8622
+ );
8623
+ }, this.reconnectDelay);
8624
+ }
8625
+ };
8626
+
8627
+ // ../hostcore/src/herdr/terminal.ts
8628
+ import { spawn as spawn5 } from "node:child_process";
8629
+ import readline2 from "node:readline";
8630
+ var MAX_REPLAY_BYTES = 1e6;
8631
+ var RELEASE_GRACE_MS = 1e3;
8632
+ var SIGKILL_GRACE_MS = 2e3;
8633
+ function killWithFallback(child, exited, graceMs = SIGKILL_GRACE_MS) {
8634
+ if (exited()) return null;
8635
+ if (!child.killed) child.kill("SIGTERM");
8636
+ const timer = setTimeout(() => {
8637
+ if (!exited()) child.kill("SIGKILL");
8638
+ }, graceMs);
8639
+ timer.unref?.();
8640
+ return timer;
8641
+ }
8642
+ var PaneStream = class {
8643
+ constructor(paneId, env, cols, rows, onDead) {
8644
+ this.paneId = paneId;
8645
+ this.env = env;
8646
+ this.onDead = onDead;
8647
+ this.cols = cols;
8648
+ this.rows = rows;
8649
+ this.watchScroll();
8650
+ this.child = this.spawn();
8651
+ }
8652
+ paneId;
8653
+ env;
8654
+ onDead;
8655
+ child;
8656
+ viewers = /* @__PURE__ */ new Set();
8657
+ /** Last full frame; the baseline replayed to late joiners. */
8658
+ full = null;
8659
+ /** Diffs received since the last full frame, replayed after it. */
8660
+ diffs = [];
8661
+ diffBytes = 0;
8662
+ cols;
8663
+ rows;
8664
+ /** Last size each viewer asked for, so focus can restore that viewer's size. */
8665
+ viewerSize = /* @__PURE__ */ new WeakMap();
8666
+ lastFullSeq = 0;
8667
+ released = false;
8668
+ dead = false;
8669
+ /** True once the child process has actually exited (not merely signalled). */
8670
+ childExited = false;
8671
+ killTimer = null;
8672
+ /** Where herdr's scrollback stands, from its pane.scroll_changed events. */
8673
+ scrolled = null;
8674
+ scrollSub = null;
8675
+ /**
8676
+ * Follow herdr's scrollback position for this pane and pass it to every
8677
+ * viewer, for the scrollbar and the way back to live. herdr reports it on a
8678
+ * scroll, on output that grows the history, and when typing snaps the pane
8679
+ * back to live. Without the socket the pane still scrolls, just unmarked.
8680
+ */
8681
+ watchScroll() {
8682
+ const socketPath = this.env.HERDR_SOCKET_PATH;
8683
+ if (!socketPath) return;
8684
+ subscribe(
8685
+ socketPath,
8686
+ [{ type: "pane.scroll_changed", pane_id: this.paneId }],
8687
+ (e) => {
8688
+ const info = e.data?.scroll;
8689
+ if (!info) return;
8690
+ const m = { type: "scrolled", offset: info.offset_from_bottom, max: info.max_offset_from_bottom, rows: info.viewport_rows };
8691
+ this.scrolled = m;
8692
+ for (const v of this.viewers) v.sendJson(m);
8693
+ },
8694
+ () => {
8695
+ }
8696
+ ).then(
8697
+ (sub) => {
8698
+ if (this.dead || this.released) sub.close();
8699
+ else this.scrollSub = sub;
8700
+ },
8701
+ () => {
8702
+ }
8703
+ );
8704
+ }
8705
+ unwatchScroll() {
8706
+ this.scrollSub?.close();
8707
+ this.scrollSub = null;
8708
+ }
8709
+ /** True once the child has exited/errored or herdr reported the pane closed. */
8710
+ get isDead() {
8711
+ return this.dead;
8712
+ }
8713
+ spawn() {
8714
+ const child = spawn5(
8715
+ "herdr",
8716
+ [
8717
+ "terminal",
8718
+ "session",
8719
+ "control",
8720
+ this.paneId,
8721
+ "--takeover",
8722
+ "--cols",
8723
+ String(this.cols),
8724
+ "--rows",
8725
+ String(this.rows)
8726
+ ],
8727
+ { env: this.env, stdio: ["pipe", "pipe", "pipe"] }
8728
+ );
8729
+ if (child.stdout) {
8730
+ readline2.createInterface({ input: child.stdout }).on("line", (line) => this.onLine(line));
8731
+ }
8732
+ child.stderr?.resume();
8733
+ child.on("error", () => this.markDead("stream error"));
8734
+ child.on("exit", () => {
8735
+ this.childExited = true;
8736
+ if (this.killTimer) {
8737
+ clearTimeout(this.killTimer);
8738
+ this.killTimer = null;
8739
+ }
8740
+ this.markDead("stream ended");
8741
+ });
8742
+ return child;
8743
+ }
8744
+ /**
8745
+ * Send SIGTERM and, if the child is still alive after a bounded grace, follow
8746
+ * it with SIGKILL. A herdr controller that ignores SIGTERM would otherwise
8747
+ * keep the process — and this stream — from ever tearing down.
8748
+ */
8749
+ killWithFallback() {
8750
+ if (this.childExited || this.killTimer) return;
8751
+ this.killTimer = killWithFallback(this.child, () => this.childExited);
8752
+ }
8753
+ /**
8754
+ * Mark the stream dead exactly once (dropping it from the registry) and close
8755
+ * any remaining viewers. Called on child exit/error and on terminal.closed.
8756
+ */
8757
+ markDead(reason) {
8758
+ this.unwatchScroll();
8759
+ if (!this.dead) {
8760
+ this.dead = true;
8761
+ this.onDead(this.paneId, this);
8762
+ }
8763
+ this.shutdownViewers(reason);
8764
+ }
8765
+ shutdownViewers(reason) {
8766
+ for (const v of this.viewers) v.close(reason);
8767
+ this.viewers.clear();
8768
+ }
8769
+ onLine(line) {
8770
+ let msg;
8771
+ try {
8772
+ msg = JSON.parse(line);
8773
+ } catch {
8774
+ return;
8775
+ }
8776
+ if (msg.type === "terminal.frame") {
8777
+ const buf = Buffer.from(msg.bytes, "base64");
8778
+ if (msg.full) {
8779
+ this.full = buf;
8780
+ this.diffs = [];
8781
+ this.diffBytes = 0;
8782
+ this.lastFullSeq = msg.seq;
8783
+ } else {
8784
+ this.diffs.push(buf);
8785
+ this.diffBytes += buf.length;
8786
+ if (this.diffBytes > MAX_REPLAY_BYTES) {
8787
+ this.full = null;
8788
+ this.diffs = [];
8789
+ this.diffBytes = 0;
8790
+ }
8791
+ }
8792
+ for (const v of this.viewers) v.send(buf);
8793
+ } else if (msg.type === "terminal.closed") {
8794
+ this.markDead(msg.reason ?? "closed");
8795
+ }
8796
+ }
8797
+ write(o) {
8798
+ this.child.stdin?.write(JSON.stringify(o) + "\n");
8799
+ }
8800
+ addViewer(v, cols, rows) {
8801
+ this.viewers.add(v);
8802
+ this.viewerSize.set(v, { cols, rows });
8803
+ if (this.scrolled) v.sendJson(this.scrolled);
8804
+ if (this.full && this.diffBytes < MAX_REPLAY_BYTES && cols === this.cols && rows === this.rows) {
8805
+ v.send(this.full);
8806
+ for (const d of this.diffs) v.send(d);
8807
+ } else {
8808
+ this.resize(cols, rows);
8809
+ }
8810
+ }
8811
+ removeViewer(v) {
8812
+ if (!this.viewers.delete(v)) return;
8813
+ if (this.viewers.size === 0) this.release();
8814
+ }
8815
+ release() {
8816
+ if (this.released) return;
8817
+ this.released = true;
8818
+ this.unwatchScroll();
8819
+ this.write({ type: "terminal.release" });
8820
+ this.child.stdin?.end();
8821
+ setTimeout(() => this.killWithFallback(), RELEASE_GRACE_MS).unref();
8822
+ }
8823
+ /** Kill the child immediately; used on shutdown, not on normal release. */
8824
+ destroy() {
8825
+ this.released = true;
8826
+ this.unwatchScroll();
8827
+ this.child.stdin?.end();
8828
+ this.killWithFallback();
8829
+ }
8830
+ resize(cols, rows) {
8831
+ this.cols = cols;
8832
+ this.rows = rows;
8833
+ this.write({ type: "terminal.resize", cols, rows });
8834
+ for (const v of this.viewers) v.sendJson({ type: "size", cols, rows });
8835
+ }
8836
+ /** Record this viewer's size and make it the current pane size. */
8837
+ resizeFor(v, cols, rows) {
8838
+ this.viewerSize.set(v, { cols, rows });
8839
+ this.resize(cols, rows);
8840
+ }
8841
+ /** The focusing viewer becomes the size owner: restore its last known size. */
8842
+ focus(v) {
8843
+ const size = this.viewerSize.get(v) ?? { cols: this.cols, rows: this.rows };
8844
+ this.resize(size.cols, size.rows);
8845
+ }
8846
+ input(text, bytes) {
8847
+ this.write({ type: "terminal.input", ...bytes ? { bytes } : { text: text ?? "" } });
8848
+ }
8849
+ scroll(direction, lines) {
8850
+ this.write({ type: "terminal.scroll", direction, lines: Math.max(1, lines | 0), source: "wheel" });
8851
+ }
8852
+ /** Put herdr's view of the pane `offset` lines above live; 0 is back to live. */
8853
+ scrollTo(offset) {
8854
+ const socketPath = this.env.HERDR_SOCKET_PATH;
8855
+ if (!socketPath || !Number.isFinite(offset)) return;
8856
+ const offset_from_bottom = Math.max(0, Math.floor(offset));
8857
+ request(socketPath, "pane.scroll", { pane_id: this.paneId, offset_from_bottom }).catch(() => {
8858
+ });
8859
+ }
8860
+ size() {
8861
+ return { cols: this.cols, rows: this.rows };
8862
+ }
8863
+ get viewerCount() {
8864
+ return this.viewers.size;
8865
+ }
8866
+ };
8867
+ var TerminalStreams = class {
8868
+ constructor(env) {
8869
+ this.env = env;
8870
+ }
8871
+ env;
8872
+ streams = /* @__PURE__ */ new Map();
8873
+ /** Drop a stream from the registry, but only if it is still the live one. */
8874
+ forget(paneId, stream) {
8875
+ if (this.streams.get(paneId) === stream) this.streams.delete(paneId);
8876
+ }
8877
+ attach(paneId, viewer, cols = 80, rows = 24) {
8878
+ let stream = this.streams.get(paneId);
8879
+ if (!stream || stream.isDead) {
8880
+ stream = new PaneStream(paneId, this.env, cols, rows, (p, s2) => this.forget(p, s2));
8881
+ this.streams.set(paneId, stream);
8882
+ }
8883
+ stream.addViewer(viewer, cols, rows);
8884
+ const s = stream;
8885
+ let detached = false;
8886
+ return {
8887
+ input: (text, bytes) => s.input(text, bytes),
8888
+ resize: (c, r) => s.resizeFor(viewer, c, r),
8889
+ scroll: (direction, lines) => s.scroll(direction, lines),
8890
+ scrollTo: (offset) => s.scrollTo(offset),
8891
+ focus: () => s.focus(viewer),
8892
+ detach: () => {
8893
+ if (detached) return;
8894
+ detached = true;
8895
+ s.removeViewer(viewer);
8896
+ if (s.viewerCount === 0) this.forget(paneId, s);
8897
+ }
8898
+ };
8899
+ }
8900
+ active(paneId) {
8901
+ return this.streams.has(paneId);
8902
+ }
8903
+ size(paneId) {
8904
+ return this.streams.get(paneId)?.size() ?? null;
8905
+ }
8906
+ lastFullSeq(paneId) {
8907
+ return this.streams.get(paneId)?.lastFullSeq ?? 0;
8908
+ }
8909
+ /** Test-only: number of live pane streams in the registry. */
8910
+ streamCount() {
8911
+ return this.streams.size;
8912
+ }
8913
+ /** Tear down every live stream; call on server shutdown. */
8914
+ stop() {
8915
+ for (const s of this.streams.values()) s.destroy();
8916
+ this.streams.clear();
8917
+ }
8918
+ };
8919
+
8920
+ // ../hostcore/src/ws-heartbeat.ts
8921
+ var HEARTBEAT_MS = 3e4;
8922
+ function heartbeat(socket2, intervalMs = HEARTBEAT_MS) {
8923
+ let alive = true;
8924
+ socket2.on("pong", () => {
8925
+ alive = true;
8926
+ });
8927
+ const timer = setInterval(() => {
8928
+ if (socket2.readyState !== socket2.OPEN) return;
8929
+ if (!alive) {
8930
+ socket2.terminate();
8931
+ return;
8932
+ }
8933
+ alive = false;
8934
+ try {
8935
+ socket2.ping();
8936
+ } catch {
8937
+ }
8938
+ }, intervalMs);
8939
+ timer.unref?.();
8940
+ const stop = () => clearInterval(timer);
8941
+ socket2.on("close", stop);
8942
+ return stop;
8943
+ }
8944
+
8945
+ // ../hostcore/src/herdr/terminal-socket.ts
8946
+ var PANE_RE = /^w[0-9A-Za-z]{1,12}:p[0-9A-Za-z]{1,12}$/;
8947
+ var MAX_BUFFERED_BYTES = 8 * 1024 * 1024;
8948
+ var MAX_CHANNELS = 64;
8949
+ function isBackpressured(bufferedAmount, limit = MAX_BUFFERED_BYTES) {
8950
+ return bufferedAmount > limit;
8951
+ }
8952
+ function clamp(value, min, max, fallback) {
8953
+ return Number.isFinite(value) ? Math.min(max, Math.max(min, Math.trunc(value))) : fallback;
8954
+ }
8955
+ function muxFrame(ch, data) {
8956
+ const out = Buffer.allocUnsafe(2 + data.length);
8957
+ out.writeUInt16BE(ch, 0);
8958
+ data.copy(out, 2);
8959
+ return out;
8960
+ }
8961
+ function sendOrShed(socket2, data) {
8962
+ if (socket2.readyState !== socket2.OPEN) return;
8963
+ if (isBackpressured(socket2.bufferedAmount)) {
8964
+ try {
8965
+ socket2.close(1013, "viewer too slow");
8966
+ } catch {
8967
+ }
8968
+ return;
8969
+ }
8970
+ socket2.send(data, { binary: true });
8971
+ }
8972
+ function sendJson(socket2, m) {
8973
+ if (socket2.readyState === socket2.OPEN) socket2.send(JSON.stringify(m));
8974
+ }
8975
+ function control(a, msg, size) {
8976
+ switch (msg.type) {
8977
+ case "input":
8978
+ a.input(msg.text, msg.bytes);
8979
+ break;
8980
+ case "resize":
8981
+ a.resize(clamp(Number(msg.cols), 20, 500, size.cols), clamp(Number(msg.rows), 5, 200, size.rows));
8982
+ break;
8983
+ case "scroll":
8984
+ if (msg.direction === "up" || msg.direction === "down") a.scroll(msg.direction, Number(msg.lines));
8985
+ break;
8986
+ case "scrollTo":
8987
+ a.scrollTo(Number(msg.offset));
8988
+ break;
8989
+ case "focus":
8990
+ a.focus();
8991
+ break;
8992
+ default:
8993
+ break;
8994
+ }
8995
+ }
8996
+ function parse(raw) {
8997
+ try {
8998
+ return JSON.parse(raw.toString());
8999
+ } catch {
9000
+ return null;
9001
+ }
9002
+ }
9003
+ function single(socket2, streams, pane, cols, rows) {
9004
+ const viewer = {
9005
+ send: (data) => sendOrShed(socket2, data),
9006
+ sendJson: (m) => sendJson(socket2, m),
9007
+ close: (reason) => {
9008
+ try {
9009
+ if (socket2.readyState === socket2.OPEN) {
9010
+ socket2.send(JSON.stringify({ type: "closed", reason }));
9011
+ socket2.close(1e3);
9012
+ }
9013
+ } catch {
9014
+ }
9015
+ }
9016
+ };
9017
+ const attachment = streams.attach(pane, viewer, cols, rows);
9018
+ socket2.on("message", (raw) => {
9019
+ const msg = parse(raw);
9020
+ if (msg) control(attachment, msg, { cols, rows });
9021
+ });
9022
+ socket2.on("close", () => attachment.detach());
9023
+ }
9024
+ function shared(socket2, streams) {
9025
+ const channels = /* @__PURE__ */ new Map();
9026
+ const notice = (ch, m) => sendJson(socket2, { ...m, ch });
9027
+ const attach2 = (ch, pane, c, r) => {
9028
+ if (channels.has(ch)) return;
9029
+ if (typeof pane !== "string" || !PANE_RE.test(pane)) return notice(ch, { type: "closed", reason: "invalid pane" });
9030
+ if (channels.size >= MAX_CHANNELS) return notice(ch, { type: "closed", reason: "too many panes on one socket" });
9031
+ const size = { cols: clamp(Number(c), 20, 500, 80), rows: clamp(Number(r), 5, 200, 24) };
9032
+ let live = true;
9033
+ const viewer = {
9034
+ send: (data) => {
9035
+ if (live) sendOrShed(socket2, muxFrame(ch, data));
9036
+ },
9037
+ sendJson: (m) => {
9038
+ if (live) notice(ch, m);
9039
+ },
9040
+ close: (reason) => {
9041
+ if (!live) return;
9042
+ live = false;
9043
+ channels.delete(ch);
9044
+ notice(ch, { type: "closed", reason });
9045
+ }
9046
+ };
9047
+ const attachment = streams.attach(pane, viewer, size.cols, size.rows);
9048
+ if (live) channels.set(ch, { attachment, size });
9049
+ };
9050
+ socket2.on("message", (raw) => {
9051
+ const msg = parse(raw);
9052
+ if (!msg) return;
9053
+ const ch = Number(msg.ch);
9054
+ if (!Number.isInteger(ch) || ch < 0 || ch > 65535) return;
9055
+ if (msg.type === "ping") return sendJson(socket2, { type: "pong", ch, t: Number(msg.t) || 0 });
9056
+ if (msg.type === "attach") return attach2(ch, msg.pane, msg.cols, msg.rows);
9057
+ const c = channels.get(ch);
9058
+ if (!c) return;
9059
+ if (msg.type === "detach") {
9060
+ channels.delete(ch);
9061
+ c.attachment.detach();
9062
+ return;
9063
+ }
9064
+ if (msg.type === "resize") c.size = { cols: clamp(Number(msg.cols), 20, 500, c.size.cols), rows: clamp(Number(msg.rows), 5, 200, c.size.rows) };
9065
+ control(c.attachment, msg, c.size);
9066
+ });
9067
+ socket2.on("close", () => {
9068
+ for (const c of channels.values()) c.attachment.detach();
9069
+ channels.clear();
9070
+ });
9071
+ }
9072
+ function serveTerminalSocket(socket2, streams, query) {
9073
+ const pane = query.pane;
9074
+ if (pane === void 0) {
9075
+ heartbeat(socket2);
9076
+ shared(socket2, streams);
9077
+ return;
9078
+ }
9079
+ if (typeof pane !== "string" || !PANE_RE.test(pane)) {
9080
+ socket2.close(1008, "invalid pane");
9081
+ return;
9082
+ }
9083
+ heartbeat(socket2);
9084
+ single(socket2, streams, pane, clamp(Number(query.cols), 20, 500, 80), clamp(Number(query.rows), 5, 200, 24));
9085
+ }
9086
+
9087
+ // ../hostcore/src/rpc-allowlist.ts
9088
+ var EXACT = /* @__PURE__ */ new Set(["ping", "session.snapshot", "notification.show"]);
9089
+ var PREFIXES = ["workspace.", "worktree.", "tab.", "pane.", "agent.", "layout."];
9090
+ function isGraphics(method) {
9091
+ return method === "pane.graphics" || method.startsWith("pane.graphics.");
9092
+ }
9093
+ function isAllowed(method) {
9094
+ if (EXACT.has(method)) return true;
9095
+ if (isGraphics(method)) return false;
9096
+ return PREFIXES.some(
9097
+ (p) => method.startsWith(p) && method.length > p.length && /^[a-z_.]+$/.test(method)
9098
+ );
9099
+ }
9100
+
9101
+ // src/node/herdr.ts
9102
+ import { spawn as spawn6 } from "node:child_process";
9103
+ import { randomBytes as randomBytes7 } from "node:crypto";
9104
+ import { EventEmitter as EventEmitter2 } from "node:events";
9105
+ import fs16 from "node:fs";
9106
+ import os8 from "node:os";
9107
+ import path14 from "node:path";
9108
+ var HERDR_SESSION = "agentbox-node";
9109
+ var CHECK_MS = 3e4;
9110
+ var FAILED_CHECKS = 2;
9111
+ var READY_MS = 15e3;
9112
+ var MAX_SOCKET_PATH = 96;
9113
+ var HerdrSupervisor = class extends EventEmitter2 {
9114
+ constructor(o) {
9115
+ super();
9116
+ this.o = o;
9117
+ this.stateFile = path14.join(o.dir, "state", "herdr.json");
9118
+ }
9119
+ o;
9120
+ child = null;
9121
+ socket = null;
9122
+ pid = null;
9123
+ stopped = false;
9124
+ failures = 0;
9125
+ checker = null;
9126
+ restarting = null;
9127
+ backoff = 1e3;
9128
+ stateFile;
9129
+ /** The control socket now, or `null` while herdr is starting. */
9130
+ get socketPath() {
9131
+ return this.socket;
9132
+ }
9133
+ log(line) {
9134
+ this.emit("log", line);
9135
+ }
9136
+ async answers(socket2) {
9137
+ try {
9138
+ const pong = await request(socket2, "ping", {}, { timeoutMs: 5e3 });
9139
+ return pong.version === (this.o.version ?? HERDR_VERSION);
9140
+ } catch {
9141
+ return false;
9142
+ }
9143
+ }
9144
+ /** A fresh socket path: in the node folder when it fits, else a private folder of its own. */
9145
+ freshSocket() {
9146
+ const name = `herdr-${randomBytes7(4).toString("hex")}.sock`;
9147
+ const inNode = path14.join(this.o.dir, name);
9148
+ if (Buffer.byteLength(inNode) <= MAX_SOCKET_PATH) return inNode;
9149
+ const dir = fs16.mkdtempSync(path14.join(os8.tmpdir(), "agentbox-node-"));
9150
+ fs16.chmodSync(dir, 448);
9151
+ return path14.join(dir, name);
9152
+ }
9153
+ async start() {
9154
+ this.stopped = false;
9155
+ const saved = this.readSaved();
9156
+ if (saved && await this.answers(saved.socket)) {
9157
+ this.socket = saved.socket;
9158
+ this.pid = saved.pid;
9159
+ this.log(`herdr: kept the running server (pid ${saved.pid})`);
9160
+ this.emit("ready", saved.socket, { restored: false });
9161
+ } else {
9162
+ if (saved) this.kill(saved.pid);
9163
+ await this.spawnServer();
9164
+ }
9165
+ this.watch();
9166
+ }
9167
+ readSaved() {
9168
+ try {
9169
+ const v = JSON.parse(fs16.readFileSync(this.stateFile, "utf8"));
9170
+ return typeof v.socket === "string" && typeof v.pid === "number" ? { socket: v.socket, pid: v.pid } : null;
9171
+ } catch {
9172
+ return null;
9173
+ }
9174
+ }
9175
+ /** Only a herdr of ours: on Linux its command line says so; elsewhere, the pid we recorded. */
9176
+ kill(pid) {
9177
+ try {
9178
+ if (process.platform === "linux") {
9179
+ const cmd = fs16.readFileSync(`/proc/${pid}/cmdline`, "utf8");
9180
+ if (!cmd.includes("herdr")) return;
9181
+ }
9182
+ process.kill(pid, "SIGTERM");
9183
+ } catch {
9184
+ }
9185
+ }
9186
+ removeSocket(socket2) {
9187
+ if (!socket2) return;
9188
+ for (const f of [socket2, socket2.replace(/\.sock$/, "-client.sock")]) fs16.rmSync(f, { force: true });
9189
+ }
9190
+ async spawnServer() {
9191
+ const old = this.socket;
9192
+ this.socket = null;
9193
+ this.removeSocket(old);
9194
+ const socket2 = this.freshSocket();
9195
+ ensurePrivateDir(path14.dirname(this.stateFile));
9196
+ const child = spawn6(this.o.bin, ["server"], {
9197
+ env: { ...this.o.env, HERDR_SOCKET_PATH: socket2, HERDR_SESSION },
9198
+ stdio: ["ignore", "ignore", "pipe"],
9199
+ // Its own session: herdr counts only such a server as a daemon, and it
9200
+ // outlives the node.
9201
+ detached: true,
9202
+ windowsHide: true
9203
+ });
9204
+ this.child = child;
9205
+ child.stderr?.on("data", (c) => this.log(`herdr: ${c.toString("utf8").trim().slice(0, 500)}`));
9206
+ child.once("error", (err) => this.log(`herdr: could not start: ${err.message}`));
9207
+ child.once("exit", (code2, signal) => {
9208
+ if (this.child === child) this.child = null;
9209
+ if (this.stopped || this.socket !== socket2) return;
9210
+ this.log(`herdr exited (code ${code2 ?? "none"}, signal ${signal ?? "none"}); restarting`);
9211
+ this.socket = null;
9212
+ this.emit("down");
9213
+ void this.restart("it exited");
9214
+ });
9215
+ child.unref();
9216
+ const end = Date.now() + READY_MS;
9217
+ while (!await this.answers(socket2)) {
9218
+ if (this.stopped || this.child !== child) return;
9219
+ if (Date.now() > end) {
9220
+ child.kill("SIGTERM");
9221
+ throw new Error(`herdr did not answer on ${socket2} within ${READY_MS / 1e3} s`);
9222
+ }
9223
+ await new Promise((r) => setTimeout(r, 200));
9224
+ }
9225
+ this.socket = socket2;
9226
+ this.pid = child.pid ?? null;
9227
+ if (this.pid !== null) writePrivate2(this.stateFile, `${JSON.stringify({ socket: socket2, pid: this.pid })}
9228
+ `);
9229
+ this.failures = 0;
9230
+ this.backoff = 1e3;
9231
+ this.log(`herdr ${this.o.version ?? HERDR_VERSION} started (pid ${this.pid ?? "?"})`);
9232
+ this.emit("ready", socket2, { restored: true });
9233
+ }
9234
+ watch() {
9235
+ if (this.checker) clearInterval(this.checker);
9236
+ this.checker = setInterval(() => void this.check(), this.o.checkMs ?? CHECK_MS);
9237
+ this.checker.unref();
9238
+ }
9239
+ /** One health check: two failures in a row and herdr is restarted. */
9240
+ async check() {
9241
+ if (this.stopped || this.restarting || !this.socket) return;
9242
+ if (await this.answers(this.socket)) {
9243
+ this.failures = 0;
9244
+ return;
9245
+ }
9246
+ this.failures += 1;
9247
+ this.log(`herdr: its control socket did not answer (${this.failures} of ${FAILED_CHECKS})`);
9248
+ if (this.failures >= FAILED_CHECKS) await this.restart("its control socket stopped answering");
9249
+ }
9250
+ /** Stop the server and start a fresh one (a hang, an exit, an update while idle). */
9251
+ restart(reason) {
9252
+ if (this.restarting) return this.restarting;
9253
+ this.restarting = (async () => {
9254
+ this.log(`herdr: restarting (${reason})`);
9255
+ this.failures = 0;
9256
+ const child = this.child;
9257
+ this.child = null;
9258
+ if (child) child.kill("SIGTERM");
9259
+ else if (this.pid !== null) this.kill(this.pid);
9260
+ this.socket = null;
9261
+ this.emit("down");
9262
+ for (; ; ) {
9263
+ if (this.stopped) return;
9264
+ try {
9265
+ await this.spawnServer();
9266
+ return;
9267
+ } catch (err) {
9268
+ this.log(`herdr: ${err.message}; trying again in ${this.backoff / 1e3} s`);
9269
+ await new Promise((r) => setTimeout(r, this.backoff));
9270
+ this.backoff = Math.min(this.backoff * 2, 3e4);
9271
+ }
9272
+ }
9273
+ })().finally(() => {
9274
+ this.restarting = null;
9275
+ });
9276
+ return this.restarting;
9277
+ }
9278
+ /** No agent is running (the updater replaces herdr and Claude Code only then). */
9279
+ async idle() {
9280
+ if (!this.socket) return true;
9281
+ try {
9282
+ const r = await request(this.socket, "session.snapshot", {}, { timeoutMs: 5e3 });
9283
+ return (r.snapshot.agents ?? []).length === 0;
9284
+ } catch {
9285
+ return false;
9286
+ }
9287
+ }
9288
+ /** Stop supervising. herdr keeps running (its agents with it) unless `kill`. */
9289
+ stop(kill = false) {
9290
+ this.stopped = true;
9291
+ if (this.checker) clearInterval(this.checker);
9292
+ this.checker = null;
9293
+ if (kill) {
9294
+ if (this.child) this.child.kill("SIGTERM");
9295
+ else if (this.pid !== null) this.kill(this.pid);
9296
+ }
9297
+ }
9298
+ };
9299
+
9300
+ // src/node/server.ts
9301
+ import http from "node:http";
9302
+ var MAX_BODY = 64 * 1024;
9303
+ function json(res, status3, body) {
9304
+ const text = JSON.stringify(body);
9305
+ res.writeHead(status3, { "content-type": "application/json", "content-length": Buffer.byteLength(text), "cache-control": "no-store" });
9306
+ res.end(text);
9307
+ }
9308
+ function readJson(req) {
9309
+ return new Promise((resolve) => {
9310
+ const chunks = [];
9311
+ let size = 0;
9312
+ req.on("data", (c) => {
9313
+ size += c.length;
9314
+ if (size > MAX_BODY) {
9315
+ resolve(null);
9316
+ req.destroy();
9317
+ return;
9318
+ }
9319
+ chunks.push(c);
9320
+ });
9321
+ req.on("end", () => {
9322
+ try {
9323
+ const v = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
9324
+ resolve(typeof v === "object" && v !== null && !Array.isArray(v) ? v : null);
9325
+ } catch {
9326
+ resolve(null);
9327
+ }
9328
+ });
9329
+ req.on("error", () => resolve(null));
9330
+ });
9331
+ }
9332
+ function refuseSocket(socket2, status3, body) {
9333
+ const text = JSON.stringify(body);
9334
+ socket2.end(
9335
+ `HTTP/1.1 ${status3} ${http.STATUS_CODES[status3] ?? ""}\r
9336
+ Connection: close\r
9337
+ Content-Type: application/json\r
9338
+ Content-Length: ${Buffer.byteLength(text)}\r
9339
+ \r
9340
+ ${text}`
9341
+ );
9342
+ }
9343
+ var NodeServer = class {
9344
+ constructor(o) {
9345
+ this.o = o;
9346
+ this.server = http.createServer({ requestTimeout: 0, headersTimeout: 3e4, keepAliveTimeout: 6e4 }, (req, res) => {
9347
+ void this.handle(req, res).catch((err) => {
9348
+ o.log?.(`server: ${err.message}`);
9349
+ if (!res.headersSent) json(res, 500, { error: "internal" });
9350
+ else res.destroy();
9351
+ });
9352
+ });
9353
+ this.server.on("upgrade", (req, socket2, head) => this.upgrade(req, socket2, head));
9354
+ this.server.on("clientError", (_err, socket2) => socket2.destroy());
9355
+ }
9356
+ o;
9357
+ server;
9358
+ wss = new import_websocket_server.default({ noServer: true, maxPayload: 1024 * 1024 });
9359
+ opens = /* @__PURE__ */ new WeakMap();
9360
+ channels = /* @__PURE__ */ new Set();
9361
+ /** A stream from the link: a connection to this server. */
9362
+ accept(stream, open3) {
9363
+ this.opens.set(stream, open3);
9364
+ this.server.emit("connection", stream);
9365
+ }
9366
+ openOf(req) {
9367
+ return this.opens.get(req.socket) ?? { kind: "http", session: null, grant: null };
9368
+ }
9369
+ /** Why `need` is not allowed for this stream now, or `null`. */
9370
+ refusal(need, open3) {
9371
+ const l = this.o.policy();
9372
+ if (need === "terminals" && !l.policy.terminals) {
9373
+ return {
9374
+ status: 403,
9375
+ body: { error: "terminals_off", message: l.problem ? `this machine's policy denies everything: ${l.problem}` : "terminals are off in this machine's policy.json" }
9376
+ };
9377
+ }
9378
+ if (!this.o.presence.check(open3.grant, open3.session)) {
9379
+ return { status: 401, body: { error: "presence", message: "this machine wants proof that you are there: the machines passphrase" } };
9380
+ }
9381
+ return null;
9382
+ }
9383
+ /** Close every channel the policy or presence no longer allows. Returns how many. */
9384
+ enforce() {
9385
+ let n = 0;
9386
+ for (const ch of [...this.channels]) {
9387
+ if (this.refusal(ch.need, ch.open) === null) continue;
9388
+ this.channels.delete(ch);
9389
+ ch.close();
9390
+ n += 1;
9391
+ }
9392
+ return n;
9393
+ }
9394
+ /** Track a channel for as long as `until` lasts; holds its grant from idling out. */
9395
+ channel(need, open3, close, onEnd) {
9396
+ const ch = { need, open: open3, close };
9397
+ this.channels.add(ch);
9398
+ const release = open3.grant ? this.o.presence.hold(open3.grant) : () => {
9399
+ };
9400
+ onEnd(() => {
9401
+ this.channels.delete(ch);
9402
+ release();
9403
+ });
9404
+ }
9405
+ get openChannels() {
9406
+ return this.channels.size;
9407
+ }
9408
+ async handle(req, res) {
9409
+ const url = new URL(req.url ?? "/", "http://machine");
9410
+ const open3 = this.openOf(req);
9411
+ const route = `${req.method} ${url.pathname}`;
9412
+ switch (route) {
9413
+ case "GET /api/health": {
9414
+ const hub = this.o.hub();
9415
+ const l = this.o.policy();
9416
+ return json(res, 200, {
9417
+ ok: true,
9418
+ herdr: { connected: hub?.connected ?? false, version: hub?.version ?? null, protocol: hub?.protocol ?? null },
9419
+ workspaceRoot: this.o.home,
9420
+ homeRoot: this.o.home,
9421
+ sharing: false,
9422
+ publicUrl: null,
9423
+ machine: { terminals: l.policy.terminals, problem: l.problem, presenceLocked: this.o.presence.locked() }
9424
+ });
9425
+ }
9426
+ case "POST /presence/challenge": {
9427
+ await readJson(req);
9428
+ const c = this.o.presence.challenge(open3.session);
9429
+ if (c === null) return json(res, 400, { error: "no_session", message: "no machines session" });
9430
+ if ("locked" in c) return json(res, 429, { error: "locked", until: c.until, message: lockedMessage(c.until) });
9431
+ return json(res, 200, c);
9432
+ }
9433
+ case "POST /presence/answer": {
9434
+ const b = await readJson(req);
9435
+ const r = this.o.presence.answer(open3.session, typeof b?.challenge === "string" ? b.challenge : "", typeof b?.answer === "string" ? b.answer : "");
9436
+ if (r.ok) return json(res, 200, { grant: r.grant, expiresAt: r.expiresAt });
9437
+ if (r.error === "locked") return json(res, 429, { error: "locked", until: r.until, message: lockedMessage(r.until) });
9438
+ if (r.error === "wrong") return json(res, 401, { error: "wrong", message: "That is not this machine's passphrase.", failures: r.failures });
9439
+ return json(res, 400, { error: r.error, message: "That challenge has expired; try again." });
9440
+ }
9441
+ case "GET /api/session":
9442
+ case "POST /api/rpc": {
9443
+ const no = this.refusal("terminals", open3);
9444
+ if (no) return json(res, no.status, no.body);
9445
+ const socket2 = this.o.herdrSocket();
9446
+ if (!socket2) return json(res, 503, { error: "herdr", message: "herdr is starting on this machine" });
9447
+ this.channel(
9448
+ "terminals",
9449
+ open3,
9450
+ () => res.destroy(),
9451
+ (cb) => res.once("close", cb)
9452
+ );
9453
+ if (route === "GET /api/session") {
9454
+ const hub = this.o.hub();
9455
+ if (!hub) return json(res, 503, { error: "herdr", message: "herdr is starting on this machine" });
9456
+ try {
9457
+ return json(res, 200, await hub.snapshot());
9458
+ } catch (err) {
9459
+ return json(res, 502, { error: "herdr", message: err.message });
9460
+ }
9461
+ }
9462
+ const b = await readJson(req);
9463
+ const method = b?.method;
9464
+ if (typeof method !== "string") return json(res, 400, { error: "method required" });
9465
+ if (!isAllowed(method)) return json(res, 403, { error: "method not allowed" });
9466
+ const params = typeof b?.params === "object" && b.params !== null ? b.params : {};
9467
+ try {
9468
+ return json(res, 200, { result: await request(socket2, method, params) });
9469
+ } catch (err) {
9470
+ if (err instanceof HerdrError) return json(res, 502, { error: { code: err.code, message: err.message } });
9471
+ return json(res, 502, { error: { code: "unreachable", message: err.message } });
9472
+ }
9473
+ }
9474
+ default:
9475
+ return json(res, 404, { error: "not found" });
9476
+ }
9477
+ }
9478
+ upgrade(req, socket2, head) {
9479
+ socket2.on("error", () => socket2.destroy());
9480
+ const url = new URL(req.url ?? "/", "http://machine");
9481
+ const open3 = this.openOf(req);
9482
+ if (url.pathname !== "/ws/terminal" && url.pathname !== "/ws/events") return refuseSocket(socket2, 404, { error: "not found" });
9483
+ const no = this.refusal("terminals", open3);
9484
+ if (no) return refuseSocket(socket2, no.status, no.body);
9485
+ const streams = this.o.streams();
9486
+ const hub = this.o.hub();
9487
+ if (!streams || !hub) return refuseSocket(socket2, 503, { error: "herdr", message: "herdr is starting on this machine" });
9488
+ this.wss.handleUpgrade(req, socket2, head, (ws) => {
9489
+ this.channel(
9490
+ "terminals",
9491
+ open3,
9492
+ () => ws.terminate(),
9493
+ (cb) => ws.once("close", cb)
9494
+ );
9495
+ if (url.pathname === "/ws/terminal") {
9496
+ serveTerminalSocket(ws, streams, { pane: url.searchParams.get("pane") ?? void 0, cols: url.searchParams.get("cols") ?? void 0, rows: url.searchParams.get("rows") ?? void 0 });
9497
+ return;
9498
+ }
9499
+ this.events(ws, hub);
9500
+ });
9501
+ }
9502
+ /** `/ws/events`: a fresh snapshot, then every hub message as it comes. */
9503
+ events(ws, hub) {
9504
+ heartbeat(ws);
9505
+ let off = null;
9506
+ const send = (m) => {
9507
+ if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(m));
9508
+ };
9509
+ hub.snapshot().then(
9510
+ (snapshot) => {
9511
+ if (ws.readyState !== ws.OPEN) return;
9512
+ send({ kind: "snapshot", snapshot });
9513
+ off = hub.on(send);
9514
+ },
9515
+ () => ws.close()
9516
+ );
9517
+ ws.on("close", () => off?.());
9518
+ }
9519
+ close() {
9520
+ for (const ch of [...this.channels]) ch.close();
9521
+ this.channels.clear();
9522
+ this.wss.close();
9523
+ this.server.close();
9524
+ }
9525
+ };
9526
+ function lockedMessage(until) {
9527
+ const mins = Math.max(1, Math.ceil((until - Date.now()) / 6e4));
9528
+ return `Too many wrong passphrases: this machine has locked presence for ${mins} more minute${mins === 1 ? "" : "s"}.`;
9529
+ }
9530
+
9531
+ // src/node/service-linux.ts
9532
+ import { execFile } from "node:child_process";
9533
+ import fs17 from "node:fs";
9534
+ import os9 from "node:os";
9535
+ import path15 from "node:path";
9536
+ var UNIT_NAME = "agentbox-node.service";
9537
+ var runCommand = (cmd, args) => new Promise((resolve) => {
9538
+ execFile(cmd, args, { timeout: 3e4 }, (err, stdout, stderr) => {
9539
+ const code2 = err ? typeof err.code === "number" ? err.code : 127 : 0;
9540
+ resolve({ code: code2, stdout: String(stdout), stderr: String(stderr) });
9541
+ });
9542
+ });
9543
+ function unitDir(env = process.env, home2 = os9.homedir()) {
9544
+ const xdg = env.XDG_CONFIG_HOME;
9545
+ return path15.join(xdg && path15.isAbsolute(xdg) ? xdg : path15.join(home2, ".config"), "systemd", "user");
9546
+ }
9547
+ function execStart(execPath = process.execPath, script = process.argv[1]) {
9548
+ const isSea = script === void 0 || path15.resolve(script) === path15.resolve(execPath);
9549
+ return isSea ? [execPath, "node", "run"] : [execPath, path15.resolve(script), "node", "run"];
9550
+ }
9551
+ function quote(arg) {
9552
+ return `"${arg.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/%/g, "%%")}"`;
9553
+ }
9554
+ function unitText(command2) {
9555
+ return [
9556
+ "[Unit]",
9557
+ "Description=agentbox node: this machine's link to its box",
9558
+ "After=network-online.target",
9559
+ "Wants=network-online.target",
9560
+ "",
9561
+ "[Service]",
9562
+ "Type=simple",
9563
+ `ExecStart=${command2.map(quote).join(" ")}`,
9564
+ "Restart=on-failure",
9565
+ "RestartSec=5",
9566
+ // herdr, and the agents in it, outlive a restart of the node (an
9567
+ // update, a crash); the next node adopts it.
9568
+ "KillMode=process",
9569
+ "",
9570
+ "[Install]",
9571
+ "WantedBy=default.target",
9572
+ ""
9573
+ ].join("\n");
9574
+ }
9575
+ async function installService(opts) {
9576
+ const run4 = opts.run ?? runCommand;
9577
+ const dir = unitDir(opts.env);
9578
+ fs17.mkdirSync(dir, { recursive: true });
9579
+ const unitFile = path15.join(dir, UNIT_NAME);
9580
+ fs17.writeFileSync(unitFile, unitText(opts.command), { mode: 420 });
9581
+ for (const args of [["--user", "daemon-reload"], ["--user", "enable", UNIT_NAME], ["--user", "restart", UNIT_NAME]]) {
9582
+ const r = await run4("systemctl", args);
9583
+ if (r.code !== 0) {
9584
+ throw new Error(`systemctl ${args.join(" ")} failed: ${(r.stderr || r.stdout).trim() || `exit ${r.code}`}`);
9585
+ }
9586
+ }
9587
+ const user = opts.user ?? os9.userInfo().username;
9588
+ const linger = await run4("loginctl", ["enable-linger", user]);
9589
+ if (linger.code === 0) return { unitFile, lingering: true, note: null };
9590
+ const cron = `@reboot ${opts.command.map((a) => `'${a.replace(/'/g, "'\\''")}'`).join(" ")}`;
9591
+ return {
9592
+ unitFile,
9593
+ lingering: false,
9594
+ note: `This machine does not let the node keep running after you log out (loginctl enable-linger was refused), so it runs while you are logged in. To start it at boot instead, add this line with \`crontab -e\`:
9595
+ ${cron}`
9596
+ };
9597
+ }
9598
+ async function removeService(opts = {}) {
9599
+ const run4 = opts.run ?? runCommand;
9600
+ const unitFile = path15.join(unitDir(opts.env), UNIT_NAME);
9601
+ if (!fs17.existsSync(unitFile)) return false;
9602
+ await run4("systemctl", ["--user", "disable", "--now", UNIT_NAME]);
9603
+ fs17.rmSync(unitFile, { force: true });
9604
+ await run4("systemctl", ["--user", "daemon-reload"]);
9605
+ return true;
9606
+ }
9607
+ function serviceInstalled(env = process.env) {
9608
+ return fs17.existsSync(path15.join(unitDir(env), UNIT_NAME));
9609
+ }
9610
+
9611
+ // src/node/specs.ts
9612
+ import { execFile as execFile2 } from "node:child_process";
9613
+ import fs18 from "node:fs";
9614
+ import os10 from "node:os";
9615
+ import path16 from "node:path";
9616
+ function run(cmd, args, timeoutMs = 3e3) {
9617
+ return new Promise((resolve) => {
9618
+ execFile2(cmd, args, { timeout: timeoutMs, windowsHide: true, maxBuffer: 256 * 1024 }, (err, stdout) => resolve(err ? null : stdout));
9619
+ });
9620
+ }
9621
+ function onPath(cmd, env) {
9622
+ const exts = process.platform === "win32" ? [".exe", ".cmd", ""] : [""];
9623
+ for (const dir of (env.PATH ?? "").split(path16.delimiter)) {
9624
+ for (const ext of exts) {
9625
+ try {
9626
+ fs18.accessSync(path16.join(dir, cmd + ext), fs18.constants.X_OK);
9627
+ return true;
9628
+ } catch {
9629
+ }
9630
+ }
9631
+ }
9632
+ return false;
9633
+ }
9634
+ async function gpus(env) {
9635
+ if (onPath("nvidia-smi", env)) {
9636
+ const out = await run("nvidia-smi", ["--query-gpu=name,memory.total", "--format=csv,noheader,nounits"]);
9637
+ if (out) {
9638
+ return out.split("\n").map((l) => l.split(",").map((s) => s.trim())).filter((p) => p[0]).map(([name, mem]) => ({ name, memoryMiB: Number(mem) || null }));
9639
+ }
9640
+ }
9641
+ if (onPath("rocm-smi", env)) {
9642
+ const out = await run("rocm-smi", ["--showproductname", "--csv"]);
9643
+ if (out) {
9644
+ return out.split("\n").slice(1).map((l) => l.split(",")[2]?.trim() ?? "").filter(Boolean).map((name) => ({ name, memoryMiB: null }));
9645
+ }
9646
+ }
9647
+ return [];
9648
+ }
9649
+ function freeBytes(dir) {
9650
+ try {
9651
+ const s = fs18.statfsSync(dir);
9652
+ return s.bavail * s.bsize;
9653
+ } catch {
9654
+ return null;
9655
+ }
9656
+ }
9657
+ async function claudeVersion(binDir, env) {
9658
+ const own = path16.join(binDir, process.platform === "win32" ? "claude.exe" : "claude");
9659
+ const cmd = fs18.existsSync(own) ? own : onPath("claude", env) ? "claude" : null;
9660
+ if (!cmd) return null;
9661
+ const out = await run(cmd, ["--version"], 5e3);
9662
+ return out ? /\d+\.\d+\.\d+/.exec(out)?.[0] ?? null : null;
9663
+ }
9664
+ async function gatherSpecs(opts) {
9665
+ const env = opts.env ?? process.env;
9666
+ const cpus = os10.cpus();
9667
+ const disks = [];
9668
+ for (const f of opts.policy.files) {
9669
+ const free = freeBytes(f.path);
9670
+ if (free !== null) disks.push({ path: f.path, freeBytes: free });
9671
+ }
9672
+ return {
9673
+ platform: process.platform,
9674
+ arch: process.arch,
9675
+ hostname: os10.hostname(),
9676
+ cpu: { model: cpus[0]?.model ?? "", cores: cpus.length },
9677
+ ramBytes: os10.totalmem(),
9678
+ disks,
9679
+ gpus: await gpus(env),
9680
+ docker: onPath("docker", env),
9681
+ claude: await claudeVersion(opts.binDir, env)
9682
+ };
9683
+ }
9684
+
9685
+ // src/node/run.ts
9686
+ var RESTART_EXIT = 75;
9687
+ var HEADLESS = { cols: 200, rows: 50, ms: 3e4 };
9688
+ async function runNode(o) {
9689
+ const now = o.now ?? Date.now;
9690
+ const creds = readCredentials(o.paths.credentials);
9691
+ if (!creds) throw new CliError("this machine has not joined a box; run `agentbox node join <box>`", EXIT.NOT_FOUND);
9692
+ if (creds.revokedAt !== void 0) {
9693
+ o.log(`this machine was revoked by ${creds.box} on ${new Date(creds.revokedAt).toISOString()}; re-join it to use it again`);
9694
+ return EXIT.OK;
9695
+ }
9696
+ if (await callControl(o.paths.control, o.paths.controlSecret, "status", 2e3) !== null) {
9697
+ throw new CliError("a node is already running on this machine (see `agentbox node status`)");
9698
+ }
9699
+ let policy = loadPolicy(o.paths.policy, o.home);
9700
+ const claude = claudePaths(o.paths);
9701
+ prepareClaude(claude, { home: o.home, machine: creds.name });
9702
+ const herdrEnv = { ...process.env, PATH: [claude.shims, o.paths.bin, process.env.PATH ?? ""].join(path17.delimiter) };
9703
+ delete herdrEnv.CLAUDE_CODE_OAUTH_TOKEN;
9704
+ const presence = new PresenceGate({ presence: creds.presence, machineId: creds.machineId, hours: () => policy.policy.presenceHours, now, log: o.log });
9705
+ let hub = null;
9706
+ let streams = null;
9707
+ const herdrBin = path17.join(o.paths.bin, process.platform === "win32" ? "herdr.exe" : "herdr");
9708
+ const herdr = fs19.existsSync(herdrBin) ? new HerdrSupervisor({ bin: herdrBin, dir: o.paths.dir, env: herdrEnv }) : null;
9709
+ if (!herdr) o.log(`herdr is not installed (${herdrBin}); terminals and agents are unavailable until \`agentbox node join\` installs it`);
9710
+ const server = new NodeServer({
9711
+ herdrSocket: () => herdr?.socketPath ?? null,
9712
+ hub: () => hub,
9713
+ streams: () => streams,
9714
+ policy: () => policy,
9715
+ presence,
9716
+ home: o.home,
9717
+ log: o.log
9718
+ });
9719
+ herdr?.on("log", (line) => o.log(line));
9720
+ herdr?.on("down", () => {
9721
+ hub?.stop();
9722
+ hub = null;
9723
+ streams?.stop();
9724
+ streams = null;
9725
+ });
9726
+ herdr?.on("ready", (socket2, info) => {
9727
+ hub?.stop();
9728
+ streams?.stop();
9729
+ const h = new SessionHub(socket2);
9730
+ hub = h;
9731
+ streams = new TerminalStreams({ ...herdrEnv, HERDR_SOCKET_PATH: socket2, HERDR_SESSION });
9732
+ const own = streams;
9733
+ const connect = (tries) => {
9734
+ h.start().then(
9735
+ async () => {
9736
+ if (!info.restored || hub !== h) return;
9737
+ const snap = await h.snapshot().catch(() => null);
9738
+ const quiet = { send: () => {
9739
+ }, sendJson: () => {
9740
+ }, close: () => {
9741
+ } };
9742
+ const held = (snap?.panes ?? []).map((p) => own.attach(p.pane_id, quiet, HEADLESS.cols, HEADLESS.rows));
9743
+ if (held.length) o.log(`herdr: ${held.length} restored pane(s) attached headless at ${HEADLESS.cols}\xD7${HEADLESS.rows}`);
9744
+ setTimeout(() => held.forEach((a) => a.detach()), HEADLESS.ms).unref();
9745
+ },
9746
+ () => {
9747
+ if (hub === h && tries > 0) setTimeout(() => connect(tries - 1), 1e3).unref();
9748
+ }
9749
+ );
9750
+ };
9751
+ connect(10);
9752
+ });
9753
+ const sayPolicy = (l) => {
9754
+ if (l.problem) o.log(`policy: ${l.problem}; everything is denied until it is fixed`);
9755
+ else o.log(`policy: terminals ${l.policy.terminals ? "on" : "off"}, ${l.policy.files.length} folder(s), ports ${l.policy.ports ? "on" : "off"}${l.policy.paused ? ", paused" : ""}`);
9756
+ };
9757
+ sayPolicy(policy);
9758
+ const link = new NodeLink({
9759
+ box: creds.box,
9760
+ token: creds.token,
9761
+ machineId: creds.machineId,
9762
+ version: o.version,
9763
+ describe: async () => {
9764
+ const summary = summarize(policy);
9765
+ return { ...await gatherSpecs({ binDir: o.paths.bin, policy: summary }), policy: summary };
9766
+ },
9767
+ ...o.link ?? {}
9768
+ });
9769
+ link.on("log", (line) => o.log(line));
9770
+ link.on("stream", (stream, open3) => server.accept(stream, open3));
9771
+ link.on("control", (m) => {
9772
+ if (m.type !== "claude-token") return;
9773
+ const token = typeof m.token === "string" ? m.token : null;
9774
+ holdToken(claude, token);
9775
+ o.log(token ? "the box sent the shared Claude sign-in; new Claude Code sessions here use it" : "the box withdrew the shared Claude sign-in from this machine");
9776
+ });
9777
+ link.on("state", (state) => o.log(`link: ${state}`));
9778
+ if (policy.policy.paused) link.pause();
9779
+ const stopWatching = watchPolicy(o.paths.policy, o.home, (l) => {
9780
+ policy = l;
9781
+ sayPolicy(l);
9782
+ const closed = server.enforce();
9783
+ if (closed) o.log(`policy: closed ${closed} channel(s) it no longer allows`);
9784
+ if (l.policy.paused && !link.isPaused) link.pause();
9785
+ else if (!l.policy.paused && link.isPaused) link.resume();
9786
+ else link.sendPolicy(summarize(l));
9787
+ });
9788
+ let finish = () => {
9789
+ };
9790
+ const done = new Promise((r) => finish = r);
9791
+ const control2 = await startControl(o.paths.control, o.paths.controlSecret, async (command2) => {
9792
+ switch (command2) {
9793
+ case "status":
9794
+ return {
9795
+ state: link.state,
9796
+ box: creds.box,
9797
+ machineId: creds.machineId,
9798
+ name: creds.name,
9799
+ paused: link.isPaused,
9800
+ policyProblem: policy.problem,
9801
+ pid: process.pid,
9802
+ herdrSocket: herdr?.socketPath ?? null,
9803
+ presenceLocked: presence.locked(),
9804
+ channels: server.openChannels
9805
+ };
9806
+ case "pause":
9807
+ setPaused(o.paths.policy, true);
9808
+ link.pause();
9809
+ return { state: link.state };
9810
+ case "resume":
9811
+ setPaused(o.paths.policy, false);
9812
+ link.resume();
9813
+ return { state: link.state };
9814
+ case "leave": {
9815
+ const told = await link.leave();
9816
+ setImmediate(() => finish(EXIT.OK));
9817
+ return { told };
9818
+ }
9819
+ }
9820
+ });
9821
+ link.on("revoked", () => {
9822
+ fs19.rmSync(o.paths.secrets, { recursive: true, force: true });
9823
+ writeCredentials(o.paths.credentials, { ...creds, revokedAt: now() });
9824
+ o.log(`${creds.box} revoked this machine. Its shared secrets are wiped; everything else is kept. Re-join it to use it again.`);
9825
+ finish(EXIT.OK);
9826
+ });
9827
+ link.on("state", (state) => {
9828
+ if (state === "left") finish(EXIT.OK);
9829
+ });
9830
+ o.signal.addEventListener("abort", () => finish(EXIT.OK), { once: true });
9831
+ let checking = false;
9832
+ const checkUpdates = async () => {
9833
+ if (checking || o.checkUpdates === false) return;
9834
+ checking = true;
9835
+ try {
9836
+ const herdrPath = herdrBin;
9837
+ const report = await checkForUpdate({
9838
+ policy: policy.policy.updates,
9839
+ running: o.version,
9840
+ // Only the standalone executable replaces itself; agentbox.mjs under Node does not.
9841
+ nodePath: execStart().length === 3 ? process.execPath : null,
9842
+ ...fs19.existsSync(herdrPath) ? { herdr: { path: herdrPath, version: HERDR_VERSION } } : {},
9843
+ ...installedClaude(claude) ? { claude: { path: claude.bin, version: installedClaude(claude), versionFile: claude.versionFile } } : {},
9844
+ // Agents live in herdr: herdr and Claude Code are replaced only while none runs.
9845
+ idle: () => herdr ? herdr.idle() : true
9846
+ });
9847
+ o.log(`update: ${describeReport(report)}`);
9848
+ if ((report.state === "current" || report.state === "updated") && report.tools.herdr === "updated") void herdr?.restart("updated");
9849
+ if (report.state === "updated" && report.restart) finish(RESTART_EXIT);
9850
+ } catch (err) {
9851
+ o.log(`update check failed: ${err.message}`);
9852
+ } finally {
9853
+ checking = false;
9854
+ }
9855
+ };
9856
+ link.on("upgrade", () => void checkUpdates());
9857
+ const firstCheck = setTimeout(() => void checkUpdates(), 10 * 6e4);
9858
+ firstCheck.unref();
9859
+ const daily = setInterval(() => void checkUpdates(), 24 * 60 * 6e4);
9860
+ daily.unref();
9861
+ const enforcer = setInterval(() => server.enforce(), o.enforceMs ?? 5e3);
9862
+ enforcer.unref();
9863
+ if (herdr) {
9864
+ herdr.start().catch((err) => {
9865
+ o.log(`herdr did not start: ${err.message}; trying again`);
9866
+ void herdr.restart("it did not start");
9867
+ });
9868
+ }
9869
+ link.start();
9870
+ const code2 = await done;
9871
+ clearInterval(enforcer);
9872
+ server.close();
9873
+ herdr?.stop();
9874
+ hub?.stop();
9875
+ streams?.stop();
9876
+ clearTimeout(firstCheck);
9877
+ clearInterval(daily);
9878
+ link.stop(link.state === "revoked" ? "revoked" : "stopped");
9879
+ stopWatching();
9880
+ control2.close();
9881
+ return code2;
9882
+ }
9883
+
9884
+ // src/commands/node.ts
9885
+ function paths(ctx) {
9886
+ return nodePaths(nodeDir(ctx.env, ctx.env.HOME || os11.homedir(), ctx.platform));
9887
+ }
9888
+ function home(ctx) {
9889
+ return ctx.env.HOME || os11.homedir();
9890
+ }
9891
+ function parseFolders(raw, homeDir2) {
9892
+ const out = [];
9893
+ for (const part of raw.split(",").map((s) => s.trim()).filter(Boolean)) {
9894
+ const m = /^(.*?)(?::(read|write))?$/.exec(part);
9895
+ let p = m[1];
9896
+ if (p === "~") p = homeDir2;
9897
+ else if (p.startsWith("~/")) p = path18.join(homeDir2, p.slice(2));
9898
+ p = path18.resolve(p);
9899
+ if (p === path18.resolve(homeDir2)) throw new UsageError("not the whole home folder: name the project folders the box may use");
9900
+ out.push({ path: p, mode: m[2] ?? "write" });
9901
+ }
9902
+ return out;
9903
+ }
9904
+ function printPolicy(ctx, p) {
9905
+ try {
9906
+ ctx.err(`${fs20.readFileSync(p.policy, "utf8")}`);
9907
+ } catch {
9908
+ ctx.err("(no policy.json)\n");
9909
+ }
9910
+ }
9911
+ var join = command({
9912
+ path: ["node", "join"],
9913
+ summary: "join this computer to a box as a machine",
9914
+ usage: "<box-url>",
9915
+ operands: { min: 1, max: 1 },
9916
+ details: "Installs what the node runs (verified), asks for the machines passphrase here, shows a code to type\non the box's machines Admin page, writes policy.json if there is none, and starts the node as a\nsystemd user service.",
9917
+ options: [
9918
+ { name: "name", type: "string", value: "name", description: "what the box calls this machine (default: the hostname)" },
9919
+ { name: "folder", type: "string", value: "dirs", description: "folders the box may use, comma-separated, each dir or dir:read (asked when absent)" },
9920
+ { name: "no-service", type: "boolean", description: "do not register a service; run `agentbox node run` yourself" },
9921
+ { name: "no-claude", type: "boolean", description: "do not install Claude Code (about 280 MiB); agents can be started once it is there" }
9922
+ ],
9923
+ async run(ctx, p) {
9924
+ const box = normalizeBoxUrl(p.operands[0]);
9925
+ const np = paths(ctx);
9926
+ const prompt = new Prompter(ctx.io.stdin, ctx.io.stderr);
9927
+ const folderArg = str(p.options, "folder");
9928
+ const noService = bool(p.options, "no-service") || ctx.platform !== "linux";
9929
+ try {
9930
+ await joinMachine(
9931
+ { box, name: str(p.options, "name") ?? os11.hostname(), folders: folderArg !== void 0 ? parseFolders(folderArg, home(ctx)) : null },
9932
+ {
9933
+ paths: np,
9934
+ platform: ctx.platform,
9935
+ arch: process.arch,
9936
+ hostname: os11.hostname(),
9937
+ now: ctx.now,
9938
+ err: (s) => ctx.err(s),
9939
+ client: ctx.client(box, null),
9940
+ freeBytes,
9941
+ install: async () => {
9942
+ await installHerdr(np.bin, { platform: ctx.platform, arch: process.arch, from: ctx.env.AGENTBOX_NODE_HERDR_FROM || void 0, log: (s) => ctx.err(s) });
9943
+ if (!bool(p.options, "no-claude")) await installClaude(claudePaths(np), { platform: ctx.platform, arch: process.arch, log: (s) => ctx.err(s) });
9944
+ },
9945
+ passphrase: () => askPassphrase(prompt, (s) => ctx.err(s)),
9946
+ enrol: (pass) => enrol(pass, ctx.now()),
9947
+ sleep: (ms) => sleep(ms, ctx.abort.signal),
9948
+ askFolders: async () => {
9949
+ ctx.err("Which project folders may the box use? (comma-separated; add :read for read-only; never your whole home)\n");
9950
+ const answer = await prompt.line("Folders: ");
9951
+ return answer ? parseFolders(answer, home(ctx)) : [];
9952
+ },
9953
+ service: noService ? null : async () => {
9954
+ try {
9955
+ const r = await installService({ command: execStart(), env: ctx.env });
9956
+ ctx.err(`Registered ${UNIT_NAME} (${r.unitFile}).
9957
+ `);
9958
+ if (r.note) ctx.err(`${r.note}
9959
+ `);
9960
+ } catch (err) {
9961
+ throw new CliError(`${err.message}
9962
+ The machine is joined; start it with \`agentbox node run\`.`);
9963
+ }
9964
+ },
9965
+ waitOnline: async () => {
9966
+ const deadline = Date.now() + 6e4;
9967
+ while (Date.now() < deadline) {
9968
+ const s = await callControl(np.control, np.controlSecret, "status", 2e3);
9969
+ if (s?.state === "online") return true;
9970
+ await sleep(1e3, ctx.abort.signal);
9971
+ }
9972
+ return false;
9973
+ },
9974
+ printPolicy: () => printPolicy(ctx, np)
9975
+ }
9976
+ );
9977
+ } finally {
9978
+ prompt.close();
9979
+ }
9980
+ }
9981
+ });
9982
+ var run2 = command({
9983
+ path: ["node", "run"],
9984
+ summary: "run the node in the foreground (the service runs this)",
9985
+ usage: "",
9986
+ ownsInterrupt: true,
9987
+ async run(ctx) {
9988
+ const np = paths(ctx);
9989
+ const log = (line) => {
9990
+ ctx.err(`${new Date(ctx.now()).toISOString()} ${line}
9991
+ `);
9992
+ };
9993
+ return runNode({ paths: np, home: home(ctx), version: VERSION, log, signal: ctx.abort.signal });
9994
+ }
9995
+ });
9996
+ var status2 = command({
9997
+ path: ["node", "status"],
9998
+ summary: "whether this machine is joined, linked and paused",
9999
+ usage: "",
10000
+ json: true,
10001
+ async run(ctx) {
10002
+ const np = paths(ctx);
10003
+ const creds = readCredentials(np.credentials);
10004
+ const live = await callControl(np.control, np.controlSecret, "status", 3e3);
10005
+ const policy = loadPolicy(np.policy, home(ctx));
10006
+ const out = {
10007
+ joined: creds !== null,
10008
+ box: creds?.box ?? null,
10009
+ machineId: creds?.machineId ?? null,
10010
+ name: creds?.name ?? null,
10011
+ revoked: creds?.revokedAt !== void 0 && creds !== null,
10012
+ running: live !== null,
10013
+ state: live ? live.state : creds ? creds.revokedAt !== void 0 ? "revoked" : "not running" : "not joined",
10014
+ paused: policy.policy.paused,
10015
+ policyProblem: policy.problem,
10016
+ folder: np.dir,
10017
+ service: ctx.platform === "linux" ? serviceInstalled(ctx.env) : false,
10018
+ herdrSocket: typeof live?.herdrSocket === "string" ? live.herdrSocket : null,
10019
+ presenceLocked: typeof live?.presenceLocked === "number" ? live.presenceLocked : null
10020
+ };
10021
+ if (ctx.json) return ctx.printJson(out);
10022
+ if (!creds) {
10023
+ ctx.out(`Not joined to a box. Run \`agentbox node join <box>\`.
10024
+ `);
10025
+ return EXIT.NOT_FOUND;
10026
+ }
10027
+ ctx.out(`${safeText(out.name ?? "")} (machine ${safeText(out.machineId ?? "")}) at ${safeText(out.box ?? "")}
10028
+ `);
10029
+ ctx.out(`state: ${safeText(String(out.state))}${out.paused ? " (paused: `agentbox node resume`)" : ""}
10030
+ `);
10031
+ if (out.policyProblem) ctx.out(`policy: ${safeText(out.policyProblem)} \u2014 everything is denied
10032
+ `);
10033
+ ctx.out(`folder: ${np.dir}
10034
+ service: ${out.service ? UNIT_NAME : "none"}
10035
+ `);
10036
+ if (out.herdrSocket) ctx.out(`herdr: HERDR_SOCKET_PATH=${out.herdrSocket} herdr (the agents the box sees)
10037
+ `);
10038
+ if (out.presenceLocked) ctx.out(`presence: locked until ${new Date(out.presenceLocked).toISOString()} after wrong passphrases
10039
+ `);
10040
+ }
10041
+ });
10042
+ async function setPause(ctx, paused) {
10043
+ const np = paths(ctx);
10044
+ if (!readCredentials(np.credentials)) throw new CliError("this machine has not joined a box", EXIT.NOT_FOUND);
10045
+ const live = await callControl(np.control, np.controlSecret, paused ? "pause" : "resume");
10046
+ if (!live) setPaused(np.policy, paused);
10047
+ else if (live.ok !== true) throw new CliError(String(live.error ?? "the node refused"));
10048
+ ctx.err(paused ? "Paused: the link is down until `agentbox node resume`.\n" : "Resumed.\n");
10049
+ }
10050
+ var pause = command({ path: ["node", "pause"], summary: "take the link down until resume", usage: "", run: (ctx) => setPause(ctx, true) });
10051
+ var resume = command({ path: ["node", "resume"], summary: "bring the link back up", usage: "", run: (ctx) => setPause(ctx, false) });
10052
+ var leave = command({
10053
+ path: ["node", "leave"],
10054
+ summary: "leave the box: tell it if it can be reached, and forget this machine's token",
10055
+ usage: "",
10056
+ async run(ctx) {
10057
+ const np = paths(ctx);
10058
+ const creds = readCredentials(np.credentials);
10059
+ if (!creds) throw new CliError("this machine has not joined a box", EXIT.NOT_FOUND);
10060
+ let told = false;
10061
+ const live = await callControl(np.control, np.controlSecret, "leave", 1e4);
10062
+ if (live) told = live.told === true;
10063
+ else if (creds.revokedAt === void 0) {
10064
+ const link = new NodeLink({
10065
+ box: creds.box,
10066
+ token: creds.token,
10067
+ machineId: creds.machineId,
10068
+ version: VERSION,
10069
+ describe: async () => {
10070
+ const policy = summarize(loadPolicy(np.policy, home(ctx)));
10071
+ return { ...await gatherSpecs({ binDir: np.bin, policy }), policy };
10072
+ }
10073
+ });
10074
+ link.start();
10075
+ const deadline = Date.now() + 1e4;
10076
+ while (link.state !== "online" && Date.now() < deadline) await sleep(200);
10077
+ told = await link.leave();
10078
+ link.stop();
10079
+ }
10080
+ if (ctx.platform === "linux") await removeService({ env: ctx.env });
10081
+ fs20.rmSync(np.secrets, { recursive: true, force: true });
10082
+ fs20.rmSync(np.credentials, { force: true });
10083
+ fs20.rmSync(np.controlSecret, { force: true });
10084
+ ctx.err(told ? `Left ${creds.box}.
10085
+ ` : `Left here; ${creds.box} could not be told, so it lists this machine until the owner removes it.
10086
+ `);
10087
+ ctx.err(`Kept ${np.policy} and ${np.bin}.
10088
+ `);
10089
+ }
10090
+ });
10091
+ var logs = command({
10092
+ path: ["node", "logs"],
10093
+ summary: "the node's log (from the journal)",
10094
+ usage: "",
10095
+ ownsInterrupt: true,
10096
+ options: [{ name: "follow", short: "f", type: "boolean", description: "keep printing new lines" }],
10097
+ async run(ctx, p) {
10098
+ const args = ["--user", "-u", UNIT_NAME, "-n", "200", "--no-pager", ...bool(p.options, "follow") ? ["-f"] : []];
10099
+ return new Promise((resolve) => {
10100
+ const child = spawn7("journalctl", args, { stdio: "inherit" });
10101
+ child.on("error", () => {
10102
+ ctx.err("journalctl is not available here; run `agentbox node run` in a terminal to see the log.\n");
10103
+ resolve(EXIT.FAILURE);
10104
+ });
10105
+ child.on("exit", (code2) => resolve(code2 ?? EXIT.OK));
10106
+ ctx.abort.signal.addEventListener("abort", () => child.kill("SIGINT"), { once: true });
10107
+ });
10108
+ }
10109
+ });
10110
+ var passphrase = command({
10111
+ path: ["node", "passphrase"],
10112
+ summary: "set this machine's copy of the machines passphrase (after it changed on the box)",
10113
+ usage: "",
10114
+ async run(ctx) {
10115
+ const np = paths(ctx);
10116
+ const creds = readCredentials(np.credentials);
10117
+ if (!creds) throw new CliError("this machine has not joined a box", EXIT.NOT_FOUND);
10118
+ const prompt = new Prompter(ctx.io.stdin, ctx.io.stderr);
10119
+ try {
10120
+ const presence = await enrol(await askPassphrase(prompt, (s) => ctx.err(s)), ctx.now());
10121
+ writeCredentials(np.credentials, { ...creds, presence });
10122
+ } finally {
10123
+ prompt.close();
10124
+ }
10125
+ ctx.err("This machine now proves presence with the new passphrase.\n");
10126
+ }
10127
+ });
10128
+ var NODE_COMMANDS = [join, run2, status2, pause, resume, leave, logs, passphrase];
10129
+
6808
10130
  // src/commands/mount.ts
6809
- import fs8 from "node:fs";
6810
- import os7 from "node:os";
6811
- import path9 from "node:path";
10131
+ import fs21 from "node:fs";
10132
+ import os12 from "node:os";
10133
+ import path19 from "node:path";
6812
10134
 
6813
10135
  // src/dav.ts
6814
- import { randomBytes as randomBytes3, timingSafeEqual } from "node:crypto";
6815
- import http from "node:http";
10136
+ import { randomBytes as randomBytes8, timingSafeEqual as timingSafeEqual4 } from "node:crypto";
10137
+ import http2 from "node:http";
6816
10138
  var REMOTE_PREFIX = "/api/dav";
6817
10139
  var BadPath = class extends Error {
6818
10140
  };
@@ -6858,7 +10180,7 @@ function underPrefix(p, prefix) {
6858
10180
  if (p.length < prefix.length) return false;
6859
10181
  const head = Buffer.from(p.slice(0, prefix.length));
6860
10182
  const want = Buffer.from(prefix);
6861
- if (head.length !== want.length || !timingSafeEqual(head, want)) return false;
10183
+ if (head.length !== want.length || !timingSafeEqual4(head, want)) return false;
6862
10184
  return p.length === prefix.length || p[prefix.length] === "/";
6863
10185
  }
6864
10186
  var LOOPBACK_HOSTS = ["127.0.0.1", "localhost", "[::1]"];
@@ -6920,20 +10242,20 @@ var DROP_REQUEST = /* @__PURE__ */ new Set(["host", "authorization", "proxy-auth
6920
10242
  var DROP_RESPONSE = /* @__PURE__ */ new Set(["set-cookie", "www-authenticate", "strict-transport-security", "alt-svc"]);
6921
10243
  var MAX_REWRITE = 64 * 1024 * 1024;
6922
10244
  function newCredentials() {
6923
- return { user: "agentbox", password: randomBytes3(24).toString("base64url") };
10245
+ return { user: "agentbox", password: randomBytes8(24).toString("base64url") };
6924
10246
  }
6925
10247
  function basicMatches(header, want) {
6926
10248
  const m = /^Basic\s+([A-Za-z0-9+/=]+)\s*$/i.exec(header ?? "");
6927
10249
  if (!m) return false;
6928
10250
  const got = Buffer.from(m[1], "base64");
6929
10251
  const expected = Buffer.from(`${want.user}:${want.password}`, "utf8");
6930
- return got.length === expected.length && timingSafeEqual(got, expected);
10252
+ return got.length === expected.length && timingSafeEqual4(got, expected);
6931
10253
  }
6932
10254
  var DavFront = class {
6933
10255
  constructor(client, opts = {}) {
6934
10256
  this.client = client;
6935
10257
  this.opts = opts;
6936
- this.secret = opts.secret ?? randomBytes3(16).toString("base64url");
10258
+ this.secret = opts.secret ?? randomBytes8(16).toString("base64url");
6937
10259
  this.credentials = opts.credentials === void 0 ? newCredentials() : opts.credentials;
6938
10260
  }
6939
10261
  client;
@@ -6947,7 +10269,7 @@ var DavFront = class {
6947
10269
  credentials;
6948
10270
  /** Start listening; resolves with the URL to give a WebDAV client. */
6949
10271
  async start() {
6950
- const server = http.createServer((req, res) => {
10272
+ const server = http2.createServer((req, res) => {
6951
10273
  this.handle(req, res).catch((err) => {
6952
10274
  this.opts.onError?.(err.message);
6953
10275
  if (!res.headersSent) {
@@ -6990,8 +10312,8 @@ var DavFront = class {
6990
10312
  }
6991
10313
  async handle(req, res) {
6992
10314
  const m = this.map;
6993
- const plain = (status2, text) => {
6994
- res.writeHead(status2, { "content-type": "text/plain; charset=utf-8", "content-length": String(Buffer.byteLength(text) + 1) });
10315
+ const plain = (status3, text) => {
10316
+ res.writeHead(status3, { "content-type": "text/plain; charset=utf-8", "content-length": String(Buffer.byteLength(text) + 1) });
6995
10317
  res.end(`${text}
6996
10318
  `);
6997
10319
  };
@@ -7070,7 +10392,7 @@ var DavFront = class {
7070
10392
  };
7071
10393
 
7072
10394
  // src/http.ts
7073
- import http2 from "node:http";
10395
+ import http3 from "node:http";
7074
10396
  import https from "node:https";
7075
10397
  import { Readable } from "node:stream";
7076
10398
  var USER_AGENT = `agentbox-cli/${VERSION} (node ${process.versions.node}; ${process.platform})`;
@@ -7116,7 +10438,7 @@ function encodeQuery(query, rawQuery) {
7116
10438
  if (rawQuery) parts.push(rawQuery);
7117
10439
  return parts.length ? `?${parts.join("&")}` : "";
7118
10440
  }
7119
- function apiErrorFrom(status2, text, what) {
10441
+ function apiErrorFrom(status3, text, what) {
7120
10442
  let message = "";
7121
10443
  let code2 = null;
7122
10444
  try {
@@ -7129,20 +10451,20 @@ function apiErrorFrom(status2, text, what) {
7129
10451
  message = text.trim().split("\n")[0] ?? "";
7130
10452
  }
7131
10453
  message = safeText(message.slice(0, 300));
7132
- if (status2 === 401) {
10454
+ if (status3 === 401) {
7133
10455
  return new ApiError(401, "the box did not accept this device's sign-in (the token was revoked or has expired); run `agentbox login` again", code2);
7134
10456
  }
7135
- if (status2 === 502 || status2 === 503 || status2 === 504) {
10457
+ if (status3 === 502 || status3 === 503 || status3 === 504) {
7136
10458
  message = message || "the box is up, but the service behind it is not answering";
7137
10459
  }
7138
- return new ApiError(status2, `${what}: ${message || `HTTP ${status2}`}`, code2);
10460
+ return new ApiError(status3, `${what}: ${message || `HTTP ${status3}`}`, code2);
7139
10461
  }
7140
10462
  var BoxClient = class {
7141
10463
  constructor(origin, token) {
7142
10464
  this.token = token;
7143
10465
  this.url = new URL(origin);
7144
10466
  this.origin = this.url.origin;
7145
- const Agent = this.url.protocol === "https:" ? https.Agent : http2.Agent;
10467
+ const Agent = this.url.protocol === "https:" ? https.Agent : http3.Agent;
7146
10468
  this.agent = new Agent({ keepAlive: true, maxSockets: 8 });
7147
10469
  }
7148
10470
  token;
@@ -7165,7 +10487,7 @@ var BoxClient = class {
7165
10487
  get hasToken() {
7166
10488
  return this.token !== null;
7167
10489
  }
7168
- request(method, path12, opts = {}) {
10490
+ request(method, path22, opts = {}) {
7169
10491
  const headers = { ...this.baseHeaders(opts.auth !== false), ...opts.headers ?? {} };
7170
10492
  let payload = null;
7171
10493
  const body = opts.body;
@@ -7180,8 +10502,8 @@ var BoxClient = class {
7180
10502
  }
7181
10503
  if (Buffer.isBuffer(payload)) headers["content-length"] = String(payload.length);
7182
10504
  }
7183
- const target = `${path12}${encodeQuery(opts.query, opts.rawQuery)}`;
7184
- const lib = this.url.protocol === "https:" ? https : http2;
10505
+ const target = `${path22}${encodeQuery(opts.query, opts.rawQuery)}`;
10506
+ const lib = this.url.protocol === "https:" ? https : http3;
7185
10507
  return new Promise((resolve, reject) => {
7186
10508
  const req = lib.request(
7187
10509
  {
@@ -7226,21 +10548,21 @@ var BoxClient = class {
7226
10548
  * A JSON call: the parsed answer on 2xx, else an {@link ApiError} with what
7227
10549
  * the box said. `what` names the action for the message ("listing /x").
7228
10550
  */
7229
- async json(method, path12, opts = {}) {
7230
- const res = await this.request(method, path12, { ...opts, headers: { accept: "application/json", ...opts.headers ?? {} } });
10551
+ async json(method, path22, opts = {}) {
10552
+ const res = await this.request(method, path22, { ...opts, headers: { accept: "application/json", ...opts.headers ?? {} } });
7231
10553
  const text = await res.text();
7232
- if (res.status < 200 || res.status >= 300) throw apiErrorFrom(res.status, text, opts.what ?? `${method} ${path12}`);
10554
+ if (res.status < 200 || res.status >= 300) throw apiErrorFrom(res.status, text, opts.what ?? `${method} ${path22}`);
7233
10555
  if (res.status === 204 || text === "") return void 0;
7234
10556
  try {
7235
10557
  return JSON.parse(text);
7236
10558
  } catch {
7237
- throw new CliError(`${opts.what ?? path12}: the box answered with something that is not JSON (is ${this.origin} an agentbox?)`);
10559
+ throw new CliError(`${opts.what ?? path22}: the box answered with something that is not JSON (is ${this.origin} an agentbox?)`);
7238
10560
  }
7239
10561
  }
7240
10562
  /** A request whose answer must be 2xx; the body is left to the caller. */
7241
- async ok(method, path12, opts = {}) {
7242
- const res = await this.request(method, path12, opts);
7243
- if (res.status < 200 || res.status >= 300) throw apiErrorFrom(res.status, await res.text(), opts.what ?? `${method} ${path12}`);
10563
+ async ok(method, path22, opts = {}) {
10564
+ const res = await this.request(method, path22, opts);
10565
+ if (res.status < 200 || res.status >= 300) throw apiErrorFrom(res.status, await res.text(), opts.what ?? `${method} ${path22}`);
7244
10566
  return res;
7245
10567
  }
7246
10568
  close() {
@@ -7254,11 +10576,11 @@ async function gvfsFolder(root, port, secret, ms) {
7254
10576
  for (; ; ) {
7255
10577
  let names = [];
7256
10578
  try {
7257
- names = fs8.readdirSync(root);
10579
+ names = fs21.readdirSync(root);
7258
10580
  } catch {
7259
10581
  }
7260
10582
  const name = gvfsMountName(names, port, secret);
7261
- if (name) return path9.join(root, name);
10583
+ if (name) return path19.join(root, name);
7262
10584
  if (Date.now() > deadline) return null;
7263
10585
  await new Promise((r) => setTimeout(r, 100));
7264
10586
  }
@@ -7318,7 +10640,7 @@ password ${credentials.password}
7318
10640
  }
7319
10641
  let plan;
7320
10642
  try {
7321
- plan = mountPlan({ platform: ctx.platform, url, dir, boxName: name, home: os7.homedir(), env: ctx.env, credentials });
10643
+ plan = mountPlan({ platform: ctx.platform, url, dir, boxName: name, home: os12.homedir(), env: ctx.env, credentials });
7322
10644
  } catch (err) {
7323
10645
  throw new CliError(err.message, EXIT.USAGE);
7324
10646
  }
@@ -7327,14 +10649,14 @@ password ${credentials.password}
7327
10649
  throw new CliError(`${plan.helper} was not found, so the box cannot be mounted here: ${plan.install}. Or serve it with \`agentbox mount --no-mount\` and use any WebDAV client.`);
7328
10650
  }
7329
10651
  let madeMountpoint = false;
7330
- if (plan.mountpoint && !fs8.existsSync(plan.mountpoint)) {
7331
- fs8.mkdirSync(plan.mountpoint, { recursive: true });
10652
+ if (plan.mountpoint && !fs21.existsSync(plan.mountpoint)) {
10653
+ fs21.mkdirSync(plan.mountpoint, { recursive: true });
7332
10654
  madeMountpoint = true;
7333
10655
  }
7334
- if (plan.link && fs8.existsSync(plan.link)) throw new CliError(`${plan.link} already exists; name a new folder for the link, or leave it out`);
10656
+ if (plan.link && fs21.existsSync(plan.link)) throw new CliError(`${plan.link} already exists; name a new folder for the link, or leave it out`);
7335
10657
  const mounted = await runStep(plan.mount, helper, ctx.env);
7336
10658
  if (mounted.code !== 0) {
7337
- if (madeMountpoint) fs8.rmSync(plan.mountpoint, { recursive: false, force: true });
10659
+ if (madeMountpoint) fs21.rmSync(plan.mountpoint, { recursive: false, force: true });
7338
10660
  throw new CliError(`${plan.helper} could not mount it${mounted.output ? `: ${mounted.output}` : ""}`);
7339
10661
  }
7340
10662
  let where = plan.where;
@@ -7356,7 +10678,7 @@ password ${credentials.password}
7356
10678
  }
7357
10679
  let linked = false;
7358
10680
  if (plan.link && where) {
7359
- fs8.symlinkSync(where, plan.link, "dir");
10681
+ fs21.symlinkSync(where, plan.link, "dir");
7360
10682
  linked = true;
7361
10683
  }
7362
10684
  if (!gioOnly) {
@@ -7371,10 +10693,10 @@ password ${credentials.password}
7371
10693
  const done = await runStep(unmount, findOnPath(unmount.command, ctx.env, ctx.platform) ?? unmount.command, ctx.env);
7372
10694
  if (done.code !== 0) ctx.warn(`${unmount.command} ${unmount.args.join(" ")} failed${done.output ? `: ${done.output}` : ""}; unmount it yourself`);
7373
10695
  }
7374
- if (linked) fs8.rmSync(plan.link, { force: true });
10696
+ if (linked) fs21.rmSync(plan.link, { force: true });
7375
10697
  if (madeMountpoint) {
7376
10698
  try {
7377
- fs8.rmdirSync(plan.mountpoint);
10699
+ fs21.rmdirSync(plan.mountpoint);
7378
10700
  } catch {
7379
10701
  }
7380
10702
  }
@@ -7386,12 +10708,12 @@ password ${credentials.password}
7386
10708
  var MOUNT_COMMANDS = [mount];
7387
10709
 
7388
10710
  // src/commands/terminal.ts
7389
- import fs9 from "node:fs";
7390
- import os8 from "node:os";
7391
- import path10 from "node:path";
10711
+ import fs22 from "node:fs";
10712
+ import os13 from "node:os";
10713
+ import path20 from "node:path";
7392
10714
 
7393
10715
  // src/ttyd.ts
7394
- import { EventEmitter } from "node:events";
10716
+ import { EventEmitter as EventEmitter3 } from "node:events";
7395
10717
  var CLIENT = { INPUT: 48, RESIZE: 49, PAUSE: 50, RESUME: 51 };
7396
10718
  var SERVER = { OUTPUT: 48, TITLE: 49, PREFERENCES: 50 };
7397
10719
  function handshakeFrame(columns, rows, authToken = "") {
@@ -7426,7 +10748,7 @@ function parseServerFrame(frame) {
7426
10748
  return { kind: "unknown", command: command2 ?? -1 };
7427
10749
  }
7428
10750
  }
7429
- var TtydSession = class extends EventEmitter {
10751
+ var TtydSession = class extends EventEmitter3 {
7430
10752
  constructor(client, basePath) {
7431
10753
  super();
7432
10754
  this.client = client;
@@ -7464,9 +10786,9 @@ var TtydSession = class extends EventEmitter {
7464
10786
  else if (parsed.kind === "title") this.emit("title", parsed.title);
7465
10787
  });
7466
10788
  ws.on("unexpected-response", (_req, res) => {
7467
- const status2 = res.statusCode ?? 0;
10789
+ const status3 = res.statusCode ?? 0;
7468
10790
  res.resume();
7469
- const message = status2 === 401 ? "the box did not accept this device's sign-in; run `agentbox login` again" : status2 === 403 ? "the box refused the terminal (origin check); is the box's URL right?" : status2 === 404 ? `this box has no ${this.basePath} terminal` : status2 === 502 || status2 === 503 ? `the ${this.basePath} terminal is not running on the box` : `the box refused the terminal (HTTP ${status2})`;
10791
+ const message = status3 === 401 ? "the box did not accept this device's sign-in; run `agentbox login` again" : status3 === 403 ? "the box refused the terminal (origin check); is the box's URL right?" : status3 === 404 ? `this box has no ${this.basePath} terminal` : status3 === 502 || status3 === 503 ? `the ${this.basePath} terminal is not running on the box` : `the box refused the terminal (HTTP ${status3})`;
7470
10792
  this.finish({ reason: "error", message });
7471
10793
  ws.terminate();
7472
10794
  });
@@ -7864,11 +11186,11 @@ agentbox: ${message}\r
7864
11186
 
7865
11187
  // src/commands/terminal.ts
7866
11188
  function cdCommand(dir) {
7867
- const quote = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
11189
+ const quote2 = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
7868
11190
  let target;
7869
11191
  if (dir === "~") target = "~";
7870
- else if (dir.startsWith("~/")) target = `~/${quote(dir.slice(2))}`;
7871
- else target = quote(dir);
11192
+ else if (dir.startsWith("~/")) target = `~/${quote2(dir.slice(2))}`;
11193
+ else target = quote2(dir);
7872
11194
  return ` cd -- ${target} && clear\r`;
7873
11195
  }
7874
11196
  async function connectTerminal(ctx, basePath, initialInput) {
@@ -7920,8 +11242,8 @@ the network: faster, and no redraw glitches. Install it now with herdr's install
7920
11242
  ctx.warn(`herdr's installer exited ${code2}`);
7921
11243
  return null;
7922
11244
  }
7923
- const home = (win ? ctx.env.USERPROFILE : ctx.env.HOME) || os8.homedir();
7924
- return findOnPath("herdr", ctx.env, ctx.platform) ?? [path10.join(home, ".local", "bin", "herdr")].find((f) => fs9.existsSync(f)) ?? null;
11245
+ const home2 = (win ? ctx.env.USERPROFILE : ctx.env.HOME) || os13.homedir();
11246
+ return findOnPath("herdr", ctx.env, ctx.platform) ?? [path20.join(home2, ".local", "bin", "herdr")].find((f) => fs22.existsSync(f)) ?? null;
7925
11247
  }
7926
11248
  var attach = command({
7927
11249
  path: ["attach"],
@@ -7971,10 +11293,11 @@ var shell = command({
7971
11293
  var TERMINAL_COMMANDS = [attach, shell];
7972
11294
 
7973
11295
  // src/commands/update.ts
7974
- import { spawnSync as spawnSync2 } from "node:child_process";
7975
- import { randomBytes as randomBytes4 } from "node:crypto";
7976
- import fs10 from "node:fs";
7977
- import path11 from "node:path";
11296
+ import { spawnSync as spawnSync3 } from "node:child_process";
11297
+ import { randomBytes as randomBytes9 } from "node:crypto";
11298
+ import fs23 from "node:fs";
11299
+ import os14 from "node:os";
11300
+ import path21 from "node:path";
7978
11301
  var MAX_BUNDLE = 16 * 1024 * 1024;
7979
11302
  function bundleVersion(bundle) {
7980
11303
  const head = (typeof bundle === "string" ? bundle : bundle.subarray(0, 512).toString("utf8")).split("\n", 3);
@@ -7987,17 +11310,17 @@ function selfInstall(argv1) {
7987
11310
  if (!argv1) return null;
7988
11311
  let file;
7989
11312
  try {
7990
- file = fs10.realpathSync(argv1);
11313
+ file = fs23.realpathSync(argv1);
7991
11314
  } catch {
7992
11315
  return null;
7993
11316
  }
7994
11317
  let checkout = false;
7995
11318
  let npm = false;
7996
11319
  try {
7997
- const root = path11.join(path11.dirname(file), "..");
7998
- const pkg = JSON.parse(fs10.readFileSync(path11.join(root, "package.json"), "utf8"));
11320
+ const root = path21.join(path21.dirname(file), "..");
11321
+ const pkg = JSON.parse(fs23.readFileSync(path21.join(root, "package.json"), "utf8"));
7999
11322
  if (pkg.name === NPM_PACKAGE) {
8000
- if (fs10.existsSync(path11.join(root, "src", "cli.ts"))) checkout = true;
11323
+ if (fs23.existsSync(path21.join(root, "src", "cli.ts"))) checkout = true;
8001
11324
  else npm = true;
8002
11325
  }
8003
11326
  } catch {
@@ -8008,6 +11331,27 @@ function npmVersionFor(boxVersion) {
8008
11331
  const m = /^v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?)$/.exec(boxVersion ?? "");
8009
11332
  return m ? m[1] : "latest";
8010
11333
  }
11334
+ function nodeCredentials(env, platform = process.platform) {
11335
+ const candidates = [];
11336
+ if (platform === "win32") {
11337
+ if (env.LOCALAPPDATA && path21.win32.isAbsolute(env.LOCALAPPDATA)) candidates.push(path21.join(env.LOCALAPPDATA, "agentbox", "node", "credentials.json"));
11338
+ } else {
11339
+ const xdg = env.XDG_DATA_HOME;
11340
+ if (xdg && path21.isAbsolute(xdg)) candidates.push(path21.join(xdg, "agentbox", "node", "credentials.json"));
11341
+ candidates.push(path21.join(env.HOME || os14.homedir(), ".local", "share", "agentbox", "node", "credentials.json"));
11342
+ }
11343
+ return candidates.find((f) => fs23.existsSync(f)) ?? null;
11344
+ }
11345
+ function signedUpdatesOnly(env, platform = process.platform, standalone = STANDALONE) {
11346
+ const creds = nodeCredentials(env, platform);
11347
+ if (creds) {
11348
+ return `this machine is joined to a box as a node (${creds}), so agentbox here updates only from signed releases, never from a box: the node checks for them itself (\`updates\` in its policy.json), or see https://github.com/jabezpauls/agentbox/releases`;
11349
+ }
11350
+ if (standalone) {
11351
+ return "this is the standalone agentbox, which updates only from signed releases, never from a box; download the latest from https://github.com/jabezpauls/agentbox/releases";
11352
+ }
11353
+ return null;
11354
+ }
8011
11355
  var update = command({
8012
11356
  path: ["update"],
8013
11357
  summary: "replace this CLI with the box's own build",
@@ -8018,6 +11362,8 @@ var update = command({
8018
11362
  ],
8019
11363
  details: "Downloads /cli/agentbox.mjs from the box (the current one, or --box), checks it runs, and swaps it in\nin one step.",
8020
11364
  async run(ctx, p) {
11365
+ const signedOnly = signedUpdatesOnly(ctx.env, ctx.platform);
11366
+ if (signedOnly) throw new CliError(signedOnly);
8021
11367
  const { name, box } = ctx.selected();
8022
11368
  const self = selfInstall(process.argv[1]);
8023
11369
  if (!self) throw new CliError("cannot tell which file this CLI runs from");
@@ -8062,7 +11408,7 @@ var update = command({
8062
11408
  }
8063
11409
  return;
8064
11410
  }
8065
- const current = fs10.readFileSync(self.file);
11411
+ const current = fs23.readFileSync(self.file);
8066
11412
  if (bundle.equals(current)) {
8067
11413
  ctx.out(`Already up to date: agentbox ${VERSION}, the same build ${name} serves.
8068
11414
  `);
@@ -8073,24 +11419,24 @@ var update = command({
8073
11419
  `);
8074
11420
  return;
8075
11421
  }
8076
- const dir = path11.dirname(self.file);
8077
- const tmp = path11.join(dir, `.agentbox-update-${randomBytes4(4).toString("hex")}.mjs`);
11422
+ const dir = path21.dirname(self.file);
11423
+ const tmp = path21.join(dir, `.agentbox-update-${randomBytes9(4).toString("hex")}.mjs`);
8078
11424
  try {
8079
- fs10.writeFileSync(tmp, bundle, { mode: 493 });
8080
- fs10.chmodSync(tmp, 493);
11425
+ fs23.writeFileSync(tmp, bundle, { mode: 493 });
11426
+ fs23.chmodSync(tmp, 493);
8081
11427
  } catch (err) {
8082
- fs10.rmSync(tmp, { force: true });
11428
+ fs23.rmSync(tmp, { force: true });
8083
11429
  throw new CliError(`cannot write to ${dir} (${err.message}); reinstall with the box's install script`);
8084
11430
  }
8085
- const check = spawnSync2(process.execPath, [tmp, "--version"], { encoding: "utf8", timeout: 3e4 });
11431
+ const check = spawnSync3(process.execPath, [tmp, "--version"], { encoding: "utf8", timeout: 3e4 });
8086
11432
  if (check.status !== 0) {
8087
- fs10.rmSync(tmp, { force: true });
11433
+ fs23.rmSync(tmp, { force: true });
8088
11434
  throw new CliError(`the downloaded build does not run here: ${(check.stderr || check.stdout || "").trim().slice(0, 300)}`);
8089
11435
  }
8090
11436
  try {
8091
- fs10.renameSync(tmp, self.file);
11437
+ fs23.renameSync(tmp, self.file);
8092
11438
  } catch (err) {
8093
- fs10.rmSync(tmp, { force: true });
11439
+ fs23.rmSync(tmp, { force: true });
8094
11440
  throw new CliError(`cannot replace ${self.file}: ${err.message}`);
8095
11441
  }
8096
11442
  ctx.out(`Updated agentbox ${VERSION} \u2192 ${safeText(version)} from ${name}.
@@ -8100,7 +11446,7 @@ var update = command({
8100
11446
  var UPDATE_COMMANDS = [update];
8101
11447
 
8102
11448
  // src/commands/index.ts
8103
- var COMMANDS = [
11449
+ var COMMANDS2 = [
8104
11450
  ...AUTH_COMMANDS,
8105
11451
  ...STATUS_COMMANDS,
8106
11452
  ...TERMINAL_COMMANDS,
@@ -8111,6 +11457,7 @@ var COMMANDS = [
8111
11457
  ...APPS_COMMANDS,
8112
11458
  ...AGENT_COMMANDS,
8113
11459
  ...HERDR_COMMANDS,
11460
+ ...NODE_COMMANDS,
8114
11461
  ...UPDATE_COMMANDS
8115
11462
  ];
8116
11463
  function optionLine(o) {
@@ -8118,8 +11465,8 @@ function optionLine(o) {
8118
11465
  return ` ${flag.padEnd(26)} ${o.description}`;
8119
11466
  }
8120
11467
  function mainHelp() {
8121
- const width2 = Math.max(...COMMANDS.filter((c) => !c.hidden).map((c) => c.path.join(" ").length));
8122
- const lines = COMMANDS.filter((c) => !c.hidden).map((c) => ` ${c.path.join(" ").padEnd(width2)} ${c.summary}`);
11468
+ const width2 = Math.max(...COMMANDS2.filter((c) => !c.hidden).map((c) => c.path.join(" ").length));
11469
+ const lines = COMMANDS2.filter((c) => !c.hidden).map((c) => ` ${c.path.join(" ").padEnd(width2)} ${c.summary}`);
8123
11470
  return [
8124
11471
  `agentbox ${VERSION} \u2014 your agentbox, from this machine`,
8125
11472
  "",
@@ -8152,7 +11499,7 @@ function commandHelp(c) {
8152
11499
  ].join("\n");
8153
11500
  }
8154
11501
  function groupHelp(prefix) {
8155
- const members = COMMANDS.filter((c) => !c.hidden && prefix.every((w, i) => c.path[i] === w) && c.path.length > prefix.length);
11502
+ const members = COMMANDS2.filter((c) => !c.hidden && prefix.every((w, i) => c.path[i] === w) && c.path.length > prefix.length);
8156
11503
  if (members.length === 0) return null;
8157
11504
  const width2 = Math.max(...members.map((c) => c.path.join(" ").length));
8158
11505
  return [
@@ -8166,7 +11513,7 @@ function groupHelp(prefix) {
8166
11513
  }
8167
11514
 
8168
11515
  // src/context.ts
8169
- import os9 from "node:os";
11516
+ import os15 from "node:os";
8170
11517
  var VERSION_CHECK_MS = 24 * 60 * 6e4;
8171
11518
  var Context = class {
8172
11519
  constructor(io, env, config, globals, platform = process.platform, now = Date.now) {
@@ -8224,7 +11571,7 @@ var Context = class {
8224
11571
  }
8225
11572
  /** Where temporary files go: `$TMPDIR` (or Windows' `%TEMP%`), else the system's. */
8226
11573
  tempRoot() {
8227
- return this.env.TMPDIR || this.env.TEMP || this.env.TMP || os9.tmpdir();
11574
+ return this.env.TMPDIR || this.env.TEMP || this.env.TMP || os15.tmpdir();
8228
11575
  }
8229
11576
  printJson(value) {
8230
11577
  this.out(`${JSON.stringify(value, null, 2)}
@@ -8307,7 +11654,7 @@ function describeLocalError(err) {
8307
11654
  return { message: e.path ? `${e.path}: ${what}` : `${what} (${e.syscall ?? e.code})`, code: e.code === "ENOENT" ? EXIT.NOT_FOUND : EXIT.FAILURE };
8308
11655
  }
8309
11656
  var INTERRUPT_GRACE_MS = 1500;
8310
- async function run(argv, deps = {}) {
11657
+ async function run3(argv, deps = {}) {
8311
11658
  const io = deps.io ?? defaultIo();
8312
11659
  const env = deps.env ?? process.env;
8313
11660
  const fail = (message, code2) => {
@@ -8317,7 +11664,7 @@ async function run(argv, deps = {}) {
8317
11664
  };
8318
11665
  let parsed;
8319
11666
  try {
8320
- parsed = parseArgs(argv, COMMANDS);
11667
+ parsed = parseArgs(argv, COMMANDS2);
8321
11668
  } catch (err) {
8322
11669
  if (err instanceof UsageError) return fail(err.message, EXIT.USAGE);
8323
11670
  throw err;
@@ -8381,6 +11728,6 @@ if (nodeMajor() < MIN_NODE_MAJOR) {
8381
11728
  `);
8382
11729
  process.exit(1);
8383
11730
  }
8384
- var code = await run(process.argv.slice(2), { signals: true });
11731
+ var code = await run3(process.argv.slice(2), { signals: true });
8385
11732
  process.exitCode = code;
8386
11733
  process.stdout.write("", () => process.exit(code));