bare-agent 0.48.0 → 0.48.1

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.
@@ -448,7 +448,7 @@ const { createShellTools } = require('bare-agent/tools');
448
448
  const gate = new Gate({
449
449
  budget: { maxCostUsd: 0.50 },
450
450
  limits: { maxTurns: 20 },
451
- fs: { readScope: ['/tmp', '~/Projects'], deny: ['/etc'] },
451
+ fs: { readScope: ['/srv/agent', '~/Projects'], deny: ['/etc'] }, // list REAL paths (bareguard >=0.19.2 throws on a symlinked scope root; on macOS /tmp is one — use fs.realpathSync(os.tmpdir()))
452
452
  bash: { allow: ['ls', 'cat', 'grep', 'ps', 'df'] }, // argv[0] allowlist
453
453
  audit: { path: './audit.jsonl' },
454
454
  humanChannel: async (event) => ({ decision: 'deny' }), // wire to your UI
@@ -1427,7 +1427,7 @@ Mobile tools follow the observe-act pattern: action tools auto-return a fresh sn
1427
1427
 
1428
1428
  **Zero baked-in allowlist.** The library ships the primitives; gating is bareguard's job via the standard `wireGate(gate)` wiring.
1429
1429
 
1430
- > **Path canonicalization + symlink refusal, together.** bareguard's own resolved-path (symlink) containment check is planned for an upcoming release, not yet published; until then the gate matches paths lexically, so a symlinked parent dir is NOT contained even when the link sits inside an allowed scope. bare-agent canonicalizes the path (`~` + resolve, via `resolveToolPath`) before the gate check and opens that same path — this closes a *different* gap (the gate judging a `~`/relative path in a different form than the one the tool opens), not the lexical-match gap above. `noFollowSymlinks` refuses a symlinked final path *component* at open time. Together they narrow but do not close the check-then-open window, and neither covers hardlinks.
1430
+ > **Path canonicalization + symlink refusal, together.** bareguard >=0.19.0 already realpath-resolves the checked path and every scope root on each call, so a symlink under an allowed scope that resolves outside it is denied (`fs.<scope>.symlinkEscape`; dangling → `danglingSymlink`). bareguard 0.19.2 additionally requires the scope ROOTS themselves not be, or sit under, a symlink: `new Gate` throws otherwise, and a root swapped for a symlink later denies `fs.readScope.symlinkRoot` / `fs.writeScope.symlinkRoot` — list real paths (e.g. `fs.realpathSync(os.tmpdir())`; on macOS `/tmp` and `os.tmpdir()` are symlinks). The checked path must ALSO pass a lexical check (`within(path, scopeRoot)`) before the resolved one, so its lexical form must lie under a realpath'd scope root: with a symlinked `$HOME`, `~/x` expands (via `resolveToolPath`, which does not realpath) to `<symlink>/x` and is DENIED against a realpath'd home scope — fail-closed, a usability gap not a leak; pass the real path. bare-agent canonicalizes the path (`~` + resolve, via `resolveToolPath`) before the gate check and opens that same path — this closes a *different* gap (the gate judging a `~`/relative path in a different form than the one the tool opens), not the symlink-containment gap, which is bareguard's job. `noFollowSymlinks` refuses a symlinked final path *component* at open time. Together they narrow but do not close the check-then-open window, and neither covers hardlinks.
1431
1431
  >
1432
1432
  > **Opt-in symlink refusal:** `createShellTools({ noFollowSymlinks: true })` makes `shell_read`, `shell_write`, `shell_edit`, and `shell_grep` refuse to open a path whose **final** path component is a symlink — a file, a dir, or dangling — instead of silently following it. Refusal is a **thrown** error with `err.code === 'ELOOP'` (feeds the Loop's `maxIdenticalToolErrors` guard like any other tool error), and for `shell_write` the link's target is never created or touched. Scope is the final component **only** — a symlinked *parent* directory is not covered (per the canonicalization note above), and neither is the `cwd` option of `shell_run`/`shell_exec` — it only gets `~` expansion, never this flag or `resolveToolPath`'s full canonicalization, so a symlinked `cwd` is always followed. `shell_grep` refuses only a symlinked **root** path loudly, via the same atomic open every other tool uses (open-then-fstat, not a look-then-open `lstat`); a file that turns into a symlink partway through the walk is skipped silently, the same as any other read error. The check is atomic (`O_NOFOLLOW`) on linux/macOS; on Windows, which has no `O_NOFOLLOW`-equivalent open flag, it falls back to a non-atomic `lstat`-then-open check — a symlink planted in that gap slips through. A directory listing (`shell_read` on a dir, `shell_grep`'s root when it's a dir) additionally rechecks the path's `dev`+`ino` against the open handle right after listing — Node has no `fdopendir`, so the listing itself still goes by path, and this narrows (does not close) the window where a directory swapped in between the open and the listing would report a different directory's names; a swap-and-swap-back inside that narrow window still passes undetected. Default `false` — omitting the option is byte-identical to the pre-existing behavior.
1433
1433
  >
@@ -1451,7 +1451,7 @@ const gate = new Gate({
1451
1451
  // Hard-deny shell_exec for this agent. tools.denylist short-circuits before content checks.
1452
1452
  tools: { denylist: ['shell_exec'] },
1453
1453
  // fs scope for shell_read / shell_grep.
1454
- fs: { readScope: ['/home/', '/tmp/'] },
1454
+ fs: { readScope: ['/home/', '/srv/agent/'] }, // real paths only — see the symlink note above
1455
1455
  audit: { path: './shell-audit.jsonl' },
1456
1456
  humanChannel: async (event) => ({ decision: 'deny' }),
1457
1457
  });
@@ -31,17 +31,20 @@ const { createShellTools } = require('bare-agent/tools');
31
31
  // floor), a shell file action denies by default when its scope is unset (fs.readScope.unset /
32
32
  // fs.writeScope.unset) — leaving writeScope out here would silently deny every shell_write /
33
33
  // shell_edit call, not leave the primitive dead like on older bareguard.
34
- // readScope is deliberately WIDER than writeScope here — read is fine anywhere under /tmp,
34
+ // Scope roots are realpath'd: bareguard >=0.19.2 throws at `new Gate` on a root that is or
35
+ // contains a symlink (macOS /tmp and os.tmpdir() are symlinks), so list real paths.
36
+ // readScope is deliberately WIDER than writeScope here — read is fine anywhere under the tmp root,
35
37
  // but writes are narrowed to one demo subdir, to show the two scopes are independent (a
36
38
  // folder listed only in readScope is NOT writable, and vice versa). The demo dir is created
37
39
  // below since fs.writeScope doesn't need the path to pre-exist, but the example's own
38
40
  // shell_write call does.
39
- const writeDir = path.join(os.tmpdir(), 'bare-agent-demo');
41
+ const TMP = fs.realpathSync(os.tmpdir());
42
+ const writeDir = path.join(TMP, 'bare-agent-demo');
40
43
  fs.mkdirSync(writeDir, { recursive: true });
41
44
  const gate = new Gate({
42
45
  budget: { maxCostUsd: 0.10 }, // hard USD cap
43
46
  limits: { maxTurns: 20 }, // safety net on think/act cycles
44
- fs: { readScope: ['/tmp'], writeScope: [writeDir] }, // read: anywhere under /tmp; write: one narrower demo dir
47
+ fs: { readScope: [TMP], writeScope: [writeDir] }, // read: anywhere under the tmp root; write: one narrower demo dir
45
48
  bash: { allow: ['ls', 'cat', 'echo', 'pwd'] }, // argv[0] allowlist for shell_run
46
49
  audit: { path: './bareagent-audit.jsonl' },
47
50
  // Required by bareguard: any ask/halt event flows through here.
@@ -87,7 +90,7 @@ const loop = new Loop({
87
90
 
88
91
  // 4. Run. Pass the tools as-is — gating is via policy/onToolResult, not by wrapping execute().
89
92
  const result = await loop.run(
90
- [{ role: 'user', content: 'List the contents of /tmp using shell_run with argv ["ls", "/tmp"].' }],
93
+ [{ role: 'user', content: `List the contents of ${TMP} using shell_run with argv ["ls", "${TMP}"].` }],
91
94
  tools,
92
95
  );
93
96
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bare-agent",
3
- "version": "0.48.0",
3
+ "version": "0.48.1",
4
4
  "files": [
5
5
  "index.js",
6
6
  "index.d.ts",
@@ -107,7 +107,7 @@
107
107
  },
108
108
  "devDependencies": {
109
109
  "@types/node": "^22.19.19",
110
- "bareguard": ">=0.19.0 <1.0.0",
110
+ "bareguard": ">=0.19.2 <1.0.0",
111
111
  "litectx": "^0.26.0",
112
112
  "typescript": "^5.7.0"
113
113
  }