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.
- package/CHANGELOG.md +96 -0
- package/LICENSE +201 -0
- package/README.md +161 -0
- package/package.json +76 -0
- package/src/agent-card.ts +39 -0
- package/src/agent.ts +283 -0
- package/src/agentmail-client.ts +138 -0
- package/src/augments/bash/index.ts +463 -0
- package/src/augments/bash/skill/SKILL.md +156 -0
- package/src/augments/budgets/budget-store.ts +513 -0
- package/src/augments/budgets/index.ts +134 -0
- package/src/augments/budgets/preamble.ts +93 -0
- package/src/augments/budgets/types.ts +89 -0
- package/src/augments/file-memory/index.ts +71 -0
- package/src/augments/filesystem/index.ts +533 -0
- package/src/augments/filesystem/skill/SKILL.md +142 -0
- package/src/augments/filesystem/skill/references/mount-permissions.md +81 -0
- package/src/augments/layered-memory/extractor/buffer.ts +56 -0
- package/src/augments/layered-memory/extractor/frequency.ts +79 -0
- package/src/augments/layered-memory/extractor/inject-handler.ts +103 -0
- package/src/augments/layered-memory/extractor/parse.ts +75 -0
- package/src/augments/layered-memory/extractor/prompt.md +26 -0
- package/src/augments/layered-memory/index.ts +757 -0
- package/src/augments/layered-memory/skill/SKILL.md +153 -0
- package/src/augments/layered-memory/storage/migrations/README.md +16 -0
- package/src/augments/layered-memory/storage/migrations/supabase-add-fact-fields.sql +9 -0
- package/src/augments/layered-memory/storage/sqlite-store.ts +352 -0
- package/src/augments/layered-memory/storage/supabase-store.ts +263 -0
- package/src/augments/layered-memory/storage/types.ts +98 -0
- package/src/augments/link/index.ts +489 -0
- package/src/augments/link/translate.ts +261 -0
- package/src/augments/notify/adapters/agentmail.ts +70 -0
- package/src/augments/notify/adapters/telegram.ts +60 -0
- package/src/augments/notify/adapters/webhook.ts +55 -0
- package/src/augments/notify/index.ts +284 -0
- package/src/augments/notify/skill/SKILL.md +150 -0
- package/src/augments/org-context/index.ts +721 -0
- package/src/augments/org-context/skill/SKILL.md +96 -0
- package/src/augments/skills/index.ts +103 -0
- package/src/augments/supabase-memory/index.ts +151 -0
- package/src/augments/telegram-transport/index.ts +312 -0
- package/src/augments/telegram-transport/polling.ts +55 -0
- package/src/augments/telegram-transport/webhook.ts +56 -0
- package/src/augments/turn-control/index.ts +61 -0
- package/src/augments/turn-control/skill/SKILL.md +155 -0
- package/src/augments/visitor-auth/email-validation.ts +66 -0
- package/src/augments/visitor-auth/index.ts +779 -0
- package/src/augments/visitor-auth/rate-limiter.ts +90 -0
- package/src/augments/visitor-auth/skill/SKILL.md +55 -0
- package/src/augments/visitor-auth/storage/sqlite-store.ts +398 -0
- package/src/augments/visitor-auth/storage/types.ts +164 -0
- package/src/augments/visitor-auth/types.ts +123 -0
- package/src/augments/visitor-auth/verify-page.ts +179 -0
- package/src/augments/web-fetch/index.ts +331 -0
- package/src/augments/web-fetch/skill/SKILL.md +100 -0
- package/src/cli/agent-index.ts +289 -0
- package/src/cli/augment-catalog.ts +320 -0
- package/src/cli/augment-resolver.ts +597 -0
- package/src/cli/commands/add-skill.ts +194 -0
- package/src/cli/commands/add.ts +87 -0
- package/src/cli/commands/chat.ts +207 -0
- package/src/cli/commands/create.ts +462 -0
- package/src/cli/commands/dev.ts +139 -0
- package/src/cli/commands/eval.ts +180 -0
- package/src/cli/commands/ls.ts +66 -0
- package/src/cli/commands/remove.ts +95 -0
- package/src/cli/commands/restart.ts +40 -0
- package/src/cli/commands/start.ts +123 -0
- package/src/cli/commands/status.ts +104 -0
- package/src/cli/commands/stop.ts +84 -0
- package/src/cli/commands/visitors-revoke.ts +155 -0
- package/src/cli/commands/visitors.ts +101 -0
- package/src/cli/config-parser.ts +1034 -0
- package/src/cli/engine-resolver.ts +68 -0
- package/src/cli/index.ts +178 -0
- package/src/cli/model-picker.ts +89 -0
- package/src/cli/pid-registry.ts +146 -0
- package/src/cli/plist-generator.ts +117 -0
- package/src/cli/resolve-config.ts +56 -0
- package/src/cli/scaffold-skills.ts +158 -0
- package/src/cli/scaffold.ts +291 -0
- package/src/cli/skill-frontmatter.ts +51 -0
- package/src/cli/skill-validator.ts +151 -0
- package/src/cli/types.ts +228 -0
- package/src/cli/yaml-helpers.ts +66 -0
- package/src/engines/_shared/cost.ts +55 -0
- package/src/engines/_shared/schema-normalize.ts +75 -0
- package/src/engines/anthropic/pricing.ts +117 -0
- package/src/engines/anthropic.ts +483 -0
- package/src/engines/openai/pricing.ts +67 -0
- package/src/engines/openai.ts +446 -0
- package/src/engines/openrouter/pricing.ts +83 -0
- package/src/engines/openrouter.ts +185 -0
- package/src/helpers.ts +24 -0
- package/src/http.ts +387 -0
- package/src/index.ts +165 -0
- package/src/kernel/capability-table.ts +172 -0
- package/src/kernel/context-allocator.ts +161 -0
- package/src/kernel/history-manager.ts +198 -0
- package/src/kernel/lifecycle-manager.ts +106 -0
- package/src/kernel/output-validator.ts +35 -0
- package/src/kernel/preamble.ts +23 -0
- package/src/kernel/route-collector.ts +97 -0
- package/src/kernel/timeout.ts +21 -0
- package/src/kernel/tool-selector.ts +47 -0
- package/src/kernel/trace-emitter.ts +66 -0
- package/src/kernel/transport-queue.ts +147 -0
- package/src/kernel/turn-loop.ts +1148 -0
- package/src/memory/context-synthesis.ts +83 -0
- package/src/memory/memory-bus.ts +61 -0
- package/src/memory/registry.ts +80 -0
- package/src/memory/tools.ts +320 -0
- package/src/memory/types.ts +8 -0
- package/src/parts.ts +30 -0
- package/src/scaffold-templates/identity.md +31 -0
- package/src/telegram-client.ts +145 -0
- package/src/tokenizer.ts +14 -0
- package/src/transports/ag-ui-events.ts +253 -0
- package/src/transports/visitor-token.ts +82 -0
- package/src/transports/web-transport.ts +948 -0
- 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).
|