agent.libx.js 0.94.12 → 0.94.14

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);
@@ -6082,8 +6152,8 @@ Reference files in them by their mount path (the left side).`;
6082
6152
  let realShell = [];
6083
6153
  const useRealShell = o.realShell ?? !virtual;
6084
6154
  if (useRealShell && !virtual) {
6085
- const jobs = new ShellJobRegistry({ cwd, killOnExit: true });
6086
- 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)];
6087
6157
  }
6088
6158
  const scratchDir = o.scratch ? o.scratchDir ?? (virtual ? `${cwd}/.agent/scratch` : `${tmpdir()}/agentx-scratch-${process.pid}`) : void 0;
6089
6159
  const scratch = scratchDir ? new Scratch(fs, { dir: scratchDir }) : void 0;
@@ -8592,7 +8662,11 @@ function parseArgs(argv) {
8592
8662
  else if (x === "--seed") a.seed = true;
8593
8663
  else if (x === "--shell") a.shell = true;
8594
8664
  else if (x === "--no-shell") a.shell = false;
8595
- 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;
8596
8670
  else if (x === "--duplex") a.duplex = true;
8597
8671
  else if (x === "--conversational" || x === "--convo" || x === "--voice") {
8598
8672
  a.voice = true;
@@ -8647,6 +8721,8 @@ Flags:
8647
8721
  surviving across runs \u2014 real disk is NEVER modified (DB-native; add --seed below)
8648
8722
  --seed with --boddb: hydrate the store from cwd on the first run (empty DB) only
8649
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
8650
8726
  --plan plan mode: edits blocked until you approve a plan
8651
8727
  --ask confirm each mutating tool (bash/Shell/Write/Edit/\u2026)
8652
8728
  --yes, -y auto-approve mutating tools (no prompts) \u2014 for trusted/unattended runs
@@ -9167,6 +9243,7 @@ function optsFor(args, ai, cfg = {}, extraTools = []) {
9167
9243
  seed: args.seed,
9168
9244
  realShell: args.shell,
9169
9245
  // undefined → core.ts defaults (on for disk, off for sandbox/boddb)
9246
+ osSandbox: args.harden ? { network: !!args.hardenNet } : void 0,
9170
9247
  scratch: args.scratch,
9171
9248
  appendSystemPrompt: args.appendSystemPrompt,
9172
9249
  addDirs: args.addDirs,
@@ -9202,6 +9279,8 @@ async function makeAgent(args, ai, cfg, extraTools = []) {
9202
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
9203
9280
  `));
9204
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
+ `));
9205
9284
  const agent = await buildAgent(optsFor(args, ai, cfg, extraTools));
9206
9285
  const display = displayHooks(agent.options.fs);
9207
9286
  agent.options.hooks = cfg.hooks ? composeHooks(display, hooksFromConfig(cfg.hooks)) : display;