@tpmjs/official-sandbox-shell 0.1.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.
@@ -0,0 +1,72 @@
1
+ import * as ai from 'ai';
2
+
3
+ /**
4
+ * Sandbox Shell Tools for TPMJS Agent Sandbox
5
+ *
6
+ * Execute shell commands, read/write files, and list directories
7
+ * within the stateful sandbox workspace. All paths are resolved
8
+ * relative to `_sandboxWorkDir` (injected by the sandbox server).
9
+ *
10
+ * Runs inside Deno — uses Deno.Command, Deno.readTextFile, etc.
11
+ */
12
+ interface ShellExecResult {
13
+ stdout: string;
14
+ stderr: string;
15
+ exitCode: number;
16
+ durationMs: number;
17
+ truncated: boolean;
18
+ }
19
+ interface ReadFileResult {
20
+ content: string;
21
+ path: string;
22
+ size: number;
23
+ truncated: boolean;
24
+ }
25
+ interface WriteFileResult {
26
+ success: boolean;
27
+ path: string;
28
+ bytesWritten: number;
29
+ }
30
+ interface FileEntry {
31
+ name: string;
32
+ type: 'file' | 'directory' | 'symlink';
33
+ size: number | null;
34
+ }
35
+ interface ListFilesResult {
36
+ path: string;
37
+ entries: FileEntry[];
38
+ truncated: boolean;
39
+ }
40
+ type ShellExecInput = {
41
+ command: string;
42
+ timeout?: number;
43
+ _sandboxWorkDir?: string;
44
+ };
45
+ declare const shellExec: ai.Tool<ShellExecInput, ShellExecResult>;
46
+ type ReadFileInput = {
47
+ path: string;
48
+ _sandboxWorkDir?: string;
49
+ };
50
+ declare const readFile: ai.Tool<ReadFileInput, ReadFileResult>;
51
+ type WriteFileInput = {
52
+ path: string;
53
+ content: string;
54
+ createDirs?: boolean;
55
+ _sandboxWorkDir?: string;
56
+ };
57
+ declare const writeFile: ai.Tool<WriteFileInput, WriteFileResult>;
58
+ type ListFilesInput = {
59
+ path?: string;
60
+ recursive?: boolean;
61
+ maxDepth?: number;
62
+ _sandboxWorkDir?: string;
63
+ };
64
+ declare const listFiles: ai.Tool<ListFilesInput, ListFilesResult>;
65
+ declare const _default: {
66
+ shellExec: ai.Tool<ShellExecInput, ShellExecResult>;
67
+ readFile: ai.Tool<ReadFileInput, ReadFileResult>;
68
+ writeFile: ai.Tool<WriteFileInput, WriteFileResult>;
69
+ listFiles: ai.Tool<ListFilesInput, ListFilesResult>;
70
+ };
71
+
72
+ export { type FileEntry, type ListFilesResult, type ReadFileResult, type ShellExecResult, type WriteFileResult, _default as default, listFiles, readFile, shellExec, writeFile };
package/dist/index.js ADDED
@@ -0,0 +1,329 @@
1
+ // src/index.ts
2
+ import { jsonSchema, tool } from "ai";
3
+ var MAX_STDOUT = 100 * 1024;
4
+ var MAX_READ_SIZE = 500 * 1024;
5
+ var DEFAULT_TIMEOUT_MS = 3e4;
6
+ var MAX_TIMEOUT_MS = 3e5;
7
+ var MAX_LIST_ENTRIES = 1e3;
8
+ var DEFAULT_MAX_DEPTH = 3;
9
+ function resolveSandboxPath(workDir, relativePath) {
10
+ const cleaned = relativePath.replace(/^\/+/, "");
11
+ const resolved = `${workDir}/${cleaned}`.replace(/\/+/g, "/");
12
+ const parts = resolved.split("/");
13
+ const normalized = [];
14
+ for (const part of parts) {
15
+ if (part === "..") {
16
+ normalized.pop();
17
+ } else if (part !== "." && part !== "") {
18
+ normalized.push(part);
19
+ }
20
+ }
21
+ const final = "/" + normalized.join("/");
22
+ const normalizedWorkDir = "/" + workDir.replace(/^\/+/, "").replace(/\/+$/, "");
23
+ if (!final.startsWith(normalizedWorkDir)) {
24
+ throw new Error(`Path escapes sandbox workspace: ${relativePath}`);
25
+ }
26
+ return final;
27
+ }
28
+ function truncate(text, maxBytes) {
29
+ const encoder = new TextEncoder();
30
+ const bytes = encoder.encode(text);
31
+ if (bytes.length <= maxBytes) {
32
+ return { text, truncated: false };
33
+ }
34
+ const decoder = new TextDecoder("utf-8", { fatal: false });
35
+ return {
36
+ text: decoder.decode(bytes.slice(0, maxBytes)) + "\n... [truncated]",
37
+ truncated: true
38
+ };
39
+ }
40
+ async function collectStream(stream, maxBytes = MAX_STDOUT * 2) {
41
+ const reader = stream.getReader();
42
+ const chunks = [];
43
+ let totalLength = 0;
44
+ try {
45
+ while (true) {
46
+ const { done, value } = await reader.read();
47
+ if (done) break;
48
+ chunks.push(value);
49
+ totalLength += value.length;
50
+ if (totalLength > maxBytes) {
51
+ reader.cancel().catch(() => {
52
+ });
53
+ break;
54
+ }
55
+ }
56
+ } finally {
57
+ reader.releaseLock();
58
+ }
59
+ const cappedLength = Math.min(totalLength, maxBytes);
60
+ const result = new Uint8Array(cappedLength);
61
+ let offset = 0;
62
+ for (const chunk of chunks) {
63
+ const copyLen = Math.min(chunk.length, cappedLength - offset);
64
+ if (copyLen <= 0) break;
65
+ result.set(chunk.subarray(0, copyLen), offset);
66
+ offset += copyLen;
67
+ }
68
+ return result.subarray(0, offset);
69
+ }
70
+ var shellExec = tool({
71
+ description: "Execute a shell command in the sandbox workspace. Supports git, npm, and any CLI tools available in the container.",
72
+ inputSchema: jsonSchema({
73
+ type: "object",
74
+ properties: {
75
+ command: {
76
+ type: "string",
77
+ description: "Shell command to execute (passed to sh -c)"
78
+ },
79
+ timeout: {
80
+ type: "number",
81
+ description: "Timeout in milliseconds (default: 30000, max: 300000)"
82
+ },
83
+ _sandboxWorkDir: {
84
+ type: "string",
85
+ description: "Injected by sandbox server \u2014 workspace directory"
86
+ }
87
+ },
88
+ required: ["command"],
89
+ additionalProperties: false
90
+ }),
91
+ async execute({ command, timeout, _sandboxWorkDir }) {
92
+ if (!_sandboxWorkDir) {
93
+ throw new Error("shellExec requires a sandbox session (_sandboxWorkDir not set)");
94
+ }
95
+ const timeoutMs = Math.min(Math.max(timeout ?? DEFAULT_TIMEOUT_MS, 1e3), MAX_TIMEOUT_MS);
96
+ const startTime = Date.now();
97
+ const cmd = new Deno.Command("sh", {
98
+ args: ["-c", command],
99
+ cwd: _sandboxWorkDir,
100
+ stdout: "piped",
101
+ stderr: "piped"
102
+ });
103
+ const child = cmd.spawn();
104
+ let timedOut = false;
105
+ const timer = setTimeout(() => {
106
+ timedOut = true;
107
+ try {
108
+ child.kill("SIGTERM");
109
+ setTimeout(() => {
110
+ try {
111
+ child.kill("SIGKILL");
112
+ } catch {
113
+ }
114
+ }, 2e3);
115
+ } catch {
116
+ }
117
+ }, timeoutMs);
118
+ try {
119
+ const [stdoutBytes, stderrBytes, status] = await Promise.all([
120
+ collectStream(child.stdout),
121
+ collectStream(child.stderr),
122
+ child.status
123
+ ]);
124
+ clearTimeout(timer);
125
+ const durationMs = Date.now() - startTime;
126
+ const decoder = new TextDecoder("utf-8", { fatal: false });
127
+ const stdoutRaw = decoder.decode(stdoutBytes);
128
+ const stderrRaw = decoder.decode(stderrBytes);
129
+ const stdout = truncate(stdoutRaw, MAX_STDOUT);
130
+ const stderr = truncate(stderrRaw, MAX_STDOUT);
131
+ const result = {
132
+ stdout: stdout.text,
133
+ stderr: timedOut ? `[TIMEOUT after ${timeoutMs}ms] ${stderr.text}` : stderr.text,
134
+ exitCode: timedOut ? 137 : status.code,
135
+ durationMs,
136
+ truncated: stdout.truncated || stderr.truncated
137
+ };
138
+ return result;
139
+ } catch (error) {
140
+ clearTimeout(timer);
141
+ throw new Error(
142
+ `Shell execution failed: ${error instanceof Error ? error.message : String(error)}`
143
+ );
144
+ }
145
+ }
146
+ });
147
+ var readFile = tool({
148
+ description: "Read a file from the sandbox workspace.",
149
+ inputSchema: jsonSchema({
150
+ type: "object",
151
+ properties: {
152
+ path: {
153
+ type: "string",
154
+ description: "File path relative to workspace root"
155
+ },
156
+ _sandboxWorkDir: {
157
+ type: "string",
158
+ description: "Injected by sandbox server \u2014 workspace directory"
159
+ }
160
+ },
161
+ required: ["path"],
162
+ additionalProperties: false
163
+ }),
164
+ async execute({ path: filePath, _sandboxWorkDir }) {
165
+ if (!_sandboxWorkDir) {
166
+ throw new Error("readFile requires a sandbox session (_sandboxWorkDir not set)");
167
+ }
168
+ const resolved = resolveSandboxPath(_sandboxWorkDir, filePath);
169
+ try {
170
+ const realPath = await Deno.realPath(resolved);
171
+ const normalizedWorkDir = "/" + _sandboxWorkDir.replace(/^\/+/, "").replace(/\/+$/, "");
172
+ if (!realPath.startsWith(normalizedWorkDir)) {
173
+ throw new Error(`Path escapes sandbox workspace via symlink: ${filePath}`);
174
+ }
175
+ } catch (error) {
176
+ if (error instanceof Error && error.message.includes("escapes sandbox")) {
177
+ throw error;
178
+ }
179
+ }
180
+ const content = await Deno.readTextFile(resolved);
181
+ const { text, truncated } = truncate(content, MAX_READ_SIZE);
182
+ const stat = await Deno.stat(resolved);
183
+ const result = {
184
+ content: text,
185
+ path: filePath,
186
+ size: stat.size,
187
+ truncated
188
+ };
189
+ return result;
190
+ }
191
+ });
192
+ var writeFile = tool({
193
+ description: "Write or create a file in the sandbox workspace.",
194
+ inputSchema: jsonSchema({
195
+ type: "object",
196
+ properties: {
197
+ path: {
198
+ type: "string",
199
+ description: "File path relative to workspace root"
200
+ },
201
+ content: {
202
+ type: "string",
203
+ description: "Content to write to the file"
204
+ },
205
+ createDirs: {
206
+ type: "boolean",
207
+ description: "Create parent directories if they do not exist (default: true)"
208
+ },
209
+ _sandboxWorkDir: {
210
+ type: "string",
211
+ description: "Injected by sandbox server \u2014 workspace directory"
212
+ }
213
+ },
214
+ required: ["path", "content"],
215
+ additionalProperties: false
216
+ }),
217
+ async execute({ path: filePath, content, createDirs = true, _sandboxWorkDir }) {
218
+ if (!_sandboxWorkDir) {
219
+ throw new Error("writeFile requires a sandbox session (_sandboxWorkDir not set)");
220
+ }
221
+ const resolved = resolveSandboxPath(_sandboxWorkDir, filePath);
222
+ if (createDirs) {
223
+ const parentDir = resolved.substring(0, resolved.lastIndexOf("/"));
224
+ if (parentDir) {
225
+ await Deno.mkdir(parentDir, { recursive: true });
226
+ }
227
+ }
228
+ await Deno.writeTextFile(resolved, content);
229
+ const encoder = new TextEncoder();
230
+ const bytesWritten = encoder.encode(content).length;
231
+ const result = {
232
+ success: true,
233
+ path: filePath,
234
+ bytesWritten
235
+ };
236
+ return result;
237
+ }
238
+ });
239
+ var listFiles = tool({
240
+ description: "List files and directories in the sandbox workspace.",
241
+ inputSchema: jsonSchema({
242
+ type: "object",
243
+ properties: {
244
+ path: {
245
+ type: "string",
246
+ description: "Directory path relative to workspace root (default: root)"
247
+ },
248
+ recursive: {
249
+ type: "boolean",
250
+ description: "List recursively (default: false)"
251
+ },
252
+ maxDepth: {
253
+ type: "number",
254
+ description: "Maximum depth for recursive listing (default: 3)"
255
+ },
256
+ _sandboxWorkDir: {
257
+ type: "string",
258
+ description: "Injected by sandbox server \u2014 workspace directory"
259
+ }
260
+ },
261
+ additionalProperties: false
262
+ }),
263
+ async execute({ path: dirPath = ".", recursive = false, maxDepth, _sandboxWorkDir }) {
264
+ if (!_sandboxWorkDir) {
265
+ throw new Error("listFiles requires a sandbox session (_sandboxWorkDir not set)");
266
+ }
267
+ const effectiveMaxDepth = maxDepth ?? DEFAULT_MAX_DEPTH;
268
+ const resolved = resolveSandboxPath(_sandboxWorkDir, dirPath);
269
+ const entries = [];
270
+ let truncated = false;
271
+ const normalizedWorkDir = "/" + _sandboxWorkDir.replace(/^\/+/, "").replace(/\/+$/, "");
272
+ async function walk(dir, depth, prefix) {
273
+ if (entries.length >= MAX_LIST_ENTRIES) {
274
+ truncated = true;
275
+ return;
276
+ }
277
+ for await (const entry of Deno.readDir(dir)) {
278
+ if (entries.length >= MAX_LIST_ENTRIES) {
279
+ truncated = true;
280
+ return;
281
+ }
282
+ const entryPath = `${dir}/${entry.name}`;
283
+ const displayName = prefix ? `${prefix}/${entry.name}` : entry.name;
284
+ if (entry.isSymlink) {
285
+ try {
286
+ const realPath = await Deno.realPath(entryPath);
287
+ if (!realPath.startsWith(normalizedWorkDir)) {
288
+ continue;
289
+ }
290
+ } catch {
291
+ continue;
292
+ }
293
+ }
294
+ let size = null;
295
+ if (entry.isFile) {
296
+ try {
297
+ const info = await Deno.stat(entryPath);
298
+ size = info.size;
299
+ } catch {
300
+ }
301
+ }
302
+ entries.push({
303
+ name: displayName,
304
+ type: entry.isDirectory ? "directory" : entry.isSymlink ? "symlink" : "file",
305
+ size
306
+ });
307
+ if (recursive && entry.isDirectory && depth < effectiveMaxDepth) {
308
+ await walk(entryPath, depth + 1, displayName);
309
+ }
310
+ }
311
+ }
312
+ await walk(resolved, 0, "");
313
+ const result = {
314
+ path: dirPath,
315
+ entries,
316
+ truncated
317
+ };
318
+ return result;
319
+ }
320
+ });
321
+ var index_default = { shellExec, readFile, writeFile, listFiles };
322
+ export {
323
+ index_default as default,
324
+ listFiles,
325
+ readFile,
326
+ shellExec,
327
+ writeFile
328
+ };
329
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Sandbox Shell Tools for TPMJS Agent Sandbox\n *\n * Execute shell commands, read/write files, and list directories\n * within the stateful sandbox workspace. All paths are resolved\n * relative to `_sandboxWorkDir` (injected by the sandbox server).\n *\n * Runs inside Deno — uses Deno.Command, Deno.readTextFile, etc.\n */\n\nimport { jsonSchema, tool } from 'ai';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst MAX_STDOUT = 100 * 1024; // 100 KB\nconst MAX_READ_SIZE = 500 * 1024; // 500 KB\nconst DEFAULT_TIMEOUT_MS = 30_000;\nconst MAX_TIMEOUT_MS = 300_000;\nconst MAX_LIST_ENTRIES = 1_000;\nconst DEFAULT_MAX_DEPTH = 3;\n\n// ---------------------------------------------------------------------------\n// Result types\n// ---------------------------------------------------------------------------\n\nexport interface ShellExecResult {\n stdout: string;\n stderr: string;\n exitCode: number;\n durationMs: number;\n truncated: boolean;\n}\n\nexport interface ReadFileResult {\n content: string;\n path: string;\n size: number;\n truncated: boolean;\n}\n\nexport interface WriteFileResult {\n success: boolean;\n path: string;\n bytesWritten: number;\n}\n\nexport interface FileEntry {\n name: string;\n type: 'file' | 'directory' | 'symlink';\n size: number | null;\n}\n\nexport interface ListFilesResult {\n path: string;\n entries: FileEntry[];\n truncated: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction resolveSandboxPath(workDir: string, relativePath: string): string {\n // Normalize: remove leading slash (paths are relative to workDir)\n const cleaned = relativePath.replace(/^\\/+/, '');\n\n // Simple join — Deno runs on Linux so forward slashes only\n const resolved = `${workDir}/${cleaned}`.replace(/\\/+/g, '/');\n\n // Security: ensure the resolved path stays inside workDir\n // Normalize away any .. segments by splitting and resolving\n const parts = resolved.split('/');\n const normalized: string[] = [];\n for (const part of parts) {\n if (part === '..') {\n normalized.pop();\n } else if (part !== '.' && part !== '') {\n normalized.push(part);\n }\n }\n const final = '/' + normalized.join('/');\n\n const normalizedWorkDir = '/' + workDir.replace(/^\\/+/, '').replace(/\\/+$/, '');\n if (!final.startsWith(normalizedWorkDir)) {\n throw new Error(`Path escapes sandbox workspace: ${relativePath}`);\n }\n\n return final;\n}\n\nfunction truncate(text: string, maxBytes: number): { text: string; truncated: boolean } {\n const encoder = new TextEncoder();\n const bytes = encoder.encode(text);\n if (bytes.length <= maxBytes) {\n return { text, truncated: false };\n }\n // Truncate at byte boundary, decode back (may lose partial char)\n const decoder = new TextDecoder('utf-8', { fatal: false });\n return {\n text: decoder.decode(bytes.slice(0, maxBytes)) + '\\n... [truncated]',\n truncated: true,\n };\n}\n\nasync function collectStream(\n stream: ReadableStream<Uint8Array>,\n maxBytes: number = MAX_STDOUT * 2,\n): Promise<Uint8Array> {\n const reader = stream.getReader();\n const chunks: Uint8Array[] = [];\n let totalLength = 0;\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n chunks.push(value);\n totalLength += value.length;\n // Prevent OOM: stop reading if we exceed the limit\n if (totalLength > maxBytes) {\n reader.cancel().catch(() => {});\n break;\n }\n }\n } finally {\n reader.releaseLock();\n }\n const cappedLength = Math.min(totalLength, maxBytes);\n const result = new Uint8Array(cappedLength);\n let offset = 0;\n for (const chunk of chunks) {\n const copyLen = Math.min(chunk.length, cappedLength - offset);\n if (copyLen <= 0) break;\n result.set(chunk.subarray(0, copyLen), offset);\n offset += copyLen;\n }\n return result.subarray(0, offset);\n}\n\n// ---------------------------------------------------------------------------\n// shellExec\n// ---------------------------------------------------------------------------\n\ntype ShellExecInput = {\n command: string;\n timeout?: number;\n _sandboxWorkDir?: string;\n};\n\nexport const shellExec = tool({\n description:\n 'Execute a shell command in the sandbox workspace. Supports git, npm, and any CLI tools available in the container.',\n inputSchema: jsonSchema<ShellExecInput>({\n type: 'object',\n properties: {\n command: {\n type: 'string',\n description: 'Shell command to execute (passed to sh -c)',\n },\n timeout: {\n type: 'number',\n description: 'Timeout in milliseconds (default: 30000, max: 300000)',\n },\n _sandboxWorkDir: {\n type: 'string',\n description: 'Injected by sandbox server — workspace directory',\n },\n },\n required: ['command'],\n additionalProperties: false,\n }),\n async execute({ command, timeout, _sandboxWorkDir }) {\n if (!_sandboxWorkDir) {\n throw new Error('shellExec requires a sandbox session (_sandboxWorkDir not set)');\n }\n\n const timeoutMs = Math.min(Math.max(timeout ?? DEFAULT_TIMEOUT_MS, 1000), MAX_TIMEOUT_MS);\n const startTime = Date.now();\n\n const cmd = new Deno.Command('sh', {\n args: ['-c', command],\n cwd: _sandboxWorkDir,\n stdout: 'piped',\n stderr: 'piped',\n });\n\n const child = cmd.spawn();\n\n // Timeout handling — kill the process tree if it exceeds the limit\n let timedOut = false;\n const timer = setTimeout(() => {\n timedOut = true;\n try {\n // Try SIGTERM first for graceful shutdown\n child.kill('SIGTERM');\n // Force kill after 2 seconds if still running\n setTimeout(() => {\n try {\n child.kill('SIGKILL');\n } catch {\n // Already exited\n }\n }, 2000);\n } catch {\n // Process may have already exited\n }\n }, timeoutMs);\n\n try {\n // Collect stdout and stderr concurrently\n const [stdoutBytes, stderrBytes, status] = await Promise.all([\n collectStream(child.stdout),\n collectStream(child.stderr),\n child.status,\n ]);\n\n clearTimeout(timer);\n const durationMs = Date.now() - startTime;\n const decoder = new TextDecoder('utf-8', { fatal: false });\n\n const stdoutRaw = decoder.decode(stdoutBytes);\n const stderrRaw = decoder.decode(stderrBytes);\n\n const stdout = truncate(stdoutRaw, MAX_STDOUT);\n const stderr = truncate(stderrRaw, MAX_STDOUT);\n\n const result: ShellExecResult = {\n stdout: stdout.text,\n stderr: timedOut ? `[TIMEOUT after ${timeoutMs}ms] ${stderr.text}` : stderr.text,\n exitCode: timedOut ? 137 : status.code,\n durationMs,\n truncated: stdout.truncated || stderr.truncated,\n };\n\n return result;\n } catch (error) {\n clearTimeout(timer);\n throw new Error(\n `Shell execution failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n },\n});\n\n// ---------------------------------------------------------------------------\n// readFile\n// ---------------------------------------------------------------------------\n\ntype ReadFileInput = {\n path: string;\n _sandboxWorkDir?: string;\n};\n\nexport const readFile = tool({\n description: 'Read a file from the sandbox workspace.',\n inputSchema: jsonSchema<ReadFileInput>({\n type: 'object',\n properties: {\n path: {\n type: 'string',\n description: 'File path relative to workspace root',\n },\n _sandboxWorkDir: {\n type: 'string',\n description: 'Injected by sandbox server — workspace directory',\n },\n },\n required: ['path'],\n additionalProperties: false,\n }),\n async execute({ path: filePath, _sandboxWorkDir }) {\n if (!_sandboxWorkDir) {\n throw new Error('readFile requires a sandbox session (_sandboxWorkDir not set)');\n }\n\n const resolved = resolveSandboxPath(_sandboxWorkDir, filePath);\n\n // Symlink escape check: verify the real (canonical) path is still in workspace\n try {\n const realPath = await Deno.realPath(resolved);\n const normalizedWorkDir = '/' + _sandboxWorkDir.replace(/^\\/+/, '').replace(/\\/+$/, '');\n if (!realPath.startsWith(normalizedWorkDir)) {\n throw new Error(`Path escapes sandbox workspace via symlink: ${filePath}`);\n }\n } catch (error) {\n // Re-throw our own escape error\n if (error instanceof Error && error.message.includes('escapes sandbox')) {\n throw error;\n }\n // NotFound is fine — stat will fail with a better error below\n }\n\n const content = await Deno.readTextFile(resolved);\n\n const { text, truncated } = truncate(content, MAX_READ_SIZE);\n\n const stat = await Deno.stat(resolved);\n\n const result: ReadFileResult = {\n content: text,\n path: filePath,\n size: stat.size,\n truncated,\n };\n\n return result;\n },\n});\n\n// ---------------------------------------------------------------------------\n// writeFile\n// ---------------------------------------------------------------------------\n\ntype WriteFileInput = {\n path: string;\n content: string;\n createDirs?: boolean;\n _sandboxWorkDir?: string;\n};\n\nexport const writeFile = tool({\n description: 'Write or create a file in the sandbox workspace.',\n inputSchema: jsonSchema<WriteFileInput>({\n type: 'object',\n properties: {\n path: {\n type: 'string',\n description: 'File path relative to workspace root',\n },\n content: {\n type: 'string',\n description: 'Content to write to the file',\n },\n createDirs: {\n type: 'boolean',\n description: 'Create parent directories if they do not exist (default: true)',\n },\n _sandboxWorkDir: {\n type: 'string',\n description: 'Injected by sandbox server — workspace directory',\n },\n },\n required: ['path', 'content'],\n additionalProperties: false,\n }),\n async execute({ path: filePath, content, createDirs = true, _sandboxWorkDir }) {\n if (!_sandboxWorkDir) {\n throw new Error('writeFile requires a sandbox session (_sandboxWorkDir not set)');\n }\n\n const resolved = resolveSandboxPath(_sandboxWorkDir, filePath);\n\n // Create parent directories if requested\n if (createDirs) {\n const parentDir = resolved.substring(0, resolved.lastIndexOf('/'));\n if (parentDir) {\n await Deno.mkdir(parentDir, { recursive: true });\n }\n }\n\n await Deno.writeTextFile(resolved, content);\n\n const encoder = new TextEncoder();\n const bytesWritten = encoder.encode(content).length;\n\n const result: WriteFileResult = {\n success: true,\n path: filePath,\n bytesWritten,\n };\n\n return result;\n },\n});\n\n// ---------------------------------------------------------------------------\n// listFiles\n// ---------------------------------------------------------------------------\n\ntype ListFilesInput = {\n path?: string;\n recursive?: boolean;\n maxDepth?: number;\n _sandboxWorkDir?: string;\n};\n\nexport const listFiles = tool({\n description: 'List files and directories in the sandbox workspace.',\n inputSchema: jsonSchema<ListFilesInput>({\n type: 'object',\n properties: {\n path: {\n type: 'string',\n description: 'Directory path relative to workspace root (default: root)',\n },\n recursive: {\n type: 'boolean',\n description: 'List recursively (default: false)',\n },\n maxDepth: {\n type: 'number',\n description: 'Maximum depth for recursive listing (default: 3)',\n },\n _sandboxWorkDir: {\n type: 'string',\n description: 'Injected by sandbox server — workspace directory',\n },\n },\n additionalProperties: false,\n }),\n async execute({ path: dirPath = '.', recursive = false, maxDepth, _sandboxWorkDir }) {\n if (!_sandboxWorkDir) {\n throw new Error('listFiles requires a sandbox session (_sandboxWorkDir not set)');\n }\n\n const effectiveMaxDepth = maxDepth ?? DEFAULT_MAX_DEPTH;\n const resolved = resolveSandboxPath(_sandboxWorkDir, dirPath);\n const entries: FileEntry[] = [];\n let truncated = false;\n\n const normalizedWorkDir = '/' + _sandboxWorkDir.replace(/^\\/+/, '').replace(/\\/+$/, '');\n\n async function walk(dir: string, depth: number, prefix: string): Promise<void> {\n if (entries.length >= MAX_LIST_ENTRIES) {\n truncated = true;\n return;\n }\n\n for await (const entry of Deno.readDir(dir)) {\n if (entries.length >= MAX_LIST_ENTRIES) {\n truncated = true;\n return;\n }\n\n const entryPath = `${dir}/${entry.name}`;\n const displayName = prefix ? `${prefix}/${entry.name}` : entry.name;\n\n // Skip symlinks that escape the workspace\n if (entry.isSymlink) {\n try {\n const realPath = await Deno.realPath(entryPath);\n if (!realPath.startsWith(normalizedWorkDir)) {\n continue; // silently skip symlinks that point outside\n }\n } catch {\n continue; // skip broken symlinks\n }\n }\n\n let size: number | null = null;\n if (entry.isFile) {\n try {\n const info = await Deno.stat(entryPath);\n size = info.size;\n } catch {\n // Permission or race condition — skip size\n }\n }\n\n entries.push({\n name: displayName,\n type: entry.isDirectory ? 'directory' : entry.isSymlink ? 'symlink' : 'file',\n size,\n });\n\n if (recursive && entry.isDirectory && depth < effectiveMaxDepth) {\n await walk(entryPath, depth + 1, displayName);\n }\n }\n }\n\n await walk(resolved, 0, '');\n\n const result: ListFilesResult = {\n path: dirPath,\n entries,\n truncated,\n };\n\n return result;\n },\n});\n\n// ---------------------------------------------------------------------------\n// Default export\n// ---------------------------------------------------------------------------\n\nexport default { shellExec, readFile, writeFile, listFiles };\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@tpmjs/official-sandbox-shell",
3
+ "version": "0.1.0",
4
+ "description": "Shell execution, file I/O, and directory listing tools for the TPMJS Agent Sandbox",
5
+ "type": "module",
6
+ "keywords": [
7
+ "tpmjs",
8
+ "sandbox",
9
+ "shell",
10
+ "git",
11
+ "filesystem",
12
+ "ai",
13
+ "agent"
14
+ ],
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "default": "./dist/index.js"
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsup",
26
+ "dev": "tsup --watch",
27
+ "type-check": "tsc --noEmit --incremental --tsBuildInfoFile tsconfig.tsbuildinfo",
28
+ "clean": "rm -rf dist .turbo"
29
+ },
30
+ "devDependencies": {
31
+ "@tpmjs/tsconfig": "workspace:*",
32
+ "tsup": "^8.5.1",
33
+ "typescript": "^5.9.3"
34
+ },
35
+ "dependencies": {
36
+ "ai": "6.0.49"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://github.com/tpmjs/tpmjs.git",
44
+ "directory": "packages/tools/official/sandbox-shell"
45
+ },
46
+ "homepage": "https://tpmjs.com",
47
+ "license": "MIT",
48
+ "tpmjs": {
49
+ "category": "sandbox",
50
+ "frameworks": [
51
+ "vercel-ai"
52
+ ],
53
+ "tools": [
54
+ {
55
+ "name": "shellExec",
56
+ "description": "Execute a shell command in the sandbox workspace. Supports git, npm, and any CLI tools."
57
+ },
58
+ {
59
+ "name": "readFile",
60
+ "description": "Read a file from the sandbox workspace."
61
+ },
62
+ {
63
+ "name": "writeFile",
64
+ "description": "Write or create a file in the sandbox workspace."
65
+ },
66
+ {
67
+ "name": "listFiles",
68
+ "description": "List files and directories in the sandbox workspace."
69
+ }
70
+ ]
71
+ }
72
+ }