@jabezpauls/agentbox 0.1.12 → 0.1.13

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 +73 -15
  2. package/package.json +1 -1
package/dist/agentbox.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // agentbox-cli v0.1.12
2
+ // agentbox-cli v0.1.13
3
3
  import { createRequire as __agentboxCreateRequire } from "node:module";
4
4
  const require = __agentboxCreateRequire(import.meta.url);
5
5
  var __create = Object.create;
@@ -3868,7 +3868,7 @@ function parseSize(raw2) {
3868
3868
  }
3869
3869
 
3870
3870
  // src/version.ts
3871
- var VERSION = true ? "v0.1.12" : "0.0.0-dev";
3871
+ var VERSION = true ? "v0.1.13" : "0.0.0-dev";
3872
3872
  var STANDALONE = typeof __AGENTBOX_STANDALONE__ === "boolean" && __AGENTBOX_STANDALONE__;
3873
3873
  var MIN_NODE_MAJOR = 20;
3874
3874
  function nodeMajor(version = process.versions.node) {
@@ -6871,7 +6871,7 @@ function writePrivate2(file, contents, mode = 384) {
6871
6871
  }
6872
6872
 
6873
6873
  // src/node/control.ts
6874
- var COMMANDS = /* @__PURE__ */ new Set(["status", "pause", "resume", "leave", "preview", "preview-list", "preview-stop"]);
6874
+ var COMMANDS = /* @__PURE__ */ new Set(["status", "pause", "resume", "leave", "preview", "preview-list", "preview-stop", "passphrase"]);
6875
6875
  var PIPE_NAME = /^\\\\\.\\pipe\\agentbox-node-[0-9a-f]{32}$/;
6876
6876
  function address(socketPath, platform) {
6877
6877
  if (platform !== "win32") return socketPath;
@@ -7033,6 +7033,7 @@ function decodeControl(payload) {
7033
7033
  import { createHash as createHash3, createHmac, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
7034
7034
  var PROTOCOL_VERSION = 1;
7035
7035
  var LINK_PATH = "/_gate/node";
7036
+ var MAX_STREAMS_PER_LINK = 256;
7036
7037
  var DUPLICATE_BACKOFF_MS = 5 * 6e4;
7037
7038
  var NODE_TOKEN_SHAPE = /^abn_[A-Za-z0-9_-]{43}$/;
7038
7039
  function tokenDigest(token) {
@@ -7997,6 +7998,7 @@ var Roots = class {
7997
7998
  isHidden;
7998
7999
  isWriteDenied;
7999
8000
  refusePath;
8001
+ holdsHidden;
8000
8002
  constructor(opts) {
8001
8003
  this.roots = opts.roots.map((r) => ({ id: r.id, path: path13.resolve(r.path), mode: r.mode, stateDir: path13.resolve(r.stateDir) }));
8002
8004
  const first = this.roots[0];
@@ -8008,6 +8010,7 @@ var Roots = class {
8008
8010
  this.isHidden = opts.isHidden;
8009
8011
  this.isWriteDenied = opts.isWriteDenied;
8010
8012
  this.refusePath = opts.refusePath;
8013
+ this.holdsHidden = opts.holdsHidden;
8011
8014
  }
8012
8015
  byId(id) {
8013
8016
  const root = this.roots.find((r) => r.id === id);
@@ -8133,6 +8136,10 @@ var Roots = class {
8133
8136
  }
8134
8137
  }
8135
8138
  }
8139
+ /** Refuse to copy an entry that holds a hidden path: the copy would show it. */
8140
+ assertCopyable(ref) {
8141
+ if (this.holdsHidden?.(ref.fs)) throw new FilesError(403, `permission denied: ${ref.abs}`, "denied");
8142
+ }
8136
8143
  /** Resolve through every symlink; the destination must stay in the root. */
8137
8144
  async target(input) {
8138
8145
  const loc = this.locate(input);
@@ -8808,6 +8815,7 @@ var FileOps = class {
8808
8815
  async copy(body) {
8809
8816
  const src = await this.roots.entry(body.from);
8810
8817
  await this.roots.assertMovable(src);
8818
+ this.roots.assertCopyable(src);
8811
8819
  const dest = await this.roots.creatable(body.to);
8812
8820
  this.roots.assertMutable(dest);
8813
8821
  if (dest.abs === src.abs) throw new FilesError(409, "a copy needs a different name", "exists");
@@ -8872,6 +8880,7 @@ var Trash = class {
8872
8880
  }
8873
8881
  throw err;
8874
8882
  }
8883
+ this.roots.assertMutable(ref);
8875
8884
  result.trashed.push(await this.put(ref.root, ref.abs, ref.fs));
8876
8885
  }
8877
8886
  return result;
@@ -9293,6 +9302,7 @@ var FilesService = class {
9293
9302
  ...opts.caseInsensitive === void 0 ? {} : { caseInsensitive: opts.caseInsensitive },
9294
9303
  ...opts.isHidden === void 0 ? {} : { isHidden: opts.isHidden },
9295
9304
  ...opts.isWriteDenied === void 0 ? {} : { isWriteDenied: opts.isWriteDenied },
9305
+ ...opts.holdsHidden === void 0 ? {} : { holdsHidden: opts.holdsHidden },
9296
9306
  ...opts.refusePath === void 0 ? {} : { refusePath: opts.refusePath }
9297
9307
  });
9298
9308
  this.git = new GitStatusCache(opts.gitTtlMs === void 0 ? {} : { ttlMs: opts.gitTtlMs });
@@ -9887,6 +9897,7 @@ function refuseWindowsPath(input) {
9887
9897
  for (const part of rest.split("\\")) {
9888
9898
  if (part === "" || part === "." || part === "..") continue;
9889
9899
  if (/[. ]$/.test(part)) return "a name ending in a dot or a space is refused";
9900
+ if (/~\d/.test(part)) return "short (8.3) names, with a ~ and a digit, are refused";
9890
9901
  if (/^(con|prn|aux|nul|com[0-9¹²³]|lpt[0-9¹²³]|conin\$|conout\$)(\..*)?$/i.test(part)) return "DOS device names are refused";
9891
9902
  }
9892
9903
  return null;
@@ -9922,11 +9933,25 @@ function pathRules(o) {
9922
9933
  };
9923
9934
  const gitRuns = (p) => {
9924
9935
  const parts = fold(p).split(P.sep);
9925
- for (let i = 0; i + 1 < parts.length; i++) {
9926
- if (parts[i] === ".git" && (parts[i + 1] === "hooks" || parts[i + 1] === "config")) return true;
9936
+ for (let i = 0; i < parts.length; i++) {
9937
+ if (parts[i] === ".git") return true;
9938
+ if (parts[i]?.endsWith(".git") && (parts[i + 1] === "hooks" || parts[i + 1] === "config")) return true;
9927
9939
  }
9928
9940
  return false;
9929
9941
  };
9942
+ const homeF = fold(o.home);
9943
+ const homeBase = homeF.endsWith(P.sep) ? homeF : homeF + P.sep;
9944
+ const inHomeDot = (p) => {
9945
+ const f = fold(p);
9946
+ if (!f.startsWith(homeBase)) return false;
9947
+ const first = f.slice(homeBase.length).split(P.sep)[0] ?? "";
9948
+ return first.startsWith(".") || platform === "win32" && first === "appdata";
9949
+ };
9950
+ const holds = (p, list) => {
9951
+ const f = fold(p);
9952
+ const base = f.endsWith(P.sep) ? f : f + P.sep;
9953
+ return list.some((e) => e.startsWith(base));
9954
+ };
9930
9955
  const remote = (p) => platform === "win32" && (p.startsWith("\\\\") || p.startsWith("//"));
9931
9956
  return {
9932
9957
  platform,
@@ -9934,7 +9959,8 @@ function pathRules(o) {
9934
9959
  hidden,
9935
9960
  writeDenied,
9936
9961
  isHidden: (p) => remote(p) || under(p, hiddenF),
9937
- isWriteDenied: (p) => remote(p) || under(p, deniedF) || gitRuns(p),
9962
+ isWriteDenied: (p) => remote(p) || under(p, deniedF) || gitRuns(p) || inHomeDot(p) || holds(p, deniedF) || holds(p, hiddenF),
9963
+ holdsHidden: (p) => holds(p, hiddenF),
9938
9964
  refusePath: (input) => platform === "win32" ? refuseWindowsPath(input) : null
9939
9965
  };
9940
9966
  }
@@ -9956,7 +9982,7 @@ function windowsDocuments(env = process.env) {
9956
9982
  const out = [];
9957
9983
  for (const key of ["User Shell Folders", "Shell Folders"]) {
9958
9984
  try {
9959
- const text = execFileSync("reg.exe", ["query", `HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\${key}`, "/v", "Personal"], {
9985
+ const text = execFileSync(path22.win32.join(env.SystemRoot && path22.win32.isAbsolute(env.SystemRoot) ? env.SystemRoot : "C:\\Windows", "System32", "reg.exe"), ["query", `HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\${key}`, "/v", "Personal"], {
9960
9986
  encoding: "utf8",
9961
9987
  timeout: 1e4,
9962
9988
  windowsHide: true,
@@ -9988,6 +10014,7 @@ function filesFor(policy, paths2, rules) {
9988
10014
  caseInsensitive: rules.caseInsensitive,
9989
10015
  isHidden: rules.isHidden,
9990
10016
  isWriteDenied: rules.isWriteDenied,
10017
+ holdsHidden: rules.holdsHidden,
9991
10018
  refusePath: rules.refusePath,
9992
10019
  ...rules.platform === "win32" ? { apiPaths: { toHost: (p) => toHostPath(p, "win32"), toApi: (p) => toApiPath(p, "win32") } } : {}
9993
10020
  });
@@ -10218,6 +10245,8 @@ var NodeLink = class extends EventEmitter {
10218
10245
  stateNow = "stopped";
10219
10246
  running = false;
10220
10247
  paused = false;
10248
+ /** It said `leave`: only then is a close saying "left" taken for one (a gate saying so unasked is a dropped link). */
10249
+ leaving = false;
10221
10250
  failures = 0;
10222
10251
  failingSince = null;
10223
10252
  retryTimer = null;
@@ -10315,6 +10344,7 @@ var NodeLink = class extends EventEmitter {
10315
10344
  clearTimeout(timer);
10316
10345
  resolve(code2 === 4005);
10317
10346
  });
10347
+ this.leaving = true;
10318
10348
  ws.send(encodeControl({ type: "leave" }));
10319
10349
  });
10320
10350
  }
@@ -10362,7 +10392,7 @@ var NodeLink = class extends EventEmitter {
10362
10392
  if (!this.running || this.paused || this.ws) return;
10363
10393
  this.set("connecting");
10364
10394
  const url = `${this.o.box.replace(/^http/, "ws")}${LINK_PATH}`;
10365
- const ws = new import_websocket.default(url, { headers: { authorization: `Bearer ${this.o.token}` }, perMessageDeflate: false, handshakeTimeout: 2e4, followRedirects: false });
10395
+ const ws = new import_websocket.default(url, { headers: { authorization: `Bearer ${this.o.token}` }, perMessageDeflate: false, handshakeTimeout: 2e4, followRedirects: false, maxPayload: STREAM_WINDOW + 16 });
10366
10396
  this.ws = ws;
10367
10397
  let welcomed = false;
10368
10398
  let stopKeepAlive = () => {
@@ -10445,7 +10475,7 @@ var NodeLink = class extends EventEmitter {
10445
10475
  return;
10446
10476
  }
10447
10477
  if (frame.type === FrameType.open) {
10448
- if (!welcomed || this.listenerCount("stream") === 0 || streams.has(frame.stream)) {
10478
+ if (!welcomed || this.listenerCount("stream") === 0 || streams.has(frame.stream) || streams.size >= MAX_STREAMS_PER_LINK) {
10449
10479
  ws.send(encodeFrame(FrameType.close, frame.stream));
10450
10480
  return;
10451
10481
  }
@@ -10460,7 +10490,7 @@ var NodeLink = class extends EventEmitter {
10460
10490
  if (this.ws !== ws) return;
10461
10491
  this.ws = null;
10462
10492
  if (!this.running || this.stateNow === "revoked") return;
10463
- if (code2 === 4005) {
10493
+ if (code2 === 4005 && this.leaving) {
10464
10494
  this.stop("left");
10465
10495
  return;
10466
10496
  }
@@ -10511,16 +10541,29 @@ function presenceAnswer(key, challenge, sessionId, machineId) {
10511
10541
  var PresenceGate = class {
10512
10542
  constructor(o) {
10513
10543
  this.o = o;
10544
+ this.presence = o.presence;
10514
10545
  this.key = Buffer.from(o.presence.key, "base64url");
10515
10546
  this.now = o.now ?? Date.now;
10516
10547
  }
10517
10548
  o;
10518
10549
  key;
10550
+ presence;
10519
10551
  challenges = /* @__PURE__ */ new Map();
10520
10552
  grants = /* @__PURE__ */ new Map();
10521
10553
  failures = [];
10522
10554
  lockedUntil = 0;
10523
10555
  now;
10556
+ /**
10557
+ * A new passphrase (`agentbox node passphrase`), taken at once: every
10558
+ * challenge and every grant the old one earned ends, so channels opened on
10559
+ * them close at the next check.
10560
+ */
10561
+ rekey(presence) {
10562
+ this.presence = presence;
10563
+ this.key = Buffer.from(presence.key, "base64url");
10564
+ this.challenges.clear();
10565
+ this.grants.clear();
10566
+ }
10524
10567
  /** Locked until this time, or `null`. */
10525
10568
  locked() {
10526
10569
  return this.lockedUntil > this.now() ? this.lockedUntil : null;
@@ -10535,7 +10578,7 @@ var PresenceGate = class {
10535
10578
  const challenge = randomBytes10(32).toString("base64url");
10536
10579
  const expiresAt = t2 + CHALLENGE_MS;
10537
10580
  this.challenges.set(challenge, { session, expiresAt });
10538
- return { challenge, salt: this.o.presence.salt, iterations: this.o.presence.iterations, expiresAt };
10581
+ return { challenge, salt: this.presence.salt, iterations: this.presence.iterations, expiresAt };
10539
10582
  }
10540
10583
  answer(session, challenge, answer) {
10541
10584
  if (session === null) return { ok: false, error: "no_session" };
@@ -11775,6 +11818,10 @@ var CONSOLE_EXE = "agentbox.exe";
11775
11818
  var LOG_MAX_BYTES = 10 * 1024 * 1024;
11776
11819
  var LOG_FILES = 3;
11777
11820
  var RESTART_EXIT = 75;
11821
+ function regExe(env = process.env) {
11822
+ const root = env.SystemRoot && path27.win32.isAbsolute(env.SystemRoot) ? env.SystemRoot : "C:\\Windows";
11823
+ return path27.win32.join(root, "System32", "reg.exe");
11824
+ }
11778
11825
  function standalone(execPath, script) {
11779
11826
  return script === void 0 || path27.resolve(script) === path27.resolve(execPath);
11780
11827
  }
@@ -11800,7 +11847,7 @@ function runCommandLine(execPath = process.execPath, script = process.argv[1]) {
11800
11847
  async function installService2(opts) {
11801
11848
  const run4 = opts.run ?? runCommand;
11802
11849
  const entry = runEntry(opts.gui);
11803
- const r = await run4("reg.exe", ["add", RUN_KEY, "/v", RUN_VALUE, "/t", "REG_SZ", "/d", entry, "/f"]);
11850
+ const r = await run4(regExe(), ["add", RUN_KEY, "/v", RUN_VALUE, "/t", "REG_SZ", "/d", entry, "/f"]);
11804
11851
  if (r.code !== 0) throw new Error(`reg add ${RUN_KEY} failed: ${(r.stderr || r.stdout).trim() || `exit ${r.code}`}`);
11805
11852
  if (opts.start !== false) {
11806
11853
  const child = spawn9(opts.gui, ["node", "supervise"], { detached: true, stdio: "ignore", windowsHide: true });
@@ -11811,14 +11858,14 @@ async function installService2(opts) {
11811
11858
  return { entry };
11812
11859
  }
11813
11860
  async function readRunEntry(run4 = runCommand) {
11814
- const r = await run4("reg.exe", ["query", RUN_KEY, "/v", RUN_VALUE]);
11861
+ const r = await run4(regExe(), ["query", RUN_KEY, "/v", RUN_VALUE]);
11815
11862
  if (r.code !== 0) return null;
11816
11863
  return new RegExp(`${RUN_VALUE}\\s+REG_SZ\\s+(.*)`).exec(r.stdout)?.[1]?.trim() ?? null;
11817
11864
  }
11818
11865
  async function removeService2(opts) {
11819
11866
  const run4 = opts.run ?? runCommand;
11820
11867
  const had = await readRunEntry(run4) !== null;
11821
- if (had) await run4("reg.exe", ["delete", RUN_KEY, "/v", RUN_VALUE, "/f"]);
11868
+ if (had) await run4(regExe(), ["delete", RUN_KEY, "/v", RUN_VALUE, "/f"]);
11822
11869
  const pid = supervisorPid(opts.paths);
11823
11870
  if (pid !== null && pid !== process.pid) {
11824
11871
  try {
@@ -12509,6 +12556,7 @@ async function runNode(o) {
12509
12556
  if (await callControl(o.paths.control, o.paths.controlSecret, "status", 2e3) !== null) {
12510
12557
  throw new CliError("a node is already running on this machine (see `agentbox node status`)");
12511
12558
  }
12559
+ process.chdir(o.paths.dir);
12512
12560
  let policy = loadPolicy(o.paths.policy, o.home);
12513
12561
  const claude = claudePaths(o.paths);
12514
12562
  prepareClaude(claude, { home: o.home, machine: creds.name });
@@ -12682,6 +12730,14 @@ async function runNode(o) {
12682
12730
  setPaused(o.paths.policy, false);
12683
12731
  link.resume();
12684
12732
  return { state: link.state };
12733
+ case "passphrase": {
12734
+ const fresh = readCredentials(o.paths.credentials);
12735
+ if (!fresh) throw new Error("credentials.json cannot be read");
12736
+ presence.rekey(fresh.presence);
12737
+ const closed = server.enforce();
12738
+ o.log(`presence: the machines passphrase changed on this machine; every grant ended${closed ? ` (${closed} channel(s) closed)` : ""}`);
12739
+ return {};
12740
+ }
12685
12741
  case "leave": {
12686
12742
  const told = await link.leave();
12687
12743
  setImmediate(() => finish(EXIT.OK));
@@ -12691,7 +12747,7 @@ async function runNode(o) {
12691
12747
  });
12692
12748
  link.on("revoked", () => {
12693
12749
  fs22.rmSync(o.paths.secrets, { recursive: true, force: true });
12694
- writeCredentials(o.paths.credentials, { ...creds, revokedAt: now() });
12750
+ writeCredentials(o.paths.credentials, { ...readCredentials(o.paths.credentials) ?? creds, revokedAt: now() });
12695
12751
  o.log(`${creds.box} revoked this machine. Its shared secrets are wiped; everything else is kept. Re-join it to use it again.`);
12696
12752
  finish(EXIT.OK);
12697
12753
  });
@@ -13059,6 +13115,8 @@ var passphrase = command({
13059
13115
  } finally {
13060
13116
  prompt.close();
13061
13117
  }
13118
+ const live = await callControl(np.control, np.controlSecret, "passphrase", 5e3);
13119
+ if (live !== null && live.ok !== true) throw new CliError(`the running node did not take it (${String(live.error ?? "refused")}); restart it`);
13062
13120
  ctx.err("This machine now proves presence with the new passphrase.\n");
13063
13121
  }
13064
13122
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jabezpauls/agentbox",
3
3
  "type": "module",
4
- "version": "0.1.12",
4
+ "version": "0.1.13",
5
5
  "description": "agentbox from your own machine: sign in, attach, move files, mount the workspace.",
6
6
  "license": "MIT",
7
7
  "homepage": "https://github.com/jabezpauls/agentbox/blob/main/docs/cli.md",