@stratusagent/tool-fs 0.6.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Stratus Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,114 @@
1
+ # @stratusagent/tool-fs
2
+
3
+ The `fs` toolset: an agent reads, lists, and searches inside directories you
4
+ chose, and writes only with the permission engine's blessing.
5
+
6
+ This is a **plugin** — it contributes a toolset, and nothing loads it unless
7
+ your config says so. See [`plugins.md`](../../docs/architecture/plugins.md)
8
+ for what that means.
9
+
10
+ ## Install and enable
11
+
12
+ ```bash
13
+ npm install @stratusagent/tool-fs
14
+ ```
15
+
16
+ ```jsonc
17
+ // ~/.stratus/config.json
18
+ {
19
+ "plugins": {
20
+ "@stratusagent/tool-fs": {
21
+ "enabled": true,
22
+ "roots": ["~/notes"],
23
+ "agents": {
24
+ "ava": { "roots": ["~/work/ava"] },
25
+ "juno": { "roots": ["~/work/juno", "~/shared"] }
26
+ }
27
+ }
28
+ }
29
+ }
30
+ ```
31
+
32
+ Read from a **trusted** config only — `~/.stratus/config.json`, or a file you
33
+ passed with `--config`. A project-local `stratus.config.json` ships in any
34
+ repository somebody clones, and cannot decide which code runs in your daemon.
35
+
36
+ Installing the plugin grants no agent anything. The soul's allowlist is the
37
+ second gate, and the per-identity one:
38
+
39
+ ```markdown
40
+ ---
41
+ id: ava
42
+ tools: [fs.read, fs.search] # or fs.* for the whole toolset
43
+ ---
44
+ ```
45
+
46
+ ## Risk model
47
+
48
+ | Tool | Risk | What approval mode does |
49
+ | --- | --- | --- |
50
+ | `fs.read` | `safe` | Runs unattended. Nothing outside the agent's roots is readable, which is what makes reading safe rather than merely convenient. |
51
+ | `fs.list` | `safe` | Runs unattended. Symlinks are listed as symlinks and never followed. |
52
+ | `fs.search` | `safe` | Runs unattended. Matches **literal text** (optionally `wholeWord`), never a regular expression — see below. Given a directory it walks it without following symlinks, skipping `.git`, `node_modules`, and files over 1 MB; given a file it searches that file and no others. |
53
+ | `fs.write` | `gated` | `interactive` asks at the terminal, `remote` asks in Slack, `headless` refuses. Writing where other people read is the thing worth a person's attention. |
54
+
55
+ ## Settings
56
+
57
+ | Key | Default | What |
58
+ | --- | --- | --- |
59
+ | `roots` | none | Directories this agent may reach. **No roots means no filesystem** — nothing is readable, rather than everything. |
60
+ | `maxBytes` | `64000` | Cap on one `fs.read`, before the `truncated` marker. |
61
+ | `maxMatches` | `100` | Cap on `fs.search` matches. |
62
+ | `maxEntries` | `500` | Cap on `fs.list` entries. |
63
+
64
+ Every key can be set per agent in the `agents` sub-block, over the defaults
65
+ above it, and `roots` is why the sub-block exists: a flat list would give
66
+ every agent enabling `fs.*` the same roots, which is one agent reading
67
+ another's files.
68
+
69
+ ## `fs.search` takes literal text, not a pattern
70
+
71
+ A regular expression written by a model is untrusted input compiled into
72
+ V8's backtracking engine and run on the process's only thread. `(a+)+$`
73
+ against a long line does not return in any time worth waiting for, and while
74
+ it runs every session in the daemon is stopped — for a tool classified
75
+ `safe`, which means nobody is watching.
76
+
77
+ There is no timeout to give a running regex and no way to interrupt one from
78
+ the thread it is on, so the expressive form is gone rather than bounded.
79
+ `wholeWord` covers what it was mostly wanted for. Anything genuinely
80
+ regex-shaped is a `grep` away through
81
+ [`@stratusagent/tool-shell`](../tool-shell), where a human approves the
82
+ command and the executor can kill it.
83
+
84
+ ## The containment rule
85
+
86
+ Roots are an **access boundary between agents**, so they are resolved on
87
+ every call from `session.agent.id` — never read once when the plugin is
88
+ constructed. A plugin that closed over its roots at setup would hand every
89
+ agent whichever set resolved first.
90
+
91
+ Containment is decided between **real** paths, not strings:
92
+
93
+ - The requested path is normalized, then the deepest part of it that exists
94
+ is canonicalized, and *that* must be inside a canonicalized root. A
95
+ lexical check would catch `../../.stratus/credentials.json` and miss a
96
+ symlink inside the root pointing at the same file.
97
+ - For a path that does not exist yet, the canonicalized part is its nearest
98
+ existing ancestor — which is what makes writing `root/link/planted.txt`
99
+ through a symlinked `link` a refusal rather than a write into `/etc`.
100
+ - Opens use `O_NOFOLLOW`, and a file that existed at check time is compared
101
+ by inode after opening, so a name repointed in between fails instead of
102
+ being followed.
103
+
104
+ A path inside a root that is not a regular file — a fifo, a socket, a device
105
+ — is refused rather than opened. Opening one of those does not fail, it
106
+ *blocks*, and a tool call that never returns is worse than one that says no.
107
+ The opens carry `O_NONBLOCK` as well, so missing that check would be a
108
+ returned error rather than a turn that never ends.
109
+
110
+ What this does not do is walk the path one directory at a time holding each
111
+ open — that needs `openat`, which Node does not expose — so a swap of an
112
+ intermediate directory remains possible in principle. The roots you choose
113
+ are the boundary; this module is what stops the ordinary escapes from
114
+ crossing it.
@@ -0,0 +1,26 @@
1
+ import type { JsonObject, Plugin } from '@stratusagent/core';
2
+ export { canonicalRoots, expandHome, openContained, PathOutsideRootError, resolveWithinRoots, } from './paths.ts';
3
+ export interface FsPluginConfig extends JsonObject {
4
+ /** Directories this agent may reach. Nothing outside them is readable. */
5
+ roots?: string[];
6
+ /** Cap on a single read, before truncation. */
7
+ maxBytes?: number;
8
+ /** Cap on `fs.search` matches. */
9
+ maxMatches?: number;
10
+ /** Cap on `fs.list` entries. */
11
+ maxEntries?: number;
12
+ }
13
+ /**
14
+ * The filesystem toolset.
15
+ *
16
+ * Reads are `safe` and writes are `gated`, which is the risk model applied
17
+ * rather than a compromise: reading inside a directory a human chose is
18
+ * work an agent does on its own, and writing where other people read is the
19
+ * thing worth a person's attention. The roots are what make the first half
20
+ * true, so they are an access boundary and not a preference — resolved from
21
+ * `session.agent.id` on every call.
22
+ */
23
+ export declare const createFsPlugin: (config?: JsonObject) => Plugin;
24
+ /** The loader's ABI. See `docs/architecture/plugins.md`. */
25
+ export declare const createPlugin: (config?: JsonObject) => Plugin;
26
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAa,MAAM,EAAiB,MAAM,oBAAoB,CAAC;AAUvF,OAAO,EACL,cAAc,EACd,UAAU,EACV,aAAa,EACb,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,YAAY,CAAC;AAQpB,MAAM,WAAW,cAAe,SAAQ,UAAU;IAChD,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kCAAkC;IAClC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gCAAgC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AA+XD;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,GAAI,SAAQ,UAAe,KAAG,MAQvD,CAAC;AAEH,4DAA4D;AAC5D,eAAO,MAAM,YAAY,YAXc,UAAU,KAAQ,MAWf,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,385 @@
1
+ import { constants } from 'node:fs';
2
+ import { mkdir, readdir, readFile, stat } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { resolvePluginAgentConfig } from '@stratusagent/plugins';
6
+ import { isRealDirectory, openContained, resolveWithinRoots, } from "./paths.js";
7
+ export { canonicalRoots, expandHome, openContained, PathOutsideRootError, resolveWithinRoots, } from "./paths.js";
8
+ /** Defaults chosen so a tool result is something a model can actually read. */
9
+ const DEFAULT_MAX_BYTES = 64_000;
10
+ const DEFAULT_MAX_MATCHES = 100;
11
+ const DEFAULT_MAX_SEARCH_FILE_BYTES = 1_000_000;
12
+ const DEFAULT_MAX_ENTRIES = 500;
13
+ const asStrings = (value) => Array.isArray(value) ? value.filter((entry) => typeof entry === 'string') : [];
14
+ const asNumber = (value, fallback) => typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : fallback;
15
+ const requireString = (input, key) => {
16
+ const value = input[key];
17
+ if (typeof value !== 'string' || value.length === 0) {
18
+ throw new Error(`${key} is required and must be a string.`);
19
+ }
20
+ return value;
21
+ };
22
+ /**
23
+ * What this agent may reach — resolved **per call**, from the session.
24
+ *
25
+ * Not a convenience: two agents with different roots is the whole point of
26
+ * the per-agent block, and a plugin that read its roots once at setup would
27
+ * hand every agent whichever set happened to be resolved first. That is one
28
+ * agent reading another's files, and it is the failure this signature
29
+ * exists to make impossible to write.
30
+ */
31
+ const settingsFor = (config, session) => {
32
+ const resolved = resolvePluginAgentConfig(config, session.agent.id);
33
+ return {
34
+ roots: asStrings(resolved.roots),
35
+ maxBytes: asNumber(resolved.maxBytes, DEFAULT_MAX_BYTES),
36
+ maxMatches: asNumber(resolved.maxMatches, DEFAULT_MAX_MATCHES),
37
+ maxEntries: asNumber(resolved.maxEntries, DEFAULT_MAX_ENTRIES),
38
+ home: typeof resolved.home === 'string' ? resolved.home : os.homedir(),
39
+ };
40
+ };
41
+ /**
42
+ * Whether a buffer is text, decided the way `grep` decides it: a NUL byte
43
+ * means binary. Returning a megabyte of mojibake to a model helps nobody,
44
+ * and returning "this is binary" is an answer it can act on.
45
+ */
46
+ const looksBinary = (buffer) => buffer.includes(0);
47
+ const relativeTo = (root, target) => path.relative(root, target) || '.';
48
+ const readContained = async (resolved, maxBytes) => {
49
+ if (resolved.kind === 'directory') {
50
+ throw new Error(`${resolved.path} is a directory; use fs.list.`);
51
+ }
52
+ if (resolved.kind === 'other') {
53
+ // A FIFO, a socket, a device. Opening one does not fail — it *blocks*,
54
+ // and a tool call that never returns is worse than one that refuses.
55
+ throw new Error(`${resolved.path} is not a regular file.`);
56
+ }
57
+ // `O_NONBLOCK` alongside the check above, and not instead of it: the check
58
+ // gives a refusal an agent can read, and this makes the failure mode of
59
+ // *missing* one a returned error rather than a turn that never ends. It
60
+ // has no effect on a regular file.
61
+ const handle = await openContained(resolved, constants.O_RDONLY | (constants.O_NONBLOCK ?? 0));
62
+ try {
63
+ const info = await handle.stat();
64
+ if (info.isDirectory()) {
65
+ throw new Error(`${resolved.path} is a directory; use fs.list.`);
66
+ }
67
+ const length = Math.min(info.size, maxBytes);
68
+ const buffer = Buffer.alloc(length);
69
+ const { bytesRead } = await handle.read(buffer, 0, length, 0);
70
+ const slice = buffer.subarray(0, bytesRead);
71
+ if (looksBinary(slice)) {
72
+ return { content: '', truncated: false, bytes: bytesRead, size: info.size, binary: true };
73
+ }
74
+ return {
75
+ content: slice.toString('utf8'),
76
+ truncated: info.size > bytesRead,
77
+ bytes: bytesRead,
78
+ size: info.size,
79
+ binary: false,
80
+ };
81
+ }
82
+ finally {
83
+ await handle.close();
84
+ }
85
+ };
86
+ const createReadTool = (config) => ({
87
+ name: 'fs.read',
88
+ description: 'Read a UTF-8 text file inside one of this agent’s roots.',
89
+ risk: 'safe',
90
+ parameters: {
91
+ type: 'object',
92
+ properties: {
93
+ path: { type: 'string', description: 'Absolute, or relative to the agent’s first root.' },
94
+ maxBytes: { type: 'number', description: 'Stop after this many bytes.' },
95
+ },
96
+ required: ['path'],
97
+ },
98
+ async execute(input, session) {
99
+ const settings = settingsFor(config, session);
100
+ const requested = requireString(input, 'path');
101
+ const resolved = await resolveWithinRoots(settings.roots, requested, { home: settings.home });
102
+ const maxBytes = asNumber(input.maxBytes, settings.maxBytes);
103
+ const result = await readContained(resolved, maxBytes);
104
+ return {
105
+ path: relativeTo(resolved.root, resolved.path),
106
+ absolutePath: resolved.path,
107
+ ...(result.binary
108
+ ? { binary: true, bytes: result.size }
109
+ : {
110
+ content: result.content,
111
+ bytes: result.bytes,
112
+ size: result.size,
113
+ // Said outright rather than left for the model to notice: a
114
+ // silently clipped file reads as a complete one, and a summary
115
+ // written from half a document is confidently wrong.
116
+ truncated: result.truncated,
117
+ }),
118
+ };
119
+ },
120
+ });
121
+ const entryKind = (entry) => {
122
+ if (entry.isSymbolicLink())
123
+ return 'symlink';
124
+ if (entry.isDirectory())
125
+ return 'directory';
126
+ if (entry.isFile())
127
+ return 'file';
128
+ return 'other';
129
+ };
130
+ const createListTool = (config) => ({
131
+ name: 'fs.list',
132
+ description: 'List a directory inside one of this agent’s roots.',
133
+ risk: 'safe',
134
+ parameters: {
135
+ type: 'object',
136
+ properties: {
137
+ path: { type: 'string', description: 'Defaults to the agent’s first root.' },
138
+ },
139
+ },
140
+ async execute(input, session) {
141
+ const settings = settingsFor(config, session);
142
+ const requested = typeof input.path === 'string' && input.path.length > 0 ? input.path : '.';
143
+ const resolved = await resolveWithinRoots(settings.roots, requested, { home: settings.home });
144
+ if (!(await isRealDirectory(resolved.path))) {
145
+ throw new Error(`${requested} is not a directory; use fs.read.`);
146
+ }
147
+ const entries = await readdir(resolved.path, { withFileTypes: true });
148
+ const listed = entries.slice(0, settings.maxEntries);
149
+ const detailed = await Promise.all(listed.map(async (entry) => {
150
+ const full = path.join(resolved.path, entry.name);
151
+ let size;
152
+ if (entry.isFile()) {
153
+ try {
154
+ size = (await stat(full)).size;
155
+ }
156
+ catch {
157
+ size = undefined;
158
+ }
159
+ }
160
+ return {
161
+ name: entry.name,
162
+ // Symlinks are reported as symlinks and never followed here: a
163
+ // listing that resolved them would describe files outside the root
164
+ // as if they were inside it.
165
+ type: entryKind(entry),
166
+ ...(size === undefined ? {} : { size }),
167
+ };
168
+ }));
169
+ return {
170
+ path: relativeTo(resolved.root, resolved.path),
171
+ entries: detailed,
172
+ truncated: entries.length > listed.length,
173
+ };
174
+ },
175
+ });
176
+ const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
177
+ /**
178
+ * The matcher for one search.
179
+ *
180
+ * Built from an **escaped literal**, always. `fs.search` is `safe` — it
181
+ * runs unattended, on the process's only thread, with a pattern a model
182
+ * wrote — and V8's regex engine backtracks: `(a+)+$` against a long line of
183
+ * `a`s does not return in any time worth waiting for, and while it runs
184
+ * every session in the daemon is stopped. There is no timeout to give it
185
+ * and no way to interrupt it on this thread.
186
+ *
187
+ * So the expressive form is gone rather than bounded. `wholeWord` covers
188
+ * the case it was mostly wanted for; anything genuinely regex-shaped is a
189
+ * `shell.run` away, where a human approves the command and the executor
190
+ * can kill it.
191
+ */
192
+ const matcherFor = (query, options) => {
193
+ const literal = escapeRegExp(query);
194
+ return new RegExp(options.wholeWord ? `\\b${literal}\\b` : literal, options.caseSensitive ? '' : 'i');
195
+ };
196
+ const walkFiles = async function* (directory, depth = 0) {
197
+ if (depth > 12) {
198
+ return;
199
+ }
200
+ let entries;
201
+ try {
202
+ entries = await readdir(directory, { withFileTypes: true });
203
+ }
204
+ catch {
205
+ return;
206
+ }
207
+ for (const entry of entries) {
208
+ // Never through a symlink: following one would walk out of the root,
209
+ // and the containment check only ran on the directory we were given.
210
+ if (entry.isSymbolicLink()) {
211
+ continue;
212
+ }
213
+ const full = path.join(directory, entry.name);
214
+ if (entry.isDirectory()) {
215
+ if (entry.name === '.git' || entry.name === 'node_modules') {
216
+ continue;
217
+ }
218
+ yield* walkFiles(full, depth + 1);
219
+ continue;
220
+ }
221
+ if (entry.isFile()) {
222
+ yield full;
223
+ }
224
+ }
225
+ };
226
+ const createSearchTool = (config) => ({
227
+ name: 'fs.search',
228
+ description: 'Search file contents for literal text under one of this agent’s roots.',
229
+ risk: 'safe',
230
+ parameters: {
231
+ type: 'object',
232
+ properties: {
233
+ query: { type: 'string', description: 'Literal text to find. Not a regular expression.' },
234
+ path: { type: 'string', description: 'Where to search. Defaults to the agent’s first root.' },
235
+ wholeWord: { type: 'boolean', description: 'Match only whole words.' },
236
+ caseSensitive: { type: 'boolean' },
237
+ maxMatches: { type: 'number' },
238
+ },
239
+ required: ['query'],
240
+ },
241
+ async execute(input, session) {
242
+ const settings = settingsFor(config, session);
243
+ const query = requireString(input, 'query');
244
+ const requested = typeof input.path === 'string' && input.path.length > 0 ? input.path : '.';
245
+ const resolved = await resolveWithinRoots(settings.roots, requested, { home: settings.home });
246
+ const limit = asNumber(input.maxMatches, settings.maxMatches);
247
+ const pattern = matcherFor(query, {
248
+ caseSensitive: input.caseSensitive === true,
249
+ wholeWord: input.wholeWord === true,
250
+ });
251
+ const matches = [];
252
+ let truncated = false;
253
+ // A file is searched as itself. Falling back to its parent would answer
254
+ // a question nobody asked — `{ path: 'notes/today.md' }` coming back
255
+ // with hits from every other note is a wrong answer, not a generous one.
256
+ if (resolved.kind === 'other') {
257
+ // The same trap `fs.read` has: `readFile` on a fifo with no writer
258
+ // does not fail, it waits — and `fs.search` is `safe`, so it waits
259
+ // with nobody watching. The walk below cannot hit this (a directory
260
+ // entry is only yielded when it is a regular file); a path handed to
261
+ // us directly can.
262
+ throw new Error(`${resolved.path} is not a regular file.`);
263
+ }
264
+ const files = resolved.kind === 'directory'
265
+ ? walkFiles(resolved.path)
266
+ : (async function* single() {
267
+ yield resolved.path;
268
+ })();
269
+ for await (const file of files) {
270
+ if (matches.length >= limit) {
271
+ truncated = true;
272
+ break;
273
+ }
274
+ let contents;
275
+ try {
276
+ const info = await stat(file);
277
+ if (info.size > DEFAULT_MAX_SEARCH_FILE_BYTES) {
278
+ continue;
279
+ }
280
+ contents = await readFile(file);
281
+ }
282
+ catch {
283
+ continue;
284
+ }
285
+ if (looksBinary(contents)) {
286
+ continue;
287
+ }
288
+ const lines = contents.toString('utf8').split('\n');
289
+ for (const [index, line] of lines.entries()) {
290
+ if (!pattern.test(line)) {
291
+ continue;
292
+ }
293
+ if (matches.length >= limit) {
294
+ truncated = true;
295
+ break;
296
+ }
297
+ matches.push({
298
+ path: relativeTo(resolved.root, file),
299
+ line: index + 1,
300
+ text: line.length > 400 ? `${line.slice(0, 400)}…` : line,
301
+ });
302
+ }
303
+ }
304
+ return { query, matches, truncated };
305
+ },
306
+ });
307
+ const createWriteTool = (config) => ({
308
+ name: 'fs.write',
309
+ description: 'Write a UTF-8 text file inside one of this agent’s roots.',
310
+ // Gated, not safe: it acts on the world outside Stratus and outlives the
311
+ // turn. Reading inside a root a human chose is the thing that does not.
312
+ risk: 'gated',
313
+ parameters: {
314
+ type: 'object',
315
+ properties: {
316
+ path: { type: 'string' },
317
+ content: { type: 'string' },
318
+ append: { type: 'boolean' },
319
+ },
320
+ required: ['path', 'content'],
321
+ },
322
+ async execute(input, session) {
323
+ const settings = settingsFor(config, session);
324
+ const requested = requireString(input, 'path');
325
+ const content = typeof input.content === 'string' ? input.content : '';
326
+ const resolved = await resolveWithinRoots(settings.roots, requested, {
327
+ home: settings.home,
328
+ allowMissing: true,
329
+ });
330
+ if (!resolved.exists) {
331
+ // The parent is created only after the resolution above put it inside
332
+ // a root — `root/link/notes/x.md` through a symlinked `link` never
333
+ // gets this far, so nothing is created outside the boundary either.
334
+ await mkdir(path.dirname(resolved.path), { recursive: true });
335
+ }
336
+ else if (resolved.kind !== 'file') {
337
+ // Same reason as the read path: writing into a directory is a mistake
338
+ // worth naming, and writing into a fifo or a device blocks forever.
339
+ throw new Error(resolved.kind === 'directory'
340
+ ? `${resolved.path} is a directory.`
341
+ : `${resolved.path} is not a regular file.`);
342
+ }
343
+ const flags = input.append === true
344
+ ? constants.O_WRONLY | constants.O_CREAT | constants.O_APPEND
345
+ : constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC;
346
+ // O_NOFOLLOW is applied by openContained: a symlink sitting at the
347
+ // destination must fail rather than write through to its target, which
348
+ // is the write half of the escape a read-only check would miss.
349
+ const handle = await openContained(resolved, flags | (constants.O_NONBLOCK ?? 0), 0o644);
350
+ try {
351
+ await handle.writeFile(content, 'utf8');
352
+ }
353
+ finally {
354
+ await handle.close();
355
+ }
356
+ return {
357
+ path: relativeTo(resolved.root, resolved.path),
358
+ absolutePath: resolved.path,
359
+ bytes: Buffer.byteLength(content, 'utf8'),
360
+ appended: input.append === true,
361
+ };
362
+ },
363
+ });
364
+ /**
365
+ * The filesystem toolset.
366
+ *
367
+ * Reads are `safe` and writes are `gated`, which is the risk model applied
368
+ * rather than a compromise: reading inside a directory a human chose is
369
+ * work an agent does on its own, and writing where other people read is the
370
+ * thing worth a person's attention. The roots are what make the first half
371
+ * true, so they are an access boundary and not a preference — resolved from
372
+ * `session.agent.id` on every call.
373
+ */
374
+ export const createFsPlugin = (config = {}) => ({
375
+ name: '@stratusagent/tool-fs',
376
+ setup(context) {
377
+ context.tools.register(createReadTool(config));
378
+ context.tools.register(createListTool(config));
379
+ context.tools.register(createSearchTool(config));
380
+ context.tools.register(createWriteTool(config));
381
+ },
382
+ });
383
+ /** The loader's ABI. See `docs/architecture/plugins.md`. */
384
+ export const createPlugin = createFsPlugin;
385
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,75 @@
1
+ import { open } from 'node:fs/promises';
2
+ /** A path that is not inside anything this agent was given. */
3
+ export declare class PathOutsideRootError extends Error {
4
+ constructor(message: string);
5
+ }
6
+ /** `~` and `~/x` mean the same thing here as they do in a shell. */
7
+ export declare const expandHome: (value: string, home?: string) => string;
8
+ /**
9
+ * The roots an agent may reach, canonicalized.
10
+ *
11
+ * A root can itself be a symlink — `~/work` pointing at a volume is
12
+ * ordinary — so containment is decided between real paths on both sides.
13
+ * Comparing a real target against a symlinked root would refuse every
14
+ * legitimate read; comparing a lexical target against a real root would
15
+ * accept every escape.
16
+ */
17
+ export declare const canonicalRoots: (roots: readonly string[], home?: string) => Promise<string[]>;
18
+ export interface ResolvedPath {
19
+ /** The canonical path to act on. */
20
+ path: string;
21
+ /** The canonical root it was found inside. */
22
+ root: string;
23
+ /** True when something is already there. */
24
+ exists: boolean;
25
+ /**
26
+ * The file this resolution was decided about, identified by inode rather
27
+ * than by name — see `openContained`.
28
+ */
29
+ identity?: {
30
+ dev: number;
31
+ ino: number;
32
+ };
33
+ /** What is actually there: a regular file, a directory, or something else. */
34
+ kind?: 'file' | 'directory' | 'other';
35
+ }
36
+ /**
37
+ * Resolve a requested path to a real one inside a real root, or refuse it.
38
+ *
39
+ * Lexical checks are not enough and this is the reason the whole module
40
+ * exists: `notes/../../.stratus/credentials.json` is caught by normalizing,
41
+ * but a *symlink* inside an allowed root pointing at the credential store
42
+ * is not — the string never leaves the root. So resolution canonicalizes
43
+ * the deepest part of the path that exists, and requires **that** to be
44
+ * inside the root.
45
+ *
46
+ * For a path that does not exist yet, the canonicalized part is its nearest
47
+ * existing ancestor, which is what makes the write-through-parent case a
48
+ * refusal: writing `root/link/passwd` where `link` points at `/etc`
49
+ * resolves to `/etc`, and `/etc` is not inside the root however innocent
50
+ * the requested string looks.
51
+ */
52
+ export declare const resolveWithinRoots: (roots: readonly string[], requested: string, options?: {
53
+ home?: string;
54
+ allowMissing?: boolean;
55
+ }) => Promise<ResolvedPath>;
56
+ /**
57
+ * Open a resolved path without following a symlink, and prove afterwards
58
+ * that what opened is what was checked.
59
+ *
60
+ * Between resolution and open there is a window in which the name can be
61
+ * repointed at something else — the classic symlink-swap race. Two things
62
+ * close it as far as this platform allows: `O_NOFOLLOW`, so a name that
63
+ * *became* a symlink fails instead of being followed, and an inode
64
+ * comparison after the fact, because a name that was replaced with a
65
+ * different real file is a different inode even though the string matches.
66
+ *
67
+ * What this cannot do is walk the path a directory at a time holding each
68
+ * one open — that needs `openat`, which Node does not expose. A swap of an
69
+ * intermediate directory therefore remains possible in principle, which is
70
+ * why the roots themselves are the boundary and not this function alone.
71
+ */
72
+ export declare const openContained: (resolved: ResolvedPath, flags: number, mode?: number) => Promise<Awaited<ReturnType<typeof open>>>;
73
+ /** Whether a directory entry is a real directory (never through a symlink). */
74
+ export declare const isRealDirectory: (target: string) => Promise<boolean>;
75
+ //# sourceMappingURL=paths.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"AACA,OAAO,EAAS,IAAI,EAAkB,MAAM,kBAAkB,CAAC;AAI/D,+DAA+D;AAC/D,qBAAa,oBAAqB,SAAQ,KAAK;gBACjC,OAAO,EAAE,MAAM;CAI5B;AAED,oEAAoE;AACpE,eAAO,MAAM,UAAU,GAAI,OAAO,MAAM,EAAE,aAAmB,KAAG,MAQ/D,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,GAAU,OAAO,SAAS,MAAM,EAAE,EAAE,aAAmB,KAAG,OAAO,CAAC,MAAM,EAAE,CAapG,CAAC;AAKF,MAAM,WAAW,YAAY;IAC3B,oCAAoC;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,MAAM,EAAE,OAAO,CAAC;IAChB;;;OAGG;IACH,QAAQ,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IACxC,8EAA8E;IAC9E,IAAI,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,OAAO,CAAC;CACvC;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,kBAAkB,GAC7B,OAAO,SAAS,MAAM,EAAE,EACxB,WAAW,MAAM,EACjB,UAAS;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,OAAO,CAAA;CAAO,KACtD,OAAO,CAAC,YAAY,CAsEtB,CAAC;AAIF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,aAAa,GACxB,UAAU,YAAY,EACtB,OAAO,MAAM,EACb,OAAO,MAAM,KACZ,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC,CAY1C,CAAC;AAEF,+EAA+E;AAC/E,eAAO,MAAM,eAAe,GAAU,QAAQ,MAAM,KAAG,OAAO,CAAC,OAAO,CAMrE,CAAC"}
package/dist/paths.js ADDED
@@ -0,0 +1,162 @@
1
+ import { constants } from 'node:fs';
2
+ import { lstat, open, realpath, stat } from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ /** A path that is not inside anything this agent was given. */
6
+ export class PathOutsideRootError extends Error {
7
+ constructor(message) {
8
+ super(message);
9
+ this.name = 'PathOutsideRootError';
10
+ }
11
+ }
12
+ /** `~` and `~/x` mean the same thing here as they do in a shell. */
13
+ export const expandHome = (value, home = os.homedir()) => {
14
+ if (value === '~') {
15
+ return home;
16
+ }
17
+ if (value.startsWith('~/')) {
18
+ return path.join(home, value.slice(2));
19
+ }
20
+ return value;
21
+ };
22
+ /**
23
+ * The roots an agent may reach, canonicalized.
24
+ *
25
+ * A root can itself be a symlink — `~/work` pointing at a volume is
26
+ * ordinary — so containment is decided between real paths on both sides.
27
+ * Comparing a real target against a symlinked root would refuse every
28
+ * legitimate read; comparing a lexical target against a real root would
29
+ * accept every escape.
30
+ */
31
+ export const canonicalRoots = async (roots, home = os.homedir()) => {
32
+ const resolved = [];
33
+ for (const root of roots) {
34
+ const absolute = path.resolve(expandHome(root, home));
35
+ try {
36
+ resolved.push(await realpath(absolute));
37
+ }
38
+ catch {
39
+ // A root that does not exist yet grants nothing, rather than
40
+ // granting its lexical prefix.
41
+ continue;
42
+ }
43
+ }
44
+ return resolved;
45
+ };
46
+ const contains = (root, candidate) => candidate === root || candidate.startsWith(`${root}${path.sep}`);
47
+ /**
48
+ * Resolve a requested path to a real one inside a real root, or refuse it.
49
+ *
50
+ * Lexical checks are not enough and this is the reason the whole module
51
+ * exists: `notes/../../.stratus/credentials.json` is caught by normalizing,
52
+ * but a *symlink* inside an allowed root pointing at the credential store
53
+ * is not — the string never leaves the root. So resolution canonicalizes
54
+ * the deepest part of the path that exists, and requires **that** to be
55
+ * inside the root.
56
+ *
57
+ * For a path that does not exist yet, the canonicalized part is its nearest
58
+ * existing ancestor, which is what makes the write-through-parent case a
59
+ * refusal: writing `root/link/passwd` where `link` points at `/etc`
60
+ * resolves to `/etc`, and `/etc` is not inside the root however innocent
61
+ * the requested string looks.
62
+ */
63
+ export const resolveWithinRoots = async (roots, requested, options = {}) => {
64
+ const home = options.home ?? os.homedir();
65
+ const canonical = await canonicalRoots(roots, home);
66
+ if (canonical.length === 0) {
67
+ throw new PathOutsideRootError('No filesystem roots are configured for this agent, so nothing is readable. '
68
+ + 'Set roots for it under plugins["@stratusagent/tool-fs"].');
69
+ }
70
+ const expanded = expandHome(requested, home);
71
+ // A relative path is relative to the agent's first root, which is what an
72
+ // agent means by `notes/today.md`. It is normalized before anything else,
73
+ // so `../` is spent against the root rather than against the daemon's cwd.
74
+ const absolute = path.isAbsolute(expanded)
75
+ ? path.normalize(expanded)
76
+ : path.resolve(canonical[0], expanded);
77
+ const missing = [];
78
+ let cursor = absolute;
79
+ let real;
80
+ for (;;) {
81
+ try {
82
+ real = await realpath(cursor);
83
+ break;
84
+ }
85
+ catch (error) {
86
+ if (error.code !== 'ENOENT') {
87
+ throw error;
88
+ }
89
+ const parent = path.dirname(cursor);
90
+ if (parent === cursor) {
91
+ break;
92
+ }
93
+ missing.unshift(path.basename(cursor));
94
+ cursor = parent;
95
+ }
96
+ }
97
+ if (real === undefined) {
98
+ throw new PathOutsideRootError(`Path does not resolve to anything inside an allowed root: ${requested}`);
99
+ }
100
+ const root = canonical.find((candidate) => contains(candidate, real));
101
+ if (!root) {
102
+ throw new PathOutsideRootError(`Refusing ${requested}: it resolves to ${real}${missing.length > 0 ? `/${missing.join('/')}` : ''}, `
103
+ + `which is outside every root this agent may reach (${canonical.join(', ')}).`);
104
+ }
105
+ if (missing.length > 0) {
106
+ const resolved = path.join(real, ...missing);
107
+ if (!options.allowMissing) {
108
+ const error = new Error(`No such file or directory: ${requested}`);
109
+ error.code = 'ENOENT';
110
+ throw error;
111
+ }
112
+ return { path: resolved, root, exists: false };
113
+ }
114
+ const resolved = real;
115
+ const info = await lstat(resolved);
116
+ return {
117
+ path: resolved,
118
+ root,
119
+ exists: true,
120
+ identity: { dev: info.dev, ino: info.ino },
121
+ // A realpath never ends at a symlink, so this is what the file is.
122
+ kind: info.isFile() ? 'file' : info.isDirectory() ? 'directory' : 'other',
123
+ };
124
+ };
125
+ const O_NOFOLLOW = constants.O_NOFOLLOW ?? 0;
126
+ /**
127
+ * Open a resolved path without following a symlink, and prove afterwards
128
+ * that what opened is what was checked.
129
+ *
130
+ * Between resolution and open there is a window in which the name can be
131
+ * repointed at something else — the classic symlink-swap race. Two things
132
+ * close it as far as this platform allows: `O_NOFOLLOW`, so a name that
133
+ * *became* a symlink fails instead of being followed, and an inode
134
+ * comparison after the fact, because a name that was replaced with a
135
+ * different real file is a different inode even though the string matches.
136
+ *
137
+ * What this cannot do is walk the path a directory at a time holding each
138
+ * one open — that needs `openat`, which Node does not expose. A swap of an
139
+ * intermediate directory therefore remains possible in principle, which is
140
+ * why the roots themselves are the boundary and not this function alone.
141
+ */
142
+ export const openContained = async (resolved, flags, mode) => {
143
+ const handle = await open(resolved.path, flags | O_NOFOLLOW, mode);
144
+ if (resolved.identity) {
145
+ const info = await handle.stat();
146
+ if (info.dev !== resolved.identity.dev || info.ino !== resolved.identity.ino) {
147
+ await handle.close();
148
+ throw new PathOutsideRootError(`Refusing ${resolved.path}: it changed between the containment check and the open.`);
149
+ }
150
+ }
151
+ return handle;
152
+ };
153
+ /** Whether a directory entry is a real directory (never through a symlink). */
154
+ export const isRealDirectory = async (target) => {
155
+ try {
156
+ return (await stat(target)).isDirectory();
157
+ }
158
+ catch {
159
+ return false;
160
+ }
161
+ };
162
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,96 @@
1
+ {
2
+ "name": "@stratusagent/tool-fs",
3
+ "version": "0.6.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ },
12
+ "./package.json": "./package.json"
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "engines": {
18
+ "node": ">=22.13 <23 || >=23.4"
19
+ },
20
+ "dependencies": {
21
+ "@stratusagent/core": "0.6.0",
22
+ "@stratusagent/plugins": "0.6.0"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^24.6.0",
26
+ "typescript": "^5.9.3"
27
+ },
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/stratuslabs/agent.git",
32
+ "directory": "packages/tool-fs"
33
+ },
34
+ "description": "Filesystem tools for Stratus agents: read, list, search, and write inside per-agent roots, with symlink-safe containment",
35
+ "keywords": [
36
+ "stratus",
37
+ "agent",
38
+ "ai",
39
+ "llm",
40
+ "stratus-plugin",
41
+ "tools",
42
+ "filesystem"
43
+ ],
44
+ "publishConfig": {
45
+ "access": "public"
46
+ },
47
+ "stratus": {
48
+ "pluginVersion": 1,
49
+ "contributes": {
50
+ "tools": [
51
+ {
52
+ "name": "fs.read",
53
+ "risk": "safe"
54
+ },
55
+ {
56
+ "name": "fs.list",
57
+ "risk": "safe"
58
+ },
59
+ {
60
+ "name": "fs.search",
61
+ "risk": "safe"
62
+ },
63
+ {
64
+ "name": "fs.write",
65
+ "risk": "gated"
66
+ }
67
+ ]
68
+ },
69
+ "config": {
70
+ "type": "object",
71
+ "properties": {
72
+ "roots": {
73
+ "type": "array",
74
+ "items": {
75
+ "type": "string"
76
+ }
77
+ },
78
+ "maxBytes": {
79
+ "type": "integer"
80
+ },
81
+ "maxMatches": {
82
+ "type": "integer"
83
+ },
84
+ "maxEntries": {
85
+ "type": "integer"
86
+ }
87
+ },
88
+ "additionalProperties": false
89
+ }
90
+ },
91
+ "scripts": {
92
+ "build": "tsc -b",
93
+ "typecheck": "tsc --noEmit --project tsconfig.test.json",
94
+ "test": "node --test --experimental-strip-types"
95
+ }
96
+ }