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.
package/bareagent.context.md
CHANGED
|
@@ -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: ['/
|
|
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
|
|
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/', '/
|
|
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
|
-
//
|
|
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
|
|
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: [
|
|
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:
|
|
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.
|
|
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.
|
|
110
|
+
"bareguard": ">=0.19.2 <1.0.0",
|
|
111
111
|
"litectx": "^0.26.0",
|
|
112
112
|
"typescript": "^5.7.0"
|
|
113
113
|
}
|