agent.libx.js 0.94.11 → 0.94.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.
package/README.md CHANGED
@@ -103,7 +103,7 @@ agentx -c "keep going" # continue the most recent session
103
103
  agentx --resume <id> "…" # resume a specific session
104
104
  ```
105
105
 
106
- - **Filesystem + Shell** — by default the CLI has **full real-filesystem access like Claude Code** (root `/` is the machine root, the launch dir is the working dir, absolute host paths and above-cwd reach both work) with a **real `/bin/sh`** (`Shell` tool) so the agent can run git, bun, node, curl, and any installed binary. Secrets (`.env`, `.ssh`, keys, `.git`) stay hidden by the jail; env secrets are scrubbed from the child shell. `--sandbox` instead operates over an in-memory copy of the working dir with a VFS-only `bash` — the real disk is never touched. `--boddb <dir>` runs over a **persistent database workspace** (a bod-db store at `<dir>` — `meta.db` tree + `files/` bytes) that survives across runs while the real disk stays untouched; DB-native by default, or add `--seed` to hydrate it from cwd on the first run. `--no-shell` forces the VFS bash in disk mode. (`/sandbox` shows the active mode.)
106
+ - **Filesystem + Shell** — by default the CLI has **full real-filesystem access like Claude Code** (root `/` is the machine root, the launch dir is the working dir, absolute host paths and above-cwd reach both work) with a **real `/bin/sh`** (`Shell` tool) so the agent can run git, bun, node, curl, and any installed binary. Secrets (`.env`, `.ssh`, keys, `.git`) stay hidden by the jail; env secrets are scrubbed from the child shell. `--sandbox` instead operates over an in-memory copy of the working dir with a VFS-only `bash` — the real disk is never touched. `--boddb <dir>` runs over a **persistent database workspace** (a bod-db store at `<dir>` — `meta.db` tree + `files/` bytes) that survives across runs while the real disk stays untouched; DB-native by default, or add `--seed` to hydrate it from cwd on the first run. `--no-shell` forces the VFS bash in disk mode. `--harden` OS-sandboxes the real shell (macOS `sandbox-exec` / Linux `bwrap`): writes confined to cwd+tmp, outbound network blocked (`--harden-net` keeps network); commands fail closed when no wrapper exists. (`/sandbox` shows the active mode.)
107
107
  - **Sessions** — every conversation persists to `./.agent/sessions/<id>.json`; `--continue`/`--resume` (and `/sessions`, `/resume`) pick it back up, *with memory across turns* — a REPL turn sees the previous one. A global symlink index at `~/.agent/sessions/` enables cross-project lookup: `--resume 090715-myproject` resolves from any directory, and `/sessions all` lists every project's sessions in one picker.
108
108
  - **Diffs** — every `Edit`/`Write`/`MultiEdit` renders a colorized `+/-` diff (TTY-gated; plain when piped).
109
109
  - **Slash commands** — `/help /tools /model /compact /memory /clear /sessions /resume /commands /init`; `/compact <focus>` preserves matching lines from the folded span; `/memory` opens the memory index in `$EDITOR`; user-defined `./.agent/commands/<name>.md` are invokable directly as `/<name>` (the same registry the model's `SlashCommand` tool uses).
package/dist/cli.d.ts CHANGED
@@ -90,6 +90,8 @@ interface Args {
90
90
  print?: boolean;
91
91
  debug?: boolean;
92
92
  scratch?: boolean;
93
+ harden?: boolean;
94
+ hardenNet?: boolean;
93
95
  }
94
96
  declare function parseArgs(argv: string[]): Args;
95
97
  /** Synthetic task-event user messages ("[task t2 completed] <multi-KB worker dump>") aren't real user
package/dist/cli.js CHANGED
@@ -1388,7 +1388,67 @@ var init_JailedFilesystem = __esm({
1388
1388
  }
1389
1389
  });
1390
1390
 
1391
+ // src/shell.sandbox.ts
1392
+ function writable(cwd, o, tmpDir) {
1393
+ const set = /* @__PURE__ */ new Set([cwd, "/tmp", "/private/tmp", "/private/var/folders", "/dev", ...tmpDir ? [tmpDir] : [], ...o.writePaths]);
1394
+ return [...set];
1395
+ }
1396
+ function seatbeltProfile(cwd, o, tmpDir) {
1397
+ const allows = writable(cwd, o, tmpDir).map((p) => `(subpath ${sbQuote(p)})`).join(" ");
1398
+ return [
1399
+ "(version 1)",
1400
+ "(allow default)",
1401
+ ...o.network ? [] : ["(deny network*)"],
1402
+ "(deny file-write*)",
1403
+ `(allow file-write* ${allows})`
1404
+ ].join("\n");
1405
+ }
1406
+ function sandboxArgv(command, cwd, opts = {}, platform2 = process.platform, tmpDir) {
1407
+ const o = { ...new OsSandboxOptions(), ...opts };
1408
+ if (platform2 === "darwin") {
1409
+ return { bin: "/usr/bin/sandbox-exec", args: ["-p", seatbeltProfile(cwd, o, tmpDir), "/bin/sh", "-c", command] };
1410
+ }
1411
+ if (platform2 === "linux") {
1412
+ const binds = writable(cwd, o, tmpDir).filter((p) => p !== "/dev" && !p.startsWith("/private")).flatMap((p) => ["--bind-try", p, p]);
1413
+ return {
1414
+ bin: "bwrap",
1415
+ args: ["--ro-bind", "/", "/", ...binds, "--dev", "/dev", "--proc", "/proc", "--die-with-parent", ...o.network ? [] : ["--unshare-net"], "/bin/sh", "-c", command]
1416
+ };
1417
+ }
1418
+ return null;
1419
+ }
1420
+ async function findSandboxWrapper(platform2 = process.platform) {
1421
+ const { existsSync: existsSync9 } = await import("fs");
1422
+ if (platform2 === "darwin") return existsSync9("/usr/bin/sandbox-exec") ? "/usr/bin/sandbox-exec" : null;
1423
+ if (platform2 === "linux") {
1424
+ for (const dir of (process.env.PATH ?? "/usr/bin:/bin").split(":")) if (dir && existsSync9(`${dir}/bwrap`)) return `${dir}/bwrap`;
1425
+ return null;
1426
+ }
1427
+ return null;
1428
+ }
1429
+ var OsSandboxOptions, sbQuote;
1430
+ var init_shell_sandbox = __esm({
1431
+ "src/shell.sandbox.ts"() {
1432
+ "use strict";
1433
+ OsSandboxOptions = class {
1434
+ /** Allow outbound network. Default OFF (Tier-1: no network unless granted). */
1435
+ network = false;
1436
+ /** Extra absolute paths writable beyond cwd + tmp (e.g. a build cache). */
1437
+ writePaths = [];
1438
+ };
1439
+ sbQuote = (p) => `"${p.replace(/(["\\])/g, "\\$1")}"`;
1440
+ }
1441
+ });
1442
+
1391
1443
  // src/tools.shell.ts
1444
+ async function spawnArgvFor(command, cwd, osSandbox) {
1445
+ if (!osSandbox) return { bin: "/bin/sh", args: ["-c", command] };
1446
+ const opts = osSandbox === true ? {} : osSandbox;
1447
+ const wrapper = await findSandboxWrapper();
1448
+ const wrapped = wrapper ? sandboxArgv(command, cwd, opts, process.platform, process.env.TMPDIR) : null;
1449
+ if (!wrapped) throw new Error(`OS sandbox requested but no wrapper available on ${process.platform} (need sandbox-exec or bwrap)`);
1450
+ return wrapped;
1451
+ }
1392
1452
  function childEnv(opts) {
1393
1453
  const base = {};
1394
1454
  const redact = opts.redactEnv !== false;
@@ -1421,6 +1481,14 @@ function makeRealShellTool(options) {
1421
1481
  return `Started background job ${id}. Poll output with ShellOutput({id:"${id}"}), check ShellStatus({id:"${id}"}), stop with ShellKill({id:"${id}"}).`;
1422
1482
  }
1423
1483
  const spawn3 = options.spawn ?? await nodeSpawn();
1484
+ let argv = { bin: "/bin/sh", args: ["-c", cmd] };
1485
+ if (options.osSandbox) {
1486
+ try {
1487
+ argv = await spawnArgvFor(cmd, options.cwd, options.osSandbox);
1488
+ } catch (e) {
1489
+ return `[exit 1] ${e?.message ?? e}`;
1490
+ }
1491
+ }
1424
1492
  const ctl = new AbortController();
1425
1493
  const onAbort = () => ctl.abort();
1426
1494
  if (ctx.signal) {
@@ -1455,7 +1523,7 @@ function makeRealShellTool(options) {
1455
1523
  };
1456
1524
  let proc;
1457
1525
  try {
1458
- proc = spawn3("/bin/sh", ["-c", cmd], { cwd: options.cwd, env: childEnv(options), signal: ctl.signal });
1526
+ proc = spawn3(argv.bin, argv.args, { cwd: options.cwd, env: childEnv(options), signal: ctl.signal });
1459
1527
  } catch (e) {
1460
1528
  return finish(`[exit 1] failed to spawn shell: ${e?.message ?? e}`);
1461
1529
  }
@@ -1541,6 +1609,7 @@ var init_tools_shell = __esm({
1541
1609
  init_tools();
1542
1610
  init_redact();
1543
1611
  init_logging();
1612
+ init_shell_sandbox();
1544
1613
  log12 = forComponent("shell");
1545
1614
  clean = (s) => truncateOutput(redactSecrets(s.replace(/\n+$/, "")));
1546
1615
  SECRET_ENV_RE = /(_API_KEY|_TOKEN|_SECRET|_PASSWORD|_PRIVATE_KEY|^AWS_|^GITHUB_TOKEN$|^OPENAI_|^ANTHROPIC_|^GOOGLE_|^GEMINI_|^GROQ_|^NPM_TOKEN$)/i;
@@ -1562,7 +1631,8 @@ var init_tools_shell = __esm({
1562
1631
  };
1563
1632
  try {
1564
1633
  const spawn3 = this.cfg.spawn ?? await nodeSpawn();
1565
- const proc = spawn3("/bin/sh", ["-c", command], { cwd: this.cfg.cwd, env: childEnv(this.cfg) });
1634
+ const argv = this.cfg.osSandbox ? await spawnArgvFor(command, this.cfg.cwd, this.cfg.osSandbox) : { bin: "/bin/sh", args: ["-c", command] };
1635
+ const proc = spawn3(argv.bin, argv.args, { cwd: this.cfg.cwd, env: childEnv(this.cfg) });
1566
1636
  job.proc = proc;
1567
1637
  proc.stdout?.on("data", append);
1568
1638
  proc.stderr?.on("data", append);
@@ -2996,7 +3066,10 @@ var Agent = class _Agent {
2996
3066
  }
2997
3067
  break;
2998
3068
  } catch (err2) {
2999
- const transient = !o.signal?.aborted && !isAbortError(err2) && !err2?.statusCode && attempt < 2 && /ECONNRESET|ECONNREFUSED|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|EPIPE|socket hang up|fetch failed|network|terminated|UND_ERR/i.test(String(err2?.message ?? err2?.code ?? err2));
3069
+ const sc = err2?.statusCode;
3070
+ const serverSide = sc >= 500 || sc === 408 || /Service Unavailable|overloaded|Internal server error|Bad gateway|Gateway time/i.test(String(err2?.message ?? ""));
3071
+ const network = !sc && /ECONNRESET|ECONNREFUSED|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|EPIPE|socket hang up|fetch failed|network|terminated|UND_ERR/i.test(String(err2?.message ?? err2?.code ?? err2));
3072
+ const transient = !o.signal?.aborted && !isAbortError(err2) && attempt < 2 && (network || serverSide);
3000
3073
  if (!transient) throw err2;
3001
3074
  const waitMs = 1e3 * (attempt + 1);
3002
3075
  log3.warn(`network drop mid-step (${err2?.message ?? err2}) \u2014 retrying in ${waitMs}ms`);
@@ -6079,8 +6152,8 @@ Reference files in them by their mount path (the left side).`;
6079
6152
  let realShell = [];
6080
6153
  const useRealShell = o.realShell ?? !virtual;
6081
6154
  if (useRealShell && !virtual) {
6082
- const jobs = new ShellJobRegistry({ cwd, killOnExit: true });
6083
- realShell = [makeRealShellTool({ cwd, registry: jobs }), ...makeShellJobTools(jobs)];
6155
+ const jobs = new ShellJobRegistry({ cwd, killOnExit: true, osSandbox: o.osSandbox });
6156
+ realShell = [makeRealShellTool({ cwd, registry: jobs, osSandbox: o.osSandbox }), ...makeShellJobTools(jobs)];
6084
6157
  }
6085
6158
  const scratchDir = o.scratch ? o.scratchDir ?? (virtual ? `${cwd}/.agent/scratch` : `${tmpdir()}/agentx-scratch-${process.pid}`) : void 0;
6086
6159
  const scratch = scratchDir ? new Scratch(fs, { dir: scratchDir }) : void 0;
@@ -8589,7 +8662,11 @@ function parseArgs(argv) {
8589
8662
  else if (x === "--seed") a.seed = true;
8590
8663
  else if (x === "--shell") a.shell = true;
8591
8664
  else if (x === "--no-shell") a.shell = false;
8592
- else if (x === "--subagents") a.subagents = true;
8665
+ else if (x === "--harden") a.harden = true;
8666
+ else if (x === "--harden-net") {
8667
+ a.harden = true;
8668
+ a.hardenNet = true;
8669
+ } else if (x === "--subagents") a.subagents = true;
8593
8670
  else if (x === "--duplex") a.duplex = true;
8594
8671
  else if (x === "--conversational" || x === "--convo" || x === "--voice") {
8595
8672
  a.voice = true;
@@ -8644,6 +8721,8 @@ Flags:
8644
8721
  surviving across runs \u2014 real disk is NEVER modified (DB-native; add --seed below)
8645
8722
  --seed with --boddb: hydrate the store from cwd on the first run (empty DB) only
8646
8723
  --shell force real /bin/sh on (default in disk mode); --no-shell to use VFS bash only
8724
+ --harden OS-sandbox the real shell (sandbox-exec/bwrap): writes confined to cwd+tmp,
8725
+ outbound network blocked; --harden-net keeps network allowed
8647
8726
  --plan plan mode: edits blocked until you approve a plan
8648
8727
  --ask confirm each mutating tool (bash/Shell/Write/Edit/\u2026)
8649
8728
  --yes, -y auto-approve mutating tools (no prompts) \u2014 for trusted/unattended runs
@@ -9164,6 +9243,7 @@ function optsFor(args, ai, cfg = {}, extraTools = []) {
9164
9243
  seed: args.seed,
9165
9244
  realShell: args.shell,
9166
9245
  // undefined → core.ts defaults (on for disk, off for sandbox/boddb)
9246
+ osSandbox: args.harden ? { network: !!args.hardenNet } : void 0,
9167
9247
  scratch: args.scratch,
9168
9248
  appendSystemPrompt: args.appendSystemPrompt,
9169
9249
  addDirs: args.addDirs,
@@ -9199,6 +9279,8 @@ async function makeAgent(args, ai, cfg, extraTools = []) {
9199
9279
  else if (args.boddb) err(dim(` \u229D boddb: files live in a database at ${args.boddb}${args.seed ? " (seeded from cwd on first run)" : ""} \u2014 the real disk will not be modified
9200
9280
  `));
9201
9281
  else if (args.shell === false) err(dim(" \u2296 --no-shell: VFS bash only (no real /bin/sh)\n"));
9282
+ if (args.harden && !virtual) err(dim(` \u26E8 hardened shell: writes confined to cwd+tmp${args.hardenNet ? "" : ", network blocked"} (sandbox-exec/bwrap)
9283
+ `));
9202
9284
  const agent = await buildAgent(optsFor(args, ai, cfg, extraTools));
9203
9285
  const display = displayHooks(agent.options.fs);
9204
9286
  agent.options.hooks = cfg.hooks ? composeHooks(display, hooksFromConfig(cfg.hooks)) : display;