auggy 0.3.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.
Files changed (121) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/LICENSE +201 -0
  3. package/README.md +161 -0
  4. package/package.json +76 -0
  5. package/src/agent-card.ts +39 -0
  6. package/src/agent.ts +283 -0
  7. package/src/agentmail-client.ts +138 -0
  8. package/src/augments/bash/index.ts +463 -0
  9. package/src/augments/bash/skill/SKILL.md +156 -0
  10. package/src/augments/budgets/budget-store.ts +513 -0
  11. package/src/augments/budgets/index.ts +134 -0
  12. package/src/augments/budgets/preamble.ts +93 -0
  13. package/src/augments/budgets/types.ts +89 -0
  14. package/src/augments/file-memory/index.ts +71 -0
  15. package/src/augments/filesystem/index.ts +533 -0
  16. package/src/augments/filesystem/skill/SKILL.md +142 -0
  17. package/src/augments/filesystem/skill/references/mount-permissions.md +81 -0
  18. package/src/augments/layered-memory/extractor/buffer.ts +56 -0
  19. package/src/augments/layered-memory/extractor/frequency.ts +79 -0
  20. package/src/augments/layered-memory/extractor/inject-handler.ts +103 -0
  21. package/src/augments/layered-memory/extractor/parse.ts +75 -0
  22. package/src/augments/layered-memory/extractor/prompt.md +26 -0
  23. package/src/augments/layered-memory/index.ts +757 -0
  24. package/src/augments/layered-memory/skill/SKILL.md +153 -0
  25. package/src/augments/layered-memory/storage/migrations/README.md +16 -0
  26. package/src/augments/layered-memory/storage/migrations/supabase-add-fact-fields.sql +9 -0
  27. package/src/augments/layered-memory/storage/sqlite-store.ts +352 -0
  28. package/src/augments/layered-memory/storage/supabase-store.ts +263 -0
  29. package/src/augments/layered-memory/storage/types.ts +98 -0
  30. package/src/augments/link/index.ts +489 -0
  31. package/src/augments/link/translate.ts +261 -0
  32. package/src/augments/notify/adapters/agentmail.ts +70 -0
  33. package/src/augments/notify/adapters/telegram.ts +60 -0
  34. package/src/augments/notify/adapters/webhook.ts +55 -0
  35. package/src/augments/notify/index.ts +284 -0
  36. package/src/augments/notify/skill/SKILL.md +150 -0
  37. package/src/augments/org-context/index.ts +721 -0
  38. package/src/augments/org-context/skill/SKILL.md +96 -0
  39. package/src/augments/skills/index.ts +103 -0
  40. package/src/augments/supabase-memory/index.ts +151 -0
  41. package/src/augments/telegram-transport/index.ts +312 -0
  42. package/src/augments/telegram-transport/polling.ts +55 -0
  43. package/src/augments/telegram-transport/webhook.ts +56 -0
  44. package/src/augments/turn-control/index.ts +61 -0
  45. package/src/augments/turn-control/skill/SKILL.md +155 -0
  46. package/src/augments/visitor-auth/email-validation.ts +66 -0
  47. package/src/augments/visitor-auth/index.ts +779 -0
  48. package/src/augments/visitor-auth/rate-limiter.ts +90 -0
  49. package/src/augments/visitor-auth/skill/SKILL.md +55 -0
  50. package/src/augments/visitor-auth/storage/sqlite-store.ts +398 -0
  51. package/src/augments/visitor-auth/storage/types.ts +164 -0
  52. package/src/augments/visitor-auth/types.ts +123 -0
  53. package/src/augments/visitor-auth/verify-page.ts +179 -0
  54. package/src/augments/web-fetch/index.ts +331 -0
  55. package/src/augments/web-fetch/skill/SKILL.md +100 -0
  56. package/src/cli/agent-index.ts +289 -0
  57. package/src/cli/augment-catalog.ts +320 -0
  58. package/src/cli/augment-resolver.ts +597 -0
  59. package/src/cli/commands/add-skill.ts +194 -0
  60. package/src/cli/commands/add.ts +87 -0
  61. package/src/cli/commands/chat.ts +207 -0
  62. package/src/cli/commands/create.ts +462 -0
  63. package/src/cli/commands/dev.ts +139 -0
  64. package/src/cli/commands/eval.ts +180 -0
  65. package/src/cli/commands/ls.ts +66 -0
  66. package/src/cli/commands/remove.ts +95 -0
  67. package/src/cli/commands/restart.ts +40 -0
  68. package/src/cli/commands/start.ts +123 -0
  69. package/src/cli/commands/status.ts +104 -0
  70. package/src/cli/commands/stop.ts +84 -0
  71. package/src/cli/commands/visitors-revoke.ts +155 -0
  72. package/src/cli/commands/visitors.ts +101 -0
  73. package/src/cli/config-parser.ts +1034 -0
  74. package/src/cli/engine-resolver.ts +68 -0
  75. package/src/cli/index.ts +178 -0
  76. package/src/cli/model-picker.ts +89 -0
  77. package/src/cli/pid-registry.ts +146 -0
  78. package/src/cli/plist-generator.ts +117 -0
  79. package/src/cli/resolve-config.ts +56 -0
  80. package/src/cli/scaffold-skills.ts +158 -0
  81. package/src/cli/scaffold.ts +291 -0
  82. package/src/cli/skill-frontmatter.ts +51 -0
  83. package/src/cli/skill-validator.ts +151 -0
  84. package/src/cli/types.ts +228 -0
  85. package/src/cli/yaml-helpers.ts +66 -0
  86. package/src/engines/_shared/cost.ts +55 -0
  87. package/src/engines/_shared/schema-normalize.ts +75 -0
  88. package/src/engines/anthropic/pricing.ts +117 -0
  89. package/src/engines/anthropic.ts +483 -0
  90. package/src/engines/openai/pricing.ts +67 -0
  91. package/src/engines/openai.ts +446 -0
  92. package/src/engines/openrouter/pricing.ts +83 -0
  93. package/src/engines/openrouter.ts +185 -0
  94. package/src/helpers.ts +24 -0
  95. package/src/http.ts +387 -0
  96. package/src/index.ts +165 -0
  97. package/src/kernel/capability-table.ts +172 -0
  98. package/src/kernel/context-allocator.ts +161 -0
  99. package/src/kernel/history-manager.ts +198 -0
  100. package/src/kernel/lifecycle-manager.ts +106 -0
  101. package/src/kernel/output-validator.ts +35 -0
  102. package/src/kernel/preamble.ts +23 -0
  103. package/src/kernel/route-collector.ts +97 -0
  104. package/src/kernel/timeout.ts +21 -0
  105. package/src/kernel/tool-selector.ts +47 -0
  106. package/src/kernel/trace-emitter.ts +66 -0
  107. package/src/kernel/transport-queue.ts +147 -0
  108. package/src/kernel/turn-loop.ts +1148 -0
  109. package/src/memory/context-synthesis.ts +83 -0
  110. package/src/memory/memory-bus.ts +61 -0
  111. package/src/memory/registry.ts +80 -0
  112. package/src/memory/tools.ts +320 -0
  113. package/src/memory/types.ts +8 -0
  114. package/src/parts.ts +30 -0
  115. package/src/scaffold-templates/identity.md +31 -0
  116. package/src/telegram-client.ts +145 -0
  117. package/src/tokenizer.ts +14 -0
  118. package/src/transports/ag-ui-events.ts +253 -0
  119. package/src/transports/visitor-token.ts +82 -0
  120. package/src/transports/web-transport.ts +948 -0
  121. package/src/types.ts +1009 -0
@@ -0,0 +1,533 @@
1
+ import { z } from "zod";
2
+ import { readFile, writeFile, readdir, mkdir, rm, realpath, stat, lstat } from "node:fs/promises";
3
+ import { resolve, join, relative, extname, isAbsolute, sep } from "node:path";
4
+ import { Glob } from "bun";
5
+ import type { Augment, ContextBlock } from "../../types";
6
+ import { defineTool } from "../../helpers";
7
+
8
+ /**
9
+ * Filesystem augment — scoped, multi-mount file access for Auggy agents.
10
+ *
11
+ * The operator declares named mounts, each with its own physical path
12
+ * and permission level. The model sees logical paths (mount-name/...)
13
+ * and the augment resolves to physical paths with security enforcement.
14
+ *
15
+ * Security model:
16
+ * - fs.realpath() resolves symlinks before every boundary check
17
+ * - startsWith() against the realpath'd mount root prevents traversal
18
+ * - Per-mount read/write/delete permissions enforced per operation
19
+ * - Binary file detection on read prevents garbage in tool results
20
+ * - maxReadSize truncation prevents large files from blowing context
21
+ *
22
+ * The mount model follows the Docker volumes pattern: operators declare
23
+ * boundaries, the augment enforces them, the model sees logical paths.
24
+ *
25
+ * IMPORTANT: Filesystem mount paths must NOT overlap with fileMemory
26
+ * source paths. If the same file is owned by fileMemory (cached at boot)
27
+ * and accessible via a writable filesystem mount, writes through the
28
+ * filesystem augment won't invalidate fileMemory's cache, causing stale
29
+ * context on subsequent turns. This is an operator responsibility in v1;
30
+ * future versions may enforce it at defineAgent time via augment metadata.
31
+ */
32
+
33
+ export interface FsMount {
34
+ /** Logical name the model uses as the first path segment. */
35
+ name: string;
36
+ /** Physical path on disk. */
37
+ path: string;
38
+ /** Allow fs_write and fs_mkdir. Default false. */
39
+ writable?: boolean;
40
+ /** Allow fs_remove. Default false. Requires writable. */
41
+ deletable?: boolean;
42
+ /** Max bytes returned by fs_read. Default 262144 (256KB). */
43
+ maxReadSize?: number;
44
+ /** Max bytes accepted by fs_write. Default 1048576 (1MB). */
45
+ maxWriteSize?: number;
46
+ /** Glob patterns excluded from fs_search. Default [".git", "node_modules"]. */
47
+ searchExcludes?: string[];
48
+ }
49
+
50
+ export interface FilesystemOptions {
51
+ /** Named mount points the agent can access. */
52
+ mounts: FsMount[];
53
+ /**
54
+ * Optional SKILL.md path. If provided, the file is boot-loaded and
55
+ * returned as an evictable context block on each turn — teaching the
56
+ * model when/why/how to use the filesystem tools.
57
+ */
58
+ skillFile?: string;
59
+ }
60
+
61
+ const DEFAULT_MAX_READ = 256 * 1024; // 256KB
62
+ const DEFAULT_MAX_WRITE = 1024 * 1024; // 1MB
63
+ const DEFAULT_SEARCH_EXCLUDES = [".git", "node_modules", ".next", "__pycache__", ".DS_Store"];
64
+
65
+ const BINARY_EXTENSIONS = new Set([
66
+ ".png",
67
+ ".jpg",
68
+ ".jpeg",
69
+ ".gif",
70
+ ".bmp",
71
+ ".ico",
72
+ ".webp",
73
+ ".svg",
74
+ ".pdf",
75
+ ".zip",
76
+ ".gz",
77
+ ".tar",
78
+ ".bz2",
79
+ ".7z",
80
+ ".rar",
81
+ ".mp3",
82
+ ".mp4",
83
+ ".avi",
84
+ ".mov",
85
+ ".wav",
86
+ ".flac",
87
+ ".woff",
88
+ ".woff2",
89
+ ".ttf",
90
+ ".otf",
91
+ ".eot",
92
+ ".exe",
93
+ ".dll",
94
+ ".so",
95
+ ".dylib",
96
+ ".o",
97
+ ".a",
98
+ ".wasm",
99
+ ".pyc",
100
+ ".class",
101
+ ]);
102
+
103
+ /**
104
+ * Boundary check via `path.relative()`. Rejects:
105
+ * - targets whose relative path escapes the mount ("..", "../foo", etc.)
106
+ * - targets on a different filesystem root (Windows cross-drive —
107
+ * `relative()` returns an absolute path in that case).
108
+ * Accepts the mount root itself (relative "") and any descendant.
109
+ *
110
+ * Chose `relative()` over `startsWith(mountRoot + sep)` because the
111
+ * separator-suffix form breaks when mountRoot is itself a filesystem root
112
+ * (e.g. "/" on POSIX → `mountRoot + sep` becomes "//", which never matches
113
+ * any real child path). The relative-based check handles both the
114
+ * root-mount case and the prefix-collision case (mount `/var/data/work`
115
+ * vs sibling `/var/data/workspace`) uniformly. Exported for testability.
116
+ */
117
+ export function isWithinMount(realTarget: string, mountRoot: string): boolean {
118
+ const rel = relative(mountRoot, realTarget);
119
+ if (rel === "") return true;
120
+ if (rel === ".." || rel.startsWith(`..${sep}`)) return false;
121
+ if (isAbsolute(rel)) return false;
122
+ return true;
123
+ }
124
+
125
+ /**
126
+ * Filesystem augment factory.
127
+ *
128
+ * Usage:
129
+ * ```ts
130
+ * filesystem({
131
+ * mounts: [
132
+ * { name: "skills", path: "./augments", writable: false },
133
+ * { name: "workspace", path: "./workspace", writable: true, deletable: true },
134
+ * { name: "repo", path: "/repos/platform", writable: false },
135
+ * ],
136
+ * })
137
+ * ```
138
+ */
139
+ export function filesystem(opts: FilesystemOptions): Augment {
140
+ // Validate mount names are unique
141
+ const names = new Set<string>();
142
+ for (const m of opts.mounts) {
143
+ if (names.has(m.name)) {
144
+ throw new Error(`filesystem: duplicate mount name "${m.name}"`);
145
+ }
146
+ if (m.name.includes("/") || m.name.includes("\\")) {
147
+ throw new Error(`filesystem: mount name "${m.name}" must not contain path separators`);
148
+ }
149
+ if (m.deletable && !m.writable) {
150
+ throw new Error(
151
+ `filesystem: mount "${m.name}" is deletable but not writable — deletable requires writable`,
152
+ );
153
+ }
154
+ names.add(m.name);
155
+ }
156
+
157
+ const mountMap = new Map<string, FsMount>();
158
+ const resolvedRoots = new Map<string, string>();
159
+ let cachedSkill: string | null = null;
160
+
161
+ // --- Path resolution and security ---
162
+
163
+ async function resolveMountRoot(mount: FsMount): Promise<string> {
164
+ const cached = resolvedRoots.get(mount.name);
165
+ if (cached) return cached;
166
+ try {
167
+ const real = await realpath(resolve(mount.path));
168
+ resolvedRoots.set(mount.name, real);
169
+ return real;
170
+ } catch {
171
+ // Mount path doesn't exist yet — resolve without following symlinks
172
+ const resolved = resolve(mount.path);
173
+ resolvedRoots.set(mount.name, resolved);
174
+ return resolved;
175
+ }
176
+ }
177
+
178
+ function parseLogicalPath(logicalPath: string): {
179
+ mountName: string;
180
+ subPath: string;
181
+ } {
182
+ const normalized = logicalPath.replace(/\\/g, "/");
183
+ const firstSlash = normalized.indexOf("/");
184
+ if (firstSlash === -1) {
185
+ return { mountName: normalized, subPath: "." };
186
+ }
187
+ return {
188
+ mountName: normalized.slice(0, firstSlash),
189
+ subPath: normalized.slice(firstSlash + 1) || ".",
190
+ };
191
+ }
192
+
193
+ async function resolveAndValidate(
194
+ logicalPath: string,
195
+ requireMount?: (m: FsMount) => string | null,
196
+ ): Promise<{ physicalPath: string; mount: FsMount }> {
197
+ const { mountName, subPath } = parseLogicalPath(logicalPath);
198
+ const mount = mountMap.get(mountName);
199
+ if (!mount) {
200
+ throw new Error(
201
+ `Unknown mount "${mountName}". Available mounts: ${[...mountMap.keys()].join(", ")}`,
202
+ );
203
+ }
204
+
205
+ // Permission check
206
+ if (requireMount) {
207
+ const err = requireMount(mount);
208
+ if (err) throw new Error(err);
209
+ }
210
+
211
+ const mountRoot = await resolveMountRoot(mount);
212
+ const targetPath = resolve(mountRoot, subPath);
213
+
214
+ // Resolve symlinks on the target to catch symlink escapes
215
+ let realTarget: string;
216
+ try {
217
+ realTarget = await realpath(targetPath);
218
+ } catch {
219
+ // Target doesn't exist yet (for writes/mkdirs) — use the resolved
220
+ // path but still validate it's within the mount boundary
221
+ realTarget = targetPath;
222
+ }
223
+
224
+ if (!isWithinMount(realTarget, mountRoot)) {
225
+ throw new Error(`Path "${logicalPath}" resolves outside mount "${mountName}" boundary`);
226
+ }
227
+
228
+ return { physicalPath: realTarget, mount };
229
+ }
230
+
231
+ // --- Tools ---
232
+
233
+ const fsRead = defineTool({
234
+ name: "fs_read",
235
+ description:
236
+ "Read file contents from a mounted directory. Path format: mount-name/path/to/file. Use fs_list first to check file sizes before reading large files.",
237
+ category: "meta",
238
+ input: z.object({
239
+ path: z.string().describe("Logical path: mount-name/path/to/file"),
240
+ }),
241
+ execute: async ({ path: logicalPath }) => {
242
+ const { physicalPath, mount } = await resolveAndValidate(logicalPath);
243
+
244
+ // Check if it's a symlink pointing outside (extra safety)
245
+ const lstats = await lstat(physicalPath).catch(() => null);
246
+ if (lstats?.isSymbolicLink()) {
247
+ const realTarget = await realpath(physicalPath);
248
+ const mountRoot = await resolveMountRoot(mount);
249
+ if (!isWithinMount(realTarget, mountRoot)) {
250
+ return `Error: Symlink "${logicalPath}" points outside mount boundary`;
251
+ }
252
+ }
253
+
254
+ // Binary detection
255
+ const ext = extname(physicalPath).toLowerCase();
256
+ if (BINARY_EXTENSIONS.has(ext)) {
257
+ const stats = await stat(physicalPath);
258
+ return `Error: Binary file (${ext}, ${formatSize(stats.size)}). Use fs_list to see metadata.`;
259
+ }
260
+
261
+ // Read with size cap
262
+ const maxRead = mount.maxReadSize ?? DEFAULT_MAX_READ;
263
+ const stats = await stat(physicalPath);
264
+
265
+ if (stats.isDirectory()) {
266
+ return `Error: "${logicalPath}" is a directory. Use fs_list instead.`;
267
+ }
268
+
269
+ const content = await Bun.file(physicalPath).slice(0, maxRead).text();
270
+
271
+ if (stats.size > maxRead) {
272
+ return `${content}\n\n[truncated at ${formatSize(maxRead)}, total size: ${formatSize(stats.size)}]`;
273
+ }
274
+ return content;
275
+ },
276
+ });
277
+
278
+ const fsWrite = defineTool({
279
+ name: "fs_write",
280
+ description:
281
+ "Write content to a file in a writable mount. Creates parent directories automatically. Path format: mount-name/path/to/file.",
282
+ category: "meta",
283
+ input: z.object({
284
+ path: z.string().describe("Logical path: mount-name/path/to/file"),
285
+ content: z.string().describe("File content to write"),
286
+ }),
287
+ execute: async ({ path: logicalPath, content }) => {
288
+ const { physicalPath, mount } = await resolveAndValidate(logicalPath, (m) =>
289
+ m.writable ? null : `Mount "${m.name}" is read-only`,
290
+ );
291
+
292
+ const maxWrite = mount.maxWriteSize ?? DEFAULT_MAX_WRITE;
293
+ if (content.length > maxWrite) {
294
+ return `Error: Content exceeds max write size (${formatSize(content.length)} > ${formatSize(maxWrite)})`;
295
+ }
296
+
297
+ // Ensure parent directory exists
298
+ const parentDir = physicalPath.slice(0, physicalPath.lastIndexOf("/"));
299
+ await mkdir(parentDir, { recursive: true });
300
+
301
+ await writeFile(physicalPath, content, "utf-8");
302
+ return `Written ${formatSize(content.length)} to "${logicalPath}"`;
303
+ },
304
+ });
305
+
306
+ const fsList = defineTool({
307
+ name: "fs_list",
308
+ description:
309
+ "List directory contents with file sizes and types. Path format: mount-name/path/to/dir. Omit the path after mount name to list the mount root.",
310
+ category: "meta",
311
+ input: z.object({
312
+ path: z.string().describe("Logical path: mount-name or mount-name/path/to/dir"),
313
+ }),
314
+ execute: async ({ path: logicalPath }) => {
315
+ const { physicalPath } = await resolveAndValidate(logicalPath);
316
+
317
+ const stats = await stat(physicalPath);
318
+ if (!stats.isDirectory()) {
319
+ // Single file stat
320
+ return JSON.stringify({
321
+ path: logicalPath,
322
+ type: "file",
323
+ size: stats.size,
324
+ sizeFormatted: formatSize(stats.size),
325
+ modified: stats.mtime.toISOString(),
326
+ });
327
+ }
328
+
329
+ const entries = await readdir(physicalPath, { withFileTypes: true });
330
+ const results = await Promise.all(
331
+ entries
332
+ .filter((e) => !e.name.startsWith(".") || e.name === ".gitignore")
333
+ .map(async (entry) => {
334
+ const entryPath = join(physicalPath, entry.name);
335
+ try {
336
+ const s = await stat(entryPath);
337
+ return {
338
+ name: entry.name,
339
+ type: entry.isDirectory() ? "dir" : "file",
340
+ size: entry.isDirectory() ? undefined : s.size,
341
+ sizeFormatted: entry.isDirectory() ? undefined : formatSize(s.size),
342
+ modified: s.mtime.toISOString(),
343
+ };
344
+ } catch {
345
+ return {
346
+ name: entry.name,
347
+ type: "unknown",
348
+ };
349
+ }
350
+ }),
351
+ );
352
+
353
+ // Sort: directories first, then files, alphabetical within each
354
+ results.sort((a, b) => {
355
+ if (a.type === "dir" && b.type !== "dir") return -1;
356
+ if (a.type !== "dir" && b.type === "dir") return 1;
357
+ return a.name.localeCompare(b.name);
358
+ });
359
+
360
+ return JSON.stringify({ path: logicalPath, entries: results });
361
+ },
362
+ });
363
+
364
+ const fsMkdir = defineTool({
365
+ name: "fs_mkdir",
366
+ description:
367
+ "Create a directory (and parent directories) in a writable mount. Path format: mount-name/path/to/new-dir.",
368
+ category: "meta",
369
+ input: z.object({
370
+ path: z.string().describe("Logical path for the new directory"),
371
+ }),
372
+ execute: async ({ path: logicalPath }) => {
373
+ const { physicalPath } = await resolveAndValidate(logicalPath, (m) =>
374
+ m.writable ? null : `Mount "${m.name}" is read-only`,
375
+ );
376
+ await mkdir(physicalPath, { recursive: true });
377
+ return `Created directory "${logicalPath}"`;
378
+ },
379
+ });
380
+
381
+ const fsRemove = defineTool({
382
+ name: "fs_remove",
383
+ description:
384
+ "Delete a file or empty directory in a deletable mount. Path format: mount-name/path/to/target. Will not delete non-empty directories.",
385
+ category: "meta",
386
+ input: z.object({
387
+ path: z.string().describe("Logical path to the file or empty directory to remove"),
388
+ }),
389
+ execute: async ({ path: logicalPath }) => {
390
+ const { physicalPath, mount } = await resolveAndValidate(logicalPath, (m) => {
391
+ if (!m.writable) return `Mount "${m.name}" is read-only`;
392
+ if (!m.deletable) return `Mount "${m.name}" does not allow deletion`;
393
+ return null;
394
+ });
395
+
396
+ const stats = await stat(physicalPath);
397
+ if (stats.isDirectory()) {
398
+ // Only remove empty directories
399
+ const entries = await readdir(physicalPath);
400
+ if (entries.length > 0) {
401
+ return `Error: Directory "${logicalPath}" is not empty (${entries.length} entries). Remove contents first.`;
402
+ }
403
+ await rm(physicalPath, { recursive: false });
404
+ return `Removed empty directory "${logicalPath}"`;
405
+ }
406
+
407
+ // Prevent deleting the mount root itself
408
+ const mountRoot = await resolveMountRoot(mount);
409
+ if (physicalPath === mountRoot) {
410
+ return `Error: Cannot delete mount root "${mount.name}"`;
411
+ }
412
+
413
+ await rm(physicalPath);
414
+ return `Removed file "${logicalPath}"`;
415
+ },
416
+ });
417
+
418
+ const fsSearch = defineTool({
419
+ name: "fs_search",
420
+ description:
421
+ "Search for files matching a glob pattern within a mount. Returns up to 100 results. Excludes .git and node_modules by default. Path format: mount-name or mount-name/subdir.",
422
+ category: "meta",
423
+ input: z.object({
424
+ path: z.string().describe("Mount name or mount-name/subdir to search within"),
425
+ pattern: z.string().describe('Glob pattern (e.g. "*.md", "**/*.ts", "config.*")'),
426
+ maxResults: z.number().optional().describe("Max results to return (default 100)"),
427
+ }),
428
+ execute: async ({ path: logicalPath, pattern, maxResults }) => {
429
+ const { physicalPath, mount } = await resolveAndValidate(logicalPath);
430
+ const cap = Math.min(maxResults ?? 100, 1000);
431
+
432
+ const excludes = mount.searchExcludes ?? DEFAULT_SEARCH_EXCLUDES;
433
+
434
+ const glob = new Glob(pattern);
435
+ const results: string[] = [];
436
+
437
+ for await (const entry of glob.scan({
438
+ cwd: physicalPath,
439
+ absolute: false,
440
+ dot: false,
441
+ })) {
442
+ // Check excludes
443
+ const shouldExclude = excludes.some(
444
+ (ex) => entry.includes(`/${ex}/`) || entry.startsWith(`${ex}/`) || entry === ex,
445
+ );
446
+ if (shouldExclude) continue;
447
+
448
+ results.push(`${logicalPath}/${entry}`);
449
+ if (results.length >= cap) break;
450
+ }
451
+
452
+ if (results.length === 0) {
453
+ return `No files matching "${pattern}" in "${logicalPath}"`;
454
+ }
455
+
456
+ const truncated = results.length >= cap;
457
+ return JSON.stringify({
458
+ pattern,
459
+ searchPath: logicalPath,
460
+ results,
461
+ count: results.length,
462
+ truncated,
463
+ });
464
+ },
465
+ });
466
+
467
+ // --- Augment definition ---
468
+
469
+ return {
470
+ name: "filesystem",
471
+ capabilities: ["tools", "context"],
472
+ constraints: {
473
+ maxToolCallsPerTurn: 15,
474
+ // Structural Layer 1 defaults: mutation tools are hidden from the
475
+ // untrusted peer's tool list entirely. Destruction is further restricted
476
+ // to facility/operator. Mount-level `writable` / `deletable` flags are a
477
+ // separate, complementary defense — they run inside the tool after it
478
+ // has already been exposed and called. perTrustLevel runs before the
479
+ // model sees the tool.
480
+ perTrustLevel: {
481
+ public: { neverExpose: ["fs_write", "fs_mkdir", "fs_remove"] },
482
+ agent: { neverExpose: ["fs_remove"] },
483
+ },
484
+ },
485
+
486
+ tools: [fsRead, fsWrite, fsList, fsMkdir, fsRemove, fsSearch],
487
+
488
+ async onBoot() {
489
+ // Resolve and cache all mount roots at boot
490
+ for (const mount of opts.mounts) {
491
+ mountMap.set(mount.name, mount);
492
+ await resolveMountRoot(mount);
493
+ }
494
+
495
+ // Boot-load SKILL.md if provided
496
+ if (opts.skillFile) {
497
+ try {
498
+ cachedSkill = await readFile(opts.skillFile, "utf-8");
499
+ } catch (err) {
500
+ // SKILL.md is optional — missing file is not a boot failure,
501
+ // but log so the operator knows the teaching layer is absent.
502
+ console.warn(`filesystem: failed to load SKILL.md from "${opts.skillFile}": ${err}`);
503
+ }
504
+ }
505
+ },
506
+
507
+ context:
508
+ cachedSkill !== null || opts.skillFile
509
+ ? async (): Promise<ContextBlock[]> => {
510
+ if (!cachedSkill) return [];
511
+ return [
512
+ {
513
+ source: "filesystem",
514
+ content: cachedSkill,
515
+ placement: "preamble",
516
+ provenance: "augment",
517
+ priority: "evictable",
518
+ eviction: "drop",
519
+ origin: "operator",
520
+ },
521
+ ];
522
+ }
523
+ : undefined,
524
+ };
525
+ }
526
+
527
+ // --- Helpers ---
528
+
529
+ function formatSize(bytes: number): string {
530
+ if (bytes < 1024) return `${bytes}B`;
531
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
532
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
533
+ }
@@ -0,0 +1,142 @@
1
+ ---
2
+ name: filesystem
3
+ description: Read, write, search, and manage files across named mount points. Use when the agent needs to access skill references, manage workspace files, read external repositories, or write output to shared directories.
4
+ ---
5
+
6
+ # Filesystem Tools
7
+
8
+ You have access to 6 filesystem tools that operate on **named mounts** — scoped directories the operator configured for you. Every path you use starts with a mount name.
9
+
10
+ ## Available mounts
11
+
12
+ Check which mounts are available and what permissions you have by calling `fs_list` on each mount name. The operator configures mounts — you cannot create new ones.
13
+
14
+ ## Tools
15
+
16
+ | Tool | What it does | When to use |
17
+ |------|-------------|-------------|
18
+ | `fs_read(path)` | Read file contents | When you need a file's content — ALWAYS check size first via `fs_list` |
19
+ | `fs_write(path, content)` | Write/create a file | When you need to save work, create notes, write output |
20
+ | `fs_list(path)` | List directory with sizes and types | **Before reading** — to see what's there and how big files are |
21
+ | `fs_mkdir(path)` | Create a directory | When organizing output into subdirectories |
22
+ | `fs_remove(path)` | Delete a file or empty directory | When cleaning up temporary files |
23
+ | `fs_search(path, pattern)` | Find files matching a glob | When looking for specific files across a directory tree |
24
+
25
+ ## Path format
26
+
27
+ Every path starts with a mount name:
28
+
29
+ ```
30
+ mount-name/path/to/file
31
+ ```
32
+
33
+ <example name="path-examples">
34
+ ```
35
+ skills/memory/SKILL.md → reads from the "skills" mount
36
+ workspace/notes/2026-04-10.md → writes to the "workspace" mount
37
+ repo/src/components/Header.tsx → reads from the "repo" mount
38
+ ```
39
+ </example>
40
+
41
+ To list a mount's root, just pass the mount name: `fs_list("workspace")`.
42
+
43
+ ## Critical rules
44
+
45
+ ### 1. ALWAYS check size before reading
46
+
47
+ ```
48
+ ❌ WRONG: fs_read("repo/package-lock.json") → 20MB into your context
49
+ ✅ RIGHT: fs_list("repo/package-lock.json") → see it's 20MB, skip it
50
+ fs_read("repo/package.json") → read the 2KB file instead
51
+ ```
52
+
53
+ Large files are truncated at 256KB with a `[truncated]` marker, but even truncated reads waste a tool call and context tokens. Check first.
54
+
55
+ ### 2. Respect mount permissions
56
+
57
+ Mounts have three permission tiers:
58
+
59
+ | Permission | What you can do |
60
+ |-----------|----------------|
61
+ | **Read-only** | `fs_read`, `fs_list`, `fs_search` |
62
+ | **Writable** | Everything above + `fs_write`, `fs_mkdir` |
63
+ | **Deletable** | Everything above + `fs_remove` |
64
+
65
+ If you try a write operation on a read-only mount, you'll get an error. Don't retry — the permission is structural.
66
+
67
+ ### 3. Use fs_search instead of recursive fs_list
68
+
69
+ ```
70
+ ❌ WRONG: fs_list("repo") → fs_list("repo/src") → fs_list("repo/src/components") → ...
71
+ ✅ RIGHT: fs_search("repo", "**/*.tsx")
72
+ ```
73
+
74
+ `fs_search` handles recursion, excludes `.git` and `node_modules` automatically, and returns up to 100 results.
75
+
76
+ ### 4. Binary files return an error, not content
77
+
78
+ If you try to `fs_read` an image, PDF, compiled binary, or other non-text file, you'll get:
79
+ ```
80
+ Error: Binary file (.png, 45.2KB). Use fs_list to see metadata.
81
+ ```
82
+
83
+ This is by design — binary content is not useful in your context window.
84
+
85
+ ## Common mistakes
86
+
87
+ | ❌ Wrong | ✅ Correct |
88
+ |----------|-----------|
89
+ | `fs_read` a large file without checking size | `fs_list` first, then `fs_read` only if reasonable size |
90
+ | Recursive `fs_list` to find files | `fs_search("mount", "**/*.ext")` in one call |
91
+ | `fs_write` to a read-only mount and retry | Check mount permissions — read-only is structural |
92
+ | Using absolute paths (`/Users/...`) | Always use logical paths (`mount-name/...`) |
93
+ | Reading binary files for content | Use `fs_list` for metadata (size, modified date) |
94
+ | Creating deeply nested directories for scratch work | Keep workspace organized — 2-3 levels max |
95
+ | Writing temporary files and not cleaning up | Remove temp files when done if mount is deletable |
96
+
97
+ ## Workflows
98
+
99
+ ### Reading reference material
100
+
101
+ When a SKILL.md tells you to check a reference file:
102
+
103
+ 1. `fs_list("skills/augment-name/references")` — see what's available
104
+ 2. `fs_read("skills/augment-name/references/api-schema.json")` — read the specific file you need
105
+ 3. Use the content to inform your response — don't dump it verbatim
106
+
107
+ ### Writing structured output
108
+
109
+ When producing a report or analysis:
110
+
111
+ 1. `fs_list("workspace")` — see existing structure
112
+ 2. `fs_mkdir("workspace/reports")` — create directory if needed
113
+ 3. `fs_write("workspace/reports/2026-04-10-analysis.md", content)` — write the output
114
+ 4. Confirm to the user: "Written analysis to workspace/reports/..."
115
+
116
+ ### Searching a codebase
117
+
118
+ When the user asks about code in a mounted repository:
119
+
120
+ 1. `fs_search("repo", "**/*.ts")` — find relevant file types
121
+ 2. `fs_list("repo/src/components")` — explore a specific directory
122
+ 3. `fs_read("repo/src/components/Header.tsx")` — read specific files
123
+ 4. Synthesize what you found — don't just list files
124
+
125
+ ## Edge cases
126
+
127
+ - **Empty directories**: `fs_list` returns `{"entries": []}`. This is normal, not an error.
128
+ - **Missing files**: `fs_read` on a non-existent file returns an error. Check with `fs_list` first if unsure.
129
+ - **Symlinks**: Symlinks that point outside the mount boundary are rejected. You'll get a clear error.
130
+ - **Mount root deletion**: You cannot delete a mount root. Only files and empty directories within it.
131
+ - **Cross-mount references**: Each mount is independent. A path in one mount cannot reference files in another.
132
+
133
+ ## What you cannot do
134
+
135
+ - Create new mounts (operator-configured only)
136
+ - Access files outside declared mounts
137
+ - Follow symlinks that escape mount boundaries
138
+ - Read binary files (images, PDFs, compiled code)
139
+ - Write files larger than the mount's size limit (default 1MB)
140
+ - Delete non-empty directories
141
+
142
+ For detailed mount permissions and limits, see [references/mount-permissions.md](references/mount-permissions.md).