@retinue/agentkit 0.1.0 → 0.2.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 (77) hide show
  1. package/README.md +59 -277
  2. package/dist/adapters/embeddings/openai.d.ts +45 -0
  3. package/dist/adapters/embeddings/openai.js +109 -0
  4. package/dist/agents/agent.d.ts +22 -1
  5. package/dist/agents/agent.js +97 -11
  6. package/dist/agents/engine.d.ts +28 -0
  7. package/dist/agents/engine.js +194 -8
  8. package/dist/capabilities/index.d.ts +5 -1
  9. package/dist/capabilities/index.js +23 -0
  10. package/dist/capabilities/runtime.d.ts +8 -0
  11. package/dist/core/budget.d.ts +55 -0
  12. package/dist/core/budget.js +56 -0
  13. package/dist/core/content-parts.d.ts +8 -0
  14. package/dist/core/events.d.ts +68 -2
  15. package/dist/core/events.js +2 -0
  16. package/dist/core/index.d.ts +1 -0
  17. package/dist/core/index.js +1 -0
  18. package/dist/documents/index.d.ts +14 -0
  19. package/dist/documents/parsers/text.d.ts +16 -0
  20. package/dist/documents/parsers/text.js +54 -2
  21. package/dist/entries/guardrails.d.ts +14 -0
  22. package/dist/entries/guardrails.js +14 -0
  23. package/dist/entries/knowledge.d.ts +9 -0
  24. package/dist/entries/knowledge.js +8 -0
  25. package/dist/graphql/resolvers.d.ts +4 -0
  26. package/dist/graphql/resolvers.js +6 -0
  27. package/dist/graphql/schema.d.ts +1 -1
  28. package/dist/graphql/schema.js +44 -0
  29. package/dist/guardrails/index.d.ts +115 -0
  30. package/dist/guardrails/index.js +108 -0
  31. package/dist/guardrails/moderation.d.ts +53 -0
  32. package/dist/guardrails/moderation.js +75 -0
  33. package/dist/guardrails/pii.d.ts +75 -0
  34. package/dist/guardrails/pii.js +193 -0
  35. package/dist/knowledge/index.d.ts +1 -0
  36. package/dist/knowledge/index.js +1 -0
  37. package/dist/knowledge/navigate.d.ts +89 -0
  38. package/dist/knowledge/navigate.js +107 -0
  39. package/dist/knowledge/retrieval.d.ts +73 -5
  40. package/dist/knowledge/retrieval.js +82 -28
  41. package/dist/models/streaming.d.ts +22 -1
  42. package/dist/models/streaming.js +5 -1
  43. package/dist/security/checklist.js +9 -0
  44. package/dist/security/findings.js +18 -9
  45. package/dist/skills/catalogue.d.ts +49 -0
  46. package/dist/skills/catalogue.js +61 -0
  47. package/dist/skills/index.d.ts +1 -0
  48. package/dist/skills/index.js +1 -0
  49. package/dist/telemetry/spans.js +12 -0
  50. package/dist/toolkit/files.d.ts +125 -0
  51. package/dist/toolkit/files.js +320 -0
  52. package/dist/toolkit/index.d.ts +4 -0
  53. package/dist/toolkit/index.js +2 -0
  54. package/dist/toolkit/sandbox.d.ts +119 -0
  55. package/dist/toolkit/sandbox.js +239 -0
  56. package/dist/toolkit/web.d.ts +13 -0
  57. package/dist/toolkit/web.js +7 -1
  58. package/dist/tools/budget.d.ts +28 -0
  59. package/dist/tools/budget.js +35 -0
  60. package/dist/tools/credentials.d.ts +57 -0
  61. package/dist/tools/credentials.js +54 -0
  62. package/dist/tools/define.d.ts +31 -0
  63. package/dist/tools/define.js +23 -0
  64. package/dist/tools/find.d.ts +109 -0
  65. package/dist/tools/find.js +210 -0
  66. package/dist/tools/index.d.ts +14 -2
  67. package/dist/tools/index.js +4 -0
  68. package/dist/tools/library/fs.d.ts +24 -0
  69. package/dist/tools/library/fs.js +102 -0
  70. package/dist/tools/library/index.d.ts +29 -2
  71. package/dist/tools/library/index.js +40 -0
  72. package/dist/tools/library/shell.d.ts +45 -0
  73. package/dist/tools/library/shell.js +70 -0
  74. package/dist/tools/meta-tools.js +8 -0
  75. package/dist/tools/registry.d.ts +113 -0
  76. package/dist/tools/registry.js +180 -4
  77. package/package.json +5 -1
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Reading and writing files, path-scoped — REQ-047 (#206), task #215.
3
+ *
4
+ * In `toolkit/` rather than `tools/` because it performs I/O and boundary rule **R7** forbids that in the tools
5
+ * layer, the same arrangement `http.ts` has with the web tools. The tools in `tools/library/fs.ts` are envelopes
6
+ * over these functions, and every security property lives here — not in the envelope, and not in the schema.
7
+ *
8
+ * ## The one property that matters
9
+ *
10
+ * **A path a model produced must not be able to name a file outside the configured root**, and there are three
11
+ * ways it tries:
12
+ *
13
+ * - `../../etc/passwd` — normalised away by resolving against the root first.
14
+ * - `/etc/passwd` — an absolute path is **refused outright** rather than silently re-rooted. Re-rooting would
15
+ * answer a different question than the one asked, and the model would not know.
16
+ * - A **symlink** inside the root pointing out of it. This is the one that gets missed, because the path is
17
+ * inside the root right up until the filesystem resolves it. So the check is against the *real* path, after
18
+ * symlink resolution, on every call — not against the string.
19
+ *
20
+ * The root itself is resolved once at construction, also through `realpath`: a root that is itself a symlink
21
+ * would otherwise make every real path look like an escape.
22
+ *
23
+ * ## Bytes are bounded while reading
24
+ *
25
+ * A cap applied after `readFile` has already buffered a two-gigabyte file protects nothing. These read into a
26
+ * fixed buffer and report `truncated`, which is the same decision `http.ts` made for the same reason. Truncation
27
+ * rather than refusal, because "the first 200 KB of the log" is usually the answer, and a refusal leaves the
28
+ * model with nothing.
29
+ */
30
+ /** Bytes returned from one read. Matches the HTTP client's ceiling, for the same reason. */
31
+ export declare const MAX_FILE_BYTES = 200000;
32
+ /** Entries returned from one listing. A directory of ten thousand files is not an answer. */
33
+ export declare const MAX_ENTRIES = 200;
34
+ /** Files examined by one search. Bounded work, so a search cannot become a filesystem crawl. */
35
+ export declare const MAX_SEARCHED_FILES = 2000;
36
+ /** Matches returned from one search. */
37
+ export declare const MAX_MATCHES = 100;
38
+ export type FileScope = {
39
+ /** Everything readable, and the only thing readable. Resolved through `realpath` at construction. */
40
+ readonly root: string;
41
+ /**
42
+ * Where writes may land, when writes are wanted at all.
43
+ *
44
+ * Separate from `root`, and absent by default. A deployment that pointed both at the same directory would let
45
+ * a model edit the material it also reads — which is how a corpus a model cites becomes a corpus a model wrote.
46
+ */
47
+ readonly writableRoot?: string;
48
+ readonly maxBytes?: number;
49
+ readonly maxEntries?: number;
50
+ readonly maxMatches?: number;
51
+ readonly maxSearchedFiles?: number;
52
+ };
53
+ /**
54
+ * Why a file operation did not happen.
55
+ *
56
+ * A *reason*, not an exception, for the reason `HttpFailure` gives: a refused path is information the model can
57
+ * act on, and a thrown error reads as "something broke", which invites an identical retry.
58
+ */
59
+ export type FileFailure = {
60
+ readonly ok: false;
61
+ readonly path: string;
62
+ readonly kind: "forbidden" | "not-found" | "not-a-file" | "not-a-directory" | "unreadable" | "too-many";
63
+ readonly reason: string;
64
+ };
65
+ export type FileRead = {
66
+ readonly ok: true;
67
+ readonly path: string;
68
+ readonly bytes: number;
69
+ readonly truncated: boolean;
70
+ readonly content: string;
71
+ };
72
+ export type FileEntry = {
73
+ readonly name: string;
74
+ readonly path: string;
75
+ readonly kind: "file" | "directory" | "other";
76
+ readonly bytes?: number;
77
+ };
78
+ export type FileList = {
79
+ readonly ok: true;
80
+ readonly path: string;
81
+ readonly entries: readonly FileEntry[];
82
+ readonly truncated: boolean;
83
+ };
84
+ export type FileMatch = {
85
+ readonly path: string;
86
+ readonly line: number;
87
+ /** The matching line, trimmed and capped. Untrusted content, like everything else read from disk. */
88
+ readonly text: string;
89
+ };
90
+ export type FileSearch = {
91
+ readonly ok: true;
92
+ readonly query: string;
93
+ readonly matches: readonly FileMatch[];
94
+ readonly filesSearched: number;
95
+ /** True when the file or match ceiling stopped the search early. */
96
+ readonly truncated: boolean;
97
+ };
98
+ export type FileWrite = {
99
+ readonly ok: true;
100
+ readonly path: string;
101
+ readonly bytes: number;
102
+ readonly created: boolean;
103
+ };
104
+ /**
105
+ * Is `candidate` inside `root`, both already real paths?
106
+ *
107
+ * `relative` rather than `startsWith`: `/srv/data-secrets` starts with `/srv/data`, and a prefix comparison would
108
+ * accept it. A relative path that begins with `..` or is absolute is outside.
109
+ */
110
+ export declare const contains: (root: string, candidate: string) => boolean;
111
+ export type FileReader = {
112
+ read(path: string): FileRead | FileFailure;
113
+ list(path?: string): FileList | FileFailure;
114
+ search(input: {
115
+ readonly query: string;
116
+ readonly path?: string;
117
+ readonly namePattern?: string;
118
+ }): FileSearch | FileFailure;
119
+ write(input: {
120
+ readonly path: string;
121
+ readonly content: string;
122
+ }): FileWrite | FileFailure;
123
+ };
124
+ export declare const createFileReader: (scope: FileScope) => FileReader;
125
+ //# sourceMappingURL=files.d.ts.map
@@ -0,0 +1,320 @@
1
+ /**
2
+ * Reading and writing files, path-scoped — REQ-047 (#206), task #215.
3
+ *
4
+ * In `toolkit/` rather than `tools/` because it performs I/O and boundary rule **R7** forbids that in the tools
5
+ * layer, the same arrangement `http.ts` has with the web tools. The tools in `tools/library/fs.ts` are envelopes
6
+ * over these functions, and every security property lives here — not in the envelope, and not in the schema.
7
+ *
8
+ * ## The one property that matters
9
+ *
10
+ * **A path a model produced must not be able to name a file outside the configured root**, and there are three
11
+ * ways it tries:
12
+ *
13
+ * - `../../etc/passwd` — normalised away by resolving against the root first.
14
+ * - `/etc/passwd` — an absolute path is **refused outright** rather than silently re-rooted. Re-rooting would
15
+ * answer a different question than the one asked, and the model would not know.
16
+ * - A **symlink** inside the root pointing out of it. This is the one that gets missed, because the path is
17
+ * inside the root right up until the filesystem resolves it. So the check is against the *real* path, after
18
+ * symlink resolution, on every call — not against the string.
19
+ *
20
+ * The root itself is resolved once at construction, also through `realpath`: a root that is itself a symlink
21
+ * would otherwise make every real path look like an escape.
22
+ *
23
+ * ## Bytes are bounded while reading
24
+ *
25
+ * A cap applied after `readFile` has already buffered a two-gigabyte file protects nothing. These read into a
26
+ * fixed buffer and report `truncated`, which is the same decision `http.ts` made for the same reason. Truncation
27
+ * rather than refusal, because "the first 200 KB of the log" is usually the answer, and a refusal leaves the
28
+ * model with nothing.
29
+ */
30
+ import { closeSync, existsSync, openSync, readSync, readdirSync, realpathSync, statSync, writeFileSync, mkdirSync } from "node:fs";
31
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
32
+ /** Bytes returned from one read. Matches the HTTP client's ceiling, for the same reason. */
33
+ export const MAX_FILE_BYTES = 200_000;
34
+ /** Entries returned from one listing. A directory of ten thousand files is not an answer. */
35
+ export const MAX_ENTRIES = 200;
36
+ /** Files examined by one search. Bounded work, so a search cannot become a filesystem crawl. */
37
+ export const MAX_SEARCHED_FILES = 2_000;
38
+ /** Matches returned from one search. */
39
+ export const MAX_MATCHES = 100;
40
+ const forbidden = (path, reason) => ({ ok: false, path, kind: "forbidden", reason });
41
+ /**
42
+ * Is `candidate` inside `root`, both already real paths?
43
+ *
44
+ * `relative` rather than `startsWith`: `/srv/data-secrets` starts with `/srv/data`, and a prefix comparison would
45
+ * accept it. A relative path that begins with `..` or is absolute is outside.
46
+ */
47
+ export const contains = (root, candidate) => {
48
+ if (candidate === root)
49
+ return true;
50
+ const rel = relative(root, candidate);
51
+ return rel !== "" && !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel);
52
+ };
53
+ export const createFileReader = (scope) => {
54
+ const maxBytes = scope.maxBytes ?? MAX_FILE_BYTES;
55
+ const maxEntries = scope.maxEntries ?? MAX_ENTRIES;
56
+ const maxMatches = scope.maxMatches ?? MAX_MATCHES;
57
+ const maxSearched = scope.maxSearchedFiles ?? MAX_SEARCHED_FILES;
58
+ /**
59
+ * The root, resolved once through `realpath`.
60
+ *
61
+ * A root that is itself a symlink — `/tmp` on macOS is `/private/tmp` — would otherwise make every resolved
62
+ * path look like an escape, and the tool would refuse everything while appearing to be configured correctly.
63
+ */
64
+ const realRoot = realpathSync(scope.root);
65
+ const realWritable = scope.writableRoot === undefined ? undefined : realpathSync(scope.writableRoot);
66
+ /** Resolve a model-supplied path inside a root, or say why not. */
67
+ const within = (root, requested, mustExist) => {
68
+ if (isAbsolute(requested))
69
+ return forbidden(requested, "Give a path relative to the configured root. An absolute path is refused rather than re-rooted, so a " +
70
+ "refusal is never mistaken for a different file.");
71
+ const joined = resolve(root, requested);
72
+ /**
73
+ * Lexical containment first, before the filesystem is consulted at all.
74
+ *
75
+ * `../../etc/passwd` used to come back as `not-found`, because `realpath` threw on a path that does not exist
76
+ * and the containment check never ran. Technically safe and a poor answer: the model learns that a path it
77
+ * may not read merely happens to be absent, which is a different fact and an invitation to try another. It
78
+ * also means the refusal depended on the target's existence, which is not a property anybody wants a security
79
+ * boundary to have.
80
+ */
81
+ if (!contains(root, joined))
82
+ return forbidden(requested, "That path is outside the configured root.");
83
+ if (mustExist) {
84
+ let real;
85
+ try {
86
+ real = realpathSync(joined);
87
+ }
88
+ catch {
89
+ return { ok: false, path: requested, kind: "not-found", reason: `No such path: ${requested}` };
90
+ }
91
+ if (!contains(root, real))
92
+ return forbidden(requested, "That path resolves outside the configured root. Symlinks are followed and then checked.");
93
+ return { ok: true, path: real };
94
+ }
95
+ /**
96
+ * For a write, the target may not exist yet — nor may its directory.
97
+ *
98
+ * So walk up to the nearest ancestor that *does* exist, resolve **that** through `realpath`, and rebuild the
99
+ * target beneath it. The first version resolved `dirname` only, which refused every write into a directory it
100
+ * was about to create — a correct-looking check that made the tool useless. Resolving the existing prefix is
101
+ * what keeps the symlink guarantee: a link anywhere along the real part of the path is followed and checked.
102
+ */
103
+ const missing = [];
104
+ let probe = joined;
105
+ while (!existsSync(probe)) {
106
+ const parent = dirname(probe);
107
+ if (parent === probe)
108
+ break;
109
+ missing.unshift(basename(probe));
110
+ probe = parent;
111
+ }
112
+ let realPrefix;
113
+ try {
114
+ realPrefix = realpathSync(probe);
115
+ }
116
+ catch {
117
+ return { ok: false, path: requested, kind: "not-found", reason: `No such path: ${requested}` };
118
+ }
119
+ const candidate = missing.length === 0 ? realPrefix : join(realPrefix, ...missing);
120
+ if (!contains(root, candidate))
121
+ return forbidden(requested, "That path resolves outside the configured root. Symlinks are followed and then checked.");
122
+ return { ok: true, path: candidate };
123
+ };
124
+ const readBounded = (path) => {
125
+ let handle;
126
+ try {
127
+ handle = openSync(path, "r");
128
+ }
129
+ catch (error) {
130
+ return { ok: false, path, kind: "unreadable", reason: error.message };
131
+ }
132
+ try {
133
+ const buffer = Buffer.alloc(maxBytes + 1);
134
+ const read = readSync(handle, buffer, 0, maxBytes + 1, 0);
135
+ const truncated = read > maxBytes;
136
+ return {
137
+ ok: true,
138
+ path,
139
+ bytes: truncated ? maxBytes : read,
140
+ truncated,
141
+ content: buffer.subarray(0, truncated ? maxBytes : read).toString("utf8"),
142
+ };
143
+ }
144
+ catch (error) {
145
+ return { ok: false, path, kind: "unreadable", reason: error.message };
146
+ }
147
+ finally {
148
+ closeSync(handle);
149
+ }
150
+ };
151
+ /** A deliberately small pattern language: `*.md`, `report*`, `*draft*`. Not a glob engine. */
152
+ const matchesName = (name, pattern) => {
153
+ if (pattern === undefined || pattern === "")
154
+ return true;
155
+ const parts = pattern.toLowerCase().split("*");
156
+ const lower = name.toLowerCase();
157
+ if (parts.length === 1)
158
+ return lower === (parts[0] ?? "");
159
+ let index = 0;
160
+ for (const [position, part] of parts.entries()) {
161
+ if (part === "")
162
+ continue;
163
+ const at = lower.indexOf(part, index);
164
+ if (at === -1)
165
+ return false;
166
+ if (position === 0 && at !== 0)
167
+ return false;
168
+ index = at + part.length;
169
+ }
170
+ const last = parts[parts.length - 1] ?? "";
171
+ return last === "" || lower.endsWith(last);
172
+ };
173
+ return {
174
+ read(requested) {
175
+ const scoped = within(realRoot, requested, true);
176
+ if (!scoped.ok)
177
+ return scoped;
178
+ let stats;
179
+ try {
180
+ stats = statSync(scoped.path);
181
+ }
182
+ catch (error) {
183
+ return { ok: false, path: requested, kind: "not-found", reason: error.message };
184
+ }
185
+ if (stats.isDirectory())
186
+ return { ok: false, path: requested, kind: "not-a-file", reason: `${requested} is a directory — use fs_list.` };
187
+ const outcome = readBounded(scoped.path);
188
+ return outcome.ok ? { ...outcome, path: relative(realRoot, scoped.path) || "." } : { ...outcome, path: requested };
189
+ },
190
+ list(requested = ".") {
191
+ const scoped = within(realRoot, requested, true);
192
+ if (!scoped.ok)
193
+ return scoped;
194
+ let names;
195
+ try {
196
+ names = readdirSync(scoped.path).sort();
197
+ }
198
+ catch (error) {
199
+ const message = error.code === "ENOTDIR" ? `${requested} is a file — use fs_read.` : error.message;
200
+ return {
201
+ ok: false,
202
+ path: requested,
203
+ kind: error.code === "ENOTDIR" ? "not-a-directory" : "unreadable",
204
+ reason: message,
205
+ };
206
+ }
207
+ const entries = [];
208
+ for (const name of names.slice(0, maxEntries)) {
209
+ const full = join(scoped.path, name);
210
+ let stats;
211
+ try {
212
+ stats = statSync(full);
213
+ }
214
+ catch {
215
+ // A broken symlink or a file removed mid-listing. Reported as `other` rather than omitted: a name that
216
+ // exists and cannot be described is more useful than a silently shorter list.
217
+ entries.push({ name, path: relative(realRoot, full), kind: "other" });
218
+ continue;
219
+ }
220
+ entries.push({
221
+ name,
222
+ path: relative(realRoot, full),
223
+ kind: stats.isDirectory() ? "directory" : stats.isFile() ? "file" : "other",
224
+ ...(stats.isFile() ? { bytes: stats.size } : {}),
225
+ });
226
+ }
227
+ return { ok: true, path: relative(realRoot, scoped.path) || ".", entries, truncated: names.length > maxEntries };
228
+ },
229
+ search({ query, path = ".", namePattern }) {
230
+ if (query.trim() === "")
231
+ return { ok: false, path, kind: "unreadable", reason: "Give something to search for." };
232
+ const scoped = within(realRoot, path, true);
233
+ if (!scoped.ok)
234
+ return scoped;
235
+ const needle = query.toLowerCase();
236
+ const matches = [];
237
+ let searched = 0;
238
+ let truncated = false;
239
+ const walk = (dir) => {
240
+ if (truncated)
241
+ return;
242
+ let names;
243
+ try {
244
+ names = readdirSync(dir).sort();
245
+ }
246
+ catch {
247
+ return;
248
+ }
249
+ for (const name of names) {
250
+ if (truncated)
251
+ return;
252
+ const full = join(dir, name);
253
+ let stats;
254
+ try {
255
+ stats = statSync(full);
256
+ }
257
+ catch {
258
+ continue;
259
+ }
260
+ // The real path is checked here too: a symlinked directory inside the root would otherwise let a
261
+ // search walk out of it, which is the same escape as a symlinked file and easier to miss.
262
+ if (!contains(realRoot, realpathSync(full)))
263
+ continue;
264
+ if (stats.isDirectory()) {
265
+ walk(full);
266
+ continue;
267
+ }
268
+ if (!stats.isFile() || !matchesName(name, namePattern))
269
+ continue;
270
+ searched += 1;
271
+ if (searched > maxSearched) {
272
+ truncated = true;
273
+ return;
274
+ }
275
+ const read = readBounded(full);
276
+ if (!read.ok)
277
+ continue;
278
+ read.content.split("\n").forEach((line, index) => {
279
+ if (truncated || !line.toLowerCase().includes(needle))
280
+ return;
281
+ if (matches.length >= maxMatches) {
282
+ truncated = true;
283
+ return;
284
+ }
285
+ matches.push({ path: relative(realRoot, full), line: index + 1, text: line.trim().slice(0, 400) });
286
+ });
287
+ }
288
+ };
289
+ walk(scoped.path);
290
+ return { ok: true, query, matches, filesSearched: searched, truncated };
291
+ },
292
+ write({ path, content }) {
293
+ if (realWritable === undefined)
294
+ return forbidden(path, "No writable root is configured, so nothing can be written. This is a wiring decision, not a " +
295
+ "permission one — see FileScope.writableRoot.");
296
+ const scoped = within(realWritable, path, false);
297
+ if (!scoped.ok)
298
+ return scoped;
299
+ const bytes = Buffer.byteLength(content, "utf8");
300
+ if (bytes > maxBytes)
301
+ return { ok: false, path, kind: "too-many", reason: `That is ${bytes} bytes; the ceiling is ${maxBytes}.` };
302
+ let created = true;
303
+ try {
304
+ created = !statSync(scoped.path).isFile();
305
+ }
306
+ catch {
307
+ created = true;
308
+ }
309
+ try {
310
+ mkdirSync(dirname(scoped.path), { recursive: true });
311
+ writeFileSync(scoped.path, content, "utf8");
312
+ }
313
+ catch (error) {
314
+ return { ok: false, path, kind: "unreadable", reason: error.message };
315
+ }
316
+ return { ok: true, path: relative(realWritable, scoped.path), bytes, created };
317
+ },
318
+ };
319
+ };
320
+ //# sourceMappingURL=files.js.map
@@ -18,4 +18,8 @@ export { MAX_CELL_CHARS, MAX_CSV_ROWS, MAX_SQL_ROWS, createSqlQuery, createSqlSc
18
18
  export type { CsvResult, JsonQueryResult, ReadOnlyQuery, SchemaResult, SqlResult } from "./data.js";
19
19
  export { MAX_EXPRESSION_CHARS, calculate, currentTime } from "./compute.js";
20
20
  export type { CalculationResult, TimeResult } from "./compute.js";
21
+ export { MAX_ENTRIES, MAX_FILE_BYTES, MAX_MATCHES, MAX_SEARCHED_FILES, contains, createFileReader, } from "./files.js";
22
+ export type { FileEntry, FileFailure, FileList, FileMatch, FileRead, FileReader, FileScope, FileSearch, FileWrite } from "./files.js";
23
+ export { DEFAULT_MEMORY_MB, DEFAULT_TIMEOUT_MS, MAX_OUTPUT_BYTES, createDockerSandbox, createLocalSandbox, dockerArgs, } from "./sandbox.js";
24
+ export type { DockerSandboxConfig, LocalSandboxConfig, Sandbox, SandboxRequest, SandboxResult } from "./sandbox.js";
21
25
  //# sourceMappingURL=index.d.ts.map
@@ -14,4 +14,6 @@ export { DEFAULT_EGRESS_POLICY, MAX_RESPONSE_BYTES, REQUEST_TIMEOUT_MS, createHt
14
14
  export { DEFAULT_SEARCH_LIMIT, MAX_SNIPPET_CHARS, createFetchJson, createFetchPage, createWebSearch, htmlToText, } from "./web.js";
15
15
  export { MAX_CELL_CHARS, MAX_CSV_ROWS, MAX_SQL_ROWS, createSqlQuery, createSqlSchema, parseCsv, queryJson, } from "./data.js";
16
16
  export { MAX_EXPRESSION_CHARS, calculate, currentTime } from "./compute.js";
17
+ export { MAX_ENTRIES, MAX_FILE_BYTES, MAX_MATCHES, MAX_SEARCHED_FILES, contains, createFileReader, } from "./files.js";
18
+ export { DEFAULT_MEMORY_MB, DEFAULT_TIMEOUT_MS, MAX_OUTPUT_BYTES, createDockerSandbox, createLocalSandbox, dockerArgs, } from "./sandbox.js";
17
19
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Running a command somewhere it cannot hurt you — REQ-047 (#206), task #215.
3
+ *
4
+ * `shell_exec` is the only tool in this package whose blast radius is not described by its schema. Every other
5
+ * tool can do one thing to one kind of object; this one can do anything the process can do. And its trigger is
6
+ * natural language — including language the model merely *read*, in a document, in an issue body, in a Slack
7
+ * message. Without isolation, a shell tool is a remote code execution endpoint reachable by anyone who can get
8
+ * text in front of the agent.
9
+ *
10
+ * So the sandbox is not a hardening step applied afterwards. It is the thing that makes the tool defensible, and
11
+ * the tool does not exist without one wired.
12
+ *
13
+ * ## What the contract guarantees
14
+ *
15
+ * | Guarantee | Why it is not optional |
16
+ * |---|---|
17
+ * | No network | A command that can reach the network can exfiltrate anything it can read, and the egress policy does not apply inside a container |
18
+ * | Read-only root, one writable scratch mount | A command that can write to the image can install a persistent foothold |
19
+ * | Memory cap | An unbounded allocation takes the host down with it, and that is a denial of service anybody can trigger by asking |
20
+ * | Wall-clock timeout | `sleep 999` must end as a *timeout*, not as an empty success |
21
+ * | No TTY | An interactive prompt would hang forever waiting for a person who is not there |
22
+ * | Output cap, with the truncation reported | Silent truncation makes a model believe it saw the whole answer |
23
+ * | The exit code in the envelope | Inferring success from output text is guessing; a non-zero exit is a fact |
24
+ *
25
+ * ## Gating is by effect, never by reading the command
26
+ *
27
+ * `shell_exec` is classified `destructive` and routed through the approval gate. It is tempting to inspect the
28
+ * command instead — refuse `rm -rf`, allow `ls` — and that is a losing game: `find . -delete`, `>file`, `dd`,
29
+ * `python -c`, a base64 pipeline. Any list of dangerous shapes is a list somebody gets around, and worse, it
30
+ * *feels* like protection. A classification cannot be evaded by rephrasing.
31
+ */
32
+ /** Bytes of stdout and of stderr returned. Beyond this the output is truncated and says so. */
33
+ export declare const MAX_OUTPUT_BYTES = 64000;
34
+ /** Wall clock. A model waiting on a hung command is a run holding a worker slot. */
35
+ export declare const DEFAULT_TIMEOUT_MS = 20000;
36
+ export declare const DEFAULT_MEMORY_MB = 256;
37
+ export type SandboxRequest = {
38
+ /** The command, run by a shell inside the sandbox. Never interpreted or inspected here. */
39
+ readonly command: string;
40
+ readonly timeoutMs?: number;
41
+ readonly memoryMb?: number;
42
+ /** Files to place in the scratch mount before running, by relative path. */
43
+ readonly files?: Readonly<Record<string, string>>;
44
+ };
45
+ export type SandboxResult = {
46
+ /** Whether the command *ran to completion*. A non-zero exit is `ok: true` with a non-zero code. */
47
+ readonly ok: boolean;
48
+ /**
49
+ * The process's exit code, or null when it never produced one.
50
+ *
51
+ * Null is the honest answer for a killed process, and it is why `reason` exists: "exit code 137" and "we killed
52
+ * it after 20 seconds" are the same event described at two levels, and the model needs the second one.
53
+ */
54
+ readonly exitCode: number | null;
55
+ readonly stdout: string;
56
+ readonly stderr: string;
57
+ readonly truncated: boolean;
58
+ /** Set when the sandbox ended the command itself. */
59
+ readonly reason?: "timeout" | "memory" | "spawn-failed";
60
+ readonly durationMs: number;
61
+ };
62
+ /**
63
+ * A place to run a command. A port, because where that place is — a container here, a microVM, E2B, Daytona — is
64
+ * a deployment's decision and not this package's.
65
+ */
66
+ export interface Sandbox {
67
+ readonly id: string;
68
+ run(request: SandboxRequest): Promise<SandboxResult>;
69
+ }
70
+ export type DockerSandboxConfig = {
71
+ /** The image. A deployment's choice, and it should be one with a shell and nothing else. */
72
+ readonly image: string;
73
+ readonly docker?: string;
74
+ readonly timeoutMs?: number;
75
+ readonly memoryMb?: number;
76
+ /** Extra flags, appended after the enforced ones so they cannot remove them. */
77
+ readonly extraArgs?: readonly string[];
78
+ readonly shell?: string;
79
+ };
80
+ /**
81
+ * The argv, built separately so it can be asserted on.
82
+ *
83
+ * Every security property of this adapter *is* this array. A test that ran a command and checked its output would
84
+ * pass just as well with `--network=none` missing, so the argv is what the tests read — and the flags come before
85
+ * `extraArgs`, so a deployment adding options cannot quietly drop one.
86
+ */
87
+ export declare const dockerArgs: (config: DockerSandboxConfig, request: SandboxRequest) => readonly string[];
88
+ /**
89
+ * The real adapter: one container per command, destroyed after.
90
+ *
91
+ * Not a pool. A reused container is a container a previous command could have left something in, and the whole
92
+ * proposition here is that a command cannot affect anything outside itself.
93
+ */
94
+ export declare const createDockerSandbox: (config: DockerSandboxConfig) => Sandbox;
95
+ export type LocalSandboxConfig = {
96
+ /**
97
+ * Required, and named to be uncomfortable to type.
98
+ *
99
+ * There is no safe default here. A shell on the runtime's own host has no isolation at all: the command runs as
100
+ * the runtime user, with its filesystem, its network and its credentials. That is a remote code execution
101
+ * endpoint reachable through content the model merely read.
102
+ */
103
+ readonly allowUnsafeLocalExecution: true;
104
+ readonly timeoutMs?: number;
105
+ readonly shell?: string;
106
+ };
107
+ /**
108
+ * The development adapter, which **refuses to exist** unless a deployment says so in words.
109
+ *
110
+ * The refusal is at construction rather than at the call, so a misconfiguration is a boot failure rather than a
111
+ * surprise the first time somebody asks the agent to run something. And the message says what to do instead,
112
+ * because "not allowed" without a next step is how a flag gets set to make an error go away.
113
+ *
114
+ * It provides the timeout and the output cap. It provides **none** of the isolation: no network isolation, no
115
+ * memory cap, no read-only filesystem, no dropped capabilities. The contract's table describes what a sandbox
116
+ * guarantees; this adapter meets one row of it, and saying so is the point.
117
+ */
118
+ export declare const createLocalSandbox: (config: LocalSandboxConfig) => Sandbox;
119
+ //# sourceMappingURL=sandbox.d.ts.map