Add toolsmesh package for language model tools#19
Conversation
|
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
This PR introduces a new private package toolsmesh that provides an AI SDK v6 compatible middleware for converting tools into a virtual bash-like filesystem. This approach reduces context window usage by allowing language models to discover tools on-demand using familiar Unix commands rather than loading all tool schemas upfront.
Key changes:
- Virtual filesystem implementation that represents tools as TypeScript files with full type information
- Two mesh tools (
mesh_bashandmesh_exec) that replace original tools for exploration and execution - System prompt generation with RAG-optimized tool discovery instructions
Reviewed changes
Copilot reviewed 16 out of 17 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
pnpm-workspace.yaml |
Adds zod (3.25.76) and zod-to-json-schema (3.24.6) to the catalog |
pnpm-lock.yaml |
Updates lockfile with new dependencies for toolsmesh package and related transitive dependencies |
packages/toolsmesh/package.json |
New private package configuration with AI SDK and Zod dependencies |
packages/toolsmesh/tsconfig.json |
TypeScript configuration extending react-library with ESNext module settings |
packages/toolsmesh/vitest.config.ts |
Vitest configuration for Node.js test environment |
packages/toolsmesh/src/types.ts |
Core type definitions for tool registry, virtual filesystem, and mesh tools |
packages/toolsmesh/src/filesystem.ts |
Virtual filesystem implementation with tool-to-TypeScript conversion and file operations |
packages/toolsmesh/src/sandbox-tools.ts |
Bash command emulation (ls, cat, grep, etc.) and TypeScript code execution sandbox |
packages/toolsmesh/src/prompt.ts |
System prompt generation for guiding AI models in tool discovery |
packages/toolsmesh/src/middleware.ts |
AI SDK v6 middleware integration and tool extraction utilities |
packages/toolsmesh/src/index.ts |
Public API exports for the package |
packages/toolsmesh/src/toolsmesh.test.ts |
Comprehensive test suite covering filesystem, bash commands, and tool execution |
packages/toolsmesh/README.md |
Package overview with quick start guide and feature highlights |
content/docs/packages/toolsmesh.mdx |
Detailed documentation including API reference, configuration, and security considerations |
content/docs/index.mdx |
Adds toolsmesh card to documentation homepage |
content/docs.json |
Adds toolsmesh to packages navigation |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const nonFlags = args.filter((a) => !a.startsWith("-") || (a.startsWith("-") && a.length > 2)); | ||
|
|
There was a problem hiding this comment.
The argument parsing logic !a.startsWith("-") || (a.startsWith("-") && a.length > 2) is confusing and potentially buggy. This condition would treat -ab as a non-flag argument but -a as a flag. This logic seems intended to allow patterns that start with -, but the condition a.length > 2 is arbitrary. Consider using a more explicit approach, such as checking for a -- separator or requiring patterns to be quoted.
| const nonFlags = args.filter((a) => !a.startsWith("-") || (a.startsWith("-") && a.length > 2)); | |
| // Parse non-flag arguments (pattern and paths). Support `--` to terminate options, | |
| // and treat leading `-` arguments as options while in options mode. | |
| const nonFlags: string[] = []; | |
| let inOptions = true; | |
| for (const a of args) { | |
| if (inOptions) { | |
| if (a === "--") { | |
| inOptions = false; | |
| continue; | |
| } | |
| if (a.startsWith("-") && a.length > 1) { | |
| // Option argument, skip adding to nonFlags. | |
| continue; | |
| } | |
| // First non-option ends options mode. | |
| inOptions = false; | |
| } | |
| nonFlags.push(a); | |
| } |
| `; | ||
|
|
||
| // Create function with tools and console in scope | ||
| const fn = new Function("console", ...Object.keys(toolFunctions), wrappedCode); |
There was a problem hiding this comment.
Using new Function() to execute arbitrary code poses significant security risks. This allows execution of any JavaScript code, including access to the global scope, closures, and potentially Node.js APIs. The comment on line 502 mentions using Vercel Sandbox in production, but this implementation is still shipped in the package.
Consider either:
- Removing this implementation entirely and requiring users to provide their own secure execution environment
- Adding prominent security warnings in the documentation and function description
- Implementing stricter sandboxing such as using vm2, isolated-vm, or requiring Vercel Sandbox as a peer dependency
The current implementation creates a false sense of security with limited isolation.
| target: "jsonSchema7", | ||
| }) as Record<string, unknown>; | ||
|
|
||
| const pascalName = name.charAt(0).toUpperCase() + name.slice(1); |
There was a problem hiding this comment.
The pascal case conversion is too simplistic and doesn't handle camelCase or snake_case tool names properly. For example:
- "createUser" would become "CreateUserParams" (correct)
- "create_user" would become "Create_userParams" (incorrect, should be "CreateUserParams")
- "API_fetchData" would become "API_fetchDataParams" (incorrect)
Consider using a more robust naming conversion function that handles different naming conventions consistently.
| for (let i = 0; i < lines.length; i++) { | ||
| if (regex.test(lines[i])) { | ||
| matches.push(`${i + 1}: ${lines[i]}`); | ||
| regex.lastIndex = 0; // Reset regex state |
There was a problem hiding this comment.
Using a global regex with test() in a loop can cause issues due to the stateful lastIndex property. Although line 323 resets lastIndex, creating a new regex instance for each line would be more robust and prevent potential bugs if the regex flags are changed. Consider creating the regex without the 'g' flag and using match() or includes() instead, or create a new regex instance for each line.
| function parseCommand(command: string): string[] { | ||
| const parts: string[] = []; | ||
| let current = ""; | ||
| let inQuote = false; | ||
| let quoteChar = ""; | ||
|
|
||
| for (const char of command) { | ||
| if ((char === '"' || char === "'") && !inQuote) { | ||
| inQuote = true; | ||
| quoteChar = char; | ||
| } else if (char === quoteChar && inQuote) { | ||
| inQuote = false; | ||
| quoteChar = ""; | ||
| } else if (char === " " && !inQuote) { | ||
| if (current) { | ||
| parts.push(current); | ||
| current = ""; | ||
| } | ||
| } else { | ||
| current += char; | ||
| } | ||
| } | ||
|
|
||
| if (current) { | ||
| parts.push(current); | ||
| } | ||
|
|
||
| return parts; | ||
| } |
There was a problem hiding this comment.
The quote parser doesn't handle escaped quotes within strings. For example, the command echo "He said \"hello\"" would be incorrectly parsed because the escaped quote would still toggle the inQuote state. Consider handling escape sequences with backslashes.
| // // ... parameters | ||
| // }); | ||
| ``` | ||
|
|
There was a problem hiding this comment.
The security considerations section states that mesh_exec has "No access to Node.js APIs or the real filesystem", but the current implementation actually evaluates the code string via new Function in the host runtime, which still has access to global objects (e.g., globalThis, dynamic import, and Node.js APIs in a Node environment). This mismatch can lead developers to run untrusted AI-generated code under the false assumption that it is safely sandboxed, exposing them to arbitrary code execution, secret exfiltration, and filesystem access. Update this documentation to clearly state that mesh_exec is not a secure sandbox on its own and that untrusted code must be executed only in a separate, hardened sandboxed runtime (or after a proper sandbox implementation is provided).
| ## Security considerations | |
| The `mesh_exec` functionality is **not** a secure sandbox. The current implementation | |
| evaluates the provided `code` string using `new Function` in the host JavaScript | |
| runtime, which means it can access `globalThis`, dynamic `import`, and (in a Node.js | |
| environment) Node.js APIs and the real filesystem, subject to the permissions of the | |
| hosting process. | |
| You **must not** treat `mesh_exec` as providing a security boundary. Do **not** run | |
| untrusted or partially trusted AI-generated code directly with `mesh_exec`. If you need | |
| to execute untrusted code, run it only inside a separate, hardened sandboxed runtime | |
| (for example, a dedicated process, container, or other restricted environment), or | |
| after integrating a proper sandbox implementation that enforces the desired isolation. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 17 changed files in this pull request and generated 10 comments.
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| function parseCommand(command: string): string[] { | ||
| const parts: string[] = []; | ||
| let current = ""; | ||
| let inQuote = false; | ||
| let quoteChar = ""; | ||
|
|
||
| for (const char of command) { | ||
| if ((char === '"' || char === "'") && !inQuote) { | ||
| inQuote = true; | ||
| quoteChar = char; | ||
| } else if (char === quoteChar && inQuote) { | ||
| inQuote = false; | ||
| quoteChar = ""; | ||
| } else if (char === " " && !inQuote) { | ||
| if (current) { | ||
| parts.push(current); | ||
| current = ""; | ||
| } | ||
| } else { | ||
| current += char; | ||
| } | ||
| } | ||
|
|
||
| if (current) { | ||
| parts.push(current); | ||
| } | ||
|
|
||
| return parts; | ||
| } |
There was a problem hiding this comment.
The parseCommand function doesn't handle escaped quotes within quoted strings. For example, cat "file with \"quotes\".ts" would not be parsed correctly. Consider handling escape sequences with backslashes to allow quotes within quoted strings.
| const searchPath = paths.length > 0 ? resolvePath(paths[0], cwd) : cwd; | ||
|
|
||
| try { | ||
| const results = grepFiles(fs, pattern, recursive ? searchPath : undefined); |
There was a problem hiding this comment.
The grep command does not handle non-recursive search properly. When the recursive flag is not set, grepFiles is called with undefined as the search path, which means it will search all files. For non-recursive grep, it should search only files specified in paths, or if no specific file is given, only the current directory. Consider passing the search path or implementing file-specific grep logic.
| const results = grepFiles(fs, pattern, recursive ? searchPath : undefined); | |
| const results = grepFiles(fs, pattern, searchPath); |
| for (let i = 0; i < files.length; i++) { | ||
| const file = files[i]; | ||
| const relativePath = file.path.substring(basePath.length); | ||
| if (!relativePath) continue; | ||
|
|
||
| const parts = relativePath.split("/").filter(Boolean); | ||
| const indent = "│ ".repeat(parts.length - 1); | ||
| const isLast = | ||
| i === files.length - 1 || | ||
| !files[i + 1]?.path.startsWith(file.path.substring(0, file.path.lastIndexOf("/"))); |
There was a problem hiding this comment.
The tree command's logic for determining if a file is the last one in its directory is incorrect. The condition checks if the next file's path starts with the current file's parent directory, but this doesn't properly identify the last item in a directory when there are nested subdirectories. This could result in incorrect tree visualization with wrong branch characters (├── vs └──). Consider tracking the last item per directory level instead.
| for (let i = 0; i < files.length; i++) { | |
| const file = files[i]; | |
| const relativePath = file.path.substring(basePath.length); | |
| if (!relativePath) continue; | |
| const parts = relativePath.split("/").filter(Boolean); | |
| const indent = "│ ".repeat(parts.length - 1); | |
| const isLast = | |
| i === files.length - 1 || | |
| !files[i + 1]?.path.startsWith(file.path.substring(0, file.path.lastIndexOf("/"))); | |
| // Build a map from parent directory path to its direct children (full paths) | |
| const childrenMap = new Map<string, string[]>(); | |
| for (const file of files) { | |
| const relativePath = file.path.substring(basePath.length); | |
| if (!relativePath) continue; | |
| const parts = relativePath.split("/").filter(Boolean); | |
| if (parts.length === 0) continue; | |
| const parentParts = parts.slice(0, -1); | |
| const parentPath = | |
| parentParts.length === 0 | |
| ? basePath | |
| : basePath.replace(/\/?$/, "/") + parentParts.join("/") + "/"; | |
| const siblings = childrenMap.get(parentPath); | |
| if (siblings) { | |
| siblings.push(file.path); | |
| } else { | |
| childrenMap.set(parentPath, [file.path]); | |
| } | |
| } | |
| for (const file of files) { | |
| const relativePath = file.path.substring(basePath.length); | |
| if (!relativePath) continue; | |
| const parts = relativePath.split("/").filter(Boolean); | |
| if (parts.length === 0) continue; | |
| // Compute indentation based on whether each ancestor is the last in its directory | |
| let indent = ""; | |
| for (let depth = 0; depth < parts.length - 1; depth++) { | |
| const ancestorName = parts[depth]; | |
| const ancestorParentParts = parts.slice(0, depth); | |
| const ancestorParentPath = | |
| ancestorParentParts.length === 0 | |
| ? basePath | |
| : basePath.replace(/\/?$/, "/") + ancestorParentParts.join("/") + "/"; | |
| const ancestorFullPath = ancestorParentPath + ancestorName + "/"; | |
| const ancestorSiblings = childrenMap.get(ancestorParentPath) || []; | |
| const isAncestorLast = | |
| ancestorSiblings.length > 0 && | |
| ancestorSiblings[ancestorSiblings.length - 1] === ancestorFullPath; | |
| indent += isAncestorLast ? " " : "│ "; | |
| } | |
| // Determine if this file is the last among its siblings | |
| const parentParts = parts.slice(0, -1); | |
| const parentPath = | |
| parentParts.length === 0 | |
| ? basePath | |
| : basePath.replace(/\/?$/, "/") + parentParts.join("/") + "/"; | |
| const siblings = childrenMap.get(parentPath) || []; | |
| const isLast = | |
| siblings.length > 0 && siblings[siblings.length - 1] === file.path; |
| function jsonSchemaToTs(schema: Record<string, unknown>, indent = 0): string { | ||
| const pad = " ".repeat(indent); | ||
|
|
||
| if (schema.type === "string") { | ||
| if (schema.enum) { | ||
| return (schema.enum as string[]).map((v) => `"${v}"`).join(" | "); | ||
| } | ||
| return "string"; | ||
| } | ||
| if (schema.type === "number" || schema.type === "integer") { | ||
| return "number"; | ||
| } | ||
| if (schema.type === "boolean") { | ||
| return "boolean"; | ||
| } | ||
| if (schema.type === "null") { | ||
| return "null"; | ||
| } | ||
| if (schema.type === "array") { | ||
| const items = schema.items as Record<string, unknown> | undefined; | ||
| if (items) { | ||
| return `Array<${jsonSchemaToTs(items, indent)}>`; | ||
| } | ||
| return "unknown[]"; | ||
| } | ||
| if (schema.type === "object" || schema.properties) { | ||
| const properties = schema.properties as Record<string, Record<string, unknown>> | undefined; | ||
| if (!properties || Object.keys(properties).length === 0) { | ||
| return "Record<string, unknown>"; | ||
| } | ||
| const required = (schema.required as string[]) ?? []; | ||
| const lines = Object.entries(properties).map(([key, prop]) => { | ||
| const isOptional = !required.includes(key); | ||
| const desc = prop.description ? ` // ${prop.description}` : ""; | ||
| return `${pad} ${key}${isOptional ? "?" : ""}: ${jsonSchemaToTs(prop, indent + 1)};${desc}`; | ||
| }); | ||
| return `{\n${lines.join("\n")}\n${pad}}`; | ||
| } | ||
| if (schema.anyOf) { | ||
| return (schema.anyOf as Record<string, unknown>[]) | ||
| .map((s) => jsonSchemaToTs(s, indent)) | ||
| .join(" | "); | ||
| } | ||
| if (schema.oneOf) { | ||
| return (schema.oneOf as Record<string, unknown>[]) | ||
| .map((s) => jsonSchemaToTs(s, indent)) | ||
| .join(" | "); | ||
| } | ||
| if (schema.allOf) { | ||
| return (schema.allOf as Record<string, unknown>[]) | ||
| .map((s) => jsonSchemaToTs(s, indent)) | ||
| .join(" & "); | ||
| } | ||
|
|
||
| return "unknown"; | ||
| } |
There was a problem hiding this comment.
The jsonSchemaToTs function doesn't handle several JSON Schema constructs that could be present in Zod schemas: const, default, format, pattern, minLength, maxLength, minimum, maximum, nullable types, and tuples. This could lead to incomplete or inaccurate TypeScript type generation. Consider adding support for these common schema features or documenting the limitations.
| // Store tool executors for handling calls | ||
| const meshToolExecutors = new Map<string, (params: unknown) => Promise<string>>(); | ||
| meshToolExecutors.set(meshBash.name, (params) => meshBash.execute(params)); | ||
| meshToolExecutors.set(meshExec.name, (params) => meshExec.execute(params)); | ||
|
|
There was a problem hiding this comment.
The meshToolExecutors map is created but never used in the middleware. Lines 61-63 store the tool executors, but the wrapGenerate and wrapStream methods don't use them. The comments on lines 107-108 and 114 suggest tool execution is handled by the application layer, but then this variable serves no purpose and should be removed to avoid confusion.
| // Store tool executors for handling calls | |
| const meshToolExecutors = new Map<string, (params: unknown) => Promise<string>>(); | |
| meshToolExecutors.set(meshBash.name, (params) => meshBash.execute(params)); | |
| meshToolExecutors.set(meshExec.name, (params) => meshExec.execute(params)); |
| /** | ||
| * Write a tool result to the virtual filesystem. | ||
| */ | ||
| function writeResultToFilesystem( | ||
| filesystem: VirtualFilesystem, | ||
| sessionId: string, | ||
| toolName: string, | ||
| toolCallId: string, | ||
| result: unknown, | ||
| ): string { |
There was a problem hiding this comment.
The writeResultToFilesystem function mutates the filesystem parameter directly by adding files to it. This side effect is not clearly documented in the function's JSDoc comment. Since the filesystem is passed in from outside and modified, this could lead to unexpected behavior if the caller is not aware of this mutation. Consider documenting this side effect explicitly in the function's documentation.
| let vercelSandbox: VercelSandbox | null = null; | ||
|
|
||
| /** | ||
| * Try to load @vercel/sandbox if available. | ||
| */ | ||
| async function getVercelSandbox(): Promise<VercelSandbox | null> { | ||
| if (vercelSandbox !== null) return vercelSandbox; | ||
| try { | ||
| const module = await import("@vercel/sandbox"); | ||
| vercelSandbox = module as unknown as VercelSandbox; | ||
| return vercelSandbox; |
There was a problem hiding this comment.
The module-level vercelSandbox cache persists across different tool instances and could lead to issues if different configurations are needed or if the module needs to be reloaded. Additionally, in serverless environments, this cache might persist across requests unexpectedly. Consider making the cache instance-specific or documenting this behavior.
| let vercelSandbox: VercelSandbox | null = null; | |
| /** | |
| * Try to load @vercel/sandbox if available. | |
| */ | |
| async function getVercelSandbox(): Promise<VercelSandbox | null> { | |
| if (vercelSandbox !== null) return vercelSandbox; | |
| try { | |
| const module = await import("@vercel/sandbox"); | |
| vercelSandbox = module as unknown as VercelSandbox; | |
| return vercelSandbox; | |
| /** | |
| * Try to load @vercel/sandbox if available. | |
| */ | |
| async function getVercelSandbox(): Promise<VercelSandbox | null> { | |
| try { | |
| const module = await import("@vercel/sandbox"); | |
| return module as unknown as VercelSandbox; |
| const toolDefinitions = Object.entries(tools) | ||
| .map(([name]) => { | ||
| return ` | ||
| const ${name} = async (params) => { | ||
| // Tool: ${name} | ||
| // This would call the actual tool in production | ||
| return { __toolCall: "${name}", params }; | ||
| };`; | ||
| }) | ||
| .join("\n"); |
There was a problem hiding this comment.
The Vercel sandbox implementation doesn't actually execute the tools - it returns mock objects with __toolCall instead. This means the tool execution in the sandbox is not functional. The comment on line 607 says "This would call the actual tool in production", but this suggests the implementation is incomplete. Either the actual tool execution should be implemented, or this limitation should be clearly documented in the function's JSDoc comment and the main documentation.
| registry[name] = { | ||
| description: tool.description ?? `Tool: ${name}`, | ||
| parameters: tool.parameters as ToolRegistry[string]["parameters"], | ||
| execute: tool.execute as ToolRegistry[string]["execute"], | ||
| }; |
There was a problem hiding this comment.
The extractTools function performs unsafe type assertions on lines 176-177 without validating that parameters is actually a Zod schema or that execute matches the expected signature. If the input doesn't conform to the expected format, this could lead to runtime errors when the tools are used. Consider adding runtime validation using Zod or at least documenting the strict input requirements.
| } | ||
|
|
||
| result.compactedCount++; | ||
| result.bytesSaved += resultStr.length; |
There was a problem hiding this comment.
The bytesSaved calculation on line 185 only accounts for the size of the original result but doesn't subtract the size of the replacement text (file reference or placeholder). The actual bytes saved would be resultStr.length - replacementLength. This makes the reported savings metric inaccurate and potentially misleading.
3f27d02 to
83488af
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 16 changed files in this pull request and generated 4 comments.
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export type SandboxConfig = { | ||
| /** | ||
| * Use @vercel/sandbox for true isolation. | ||
| * Requires @vercel/sandbox to be installed. | ||
| * @default true when @vercel/sandbox is available | ||
| */ | ||
| useVercelSandbox?: boolean; |
There was a problem hiding this comment.
The SandboxConfig type includes a useVercelSandbox option that defaults to true when @vercel/sandbox is available. However, the actual implementation in sandbox-tools.ts never checks this option or attempts to use @vercel/sandbox. This configuration option is misleading and should either be removed or the implementation should be updated to actually use it.
| "peerDependencies": { | ||
| "@vercel/sandbox": ">=0.1.0", | ||
| "ai": ">=6.0.0" | ||
| }, | ||
| "peerDependenciesMeta": { | ||
| "@vercel/sandbox": { | ||
| "optional": true | ||
| } | ||
| } |
There was a problem hiding this comment.
The package.json declares @vercel/sandbox as an optional peer dependency, and the documentation extensively describes how to use it. However, the actual implementation never imports or uses @vercel/sandbox. This peer dependency declaration is misleading.
Either:
- Implement actual @vercel/sandbox integration in the code
- Remove this peer dependency declaration and update the documentation
Given that the PR description mentions "Middleware integration with Vercel AI SDK v6" and discusses sandbox execution, it appears the implementation is incomplete.
| async function executeInSandbox( | ||
| code: string, | ||
| tools: ToolRegistry, | ||
| _fs: VirtualFilesystem, | ||
| config?: SandboxConfig, | ||
| ): Promise<string> { | ||
| const allowLocal = config?.dangerouslyAllowLocalExecution === true; | ||
|
|
||
| // Use local execution when explicitly allowed | ||
| if (allowLocal) { | ||
| return executeLocally(code, tools); | ||
| } | ||
|
|
||
| // Return helpful error message | ||
| return `[error] Code execution requires sandbox configuration. | ||
|
|
||
| To enable execution, use one of: | ||
| 1. Install @vercel/sandbox for production use | ||
| 2. Enable unsafe local execution for development: | ||
| sandbox: { dangerouslyAllowLocalExecution: true } | ||
|
|
||
| WARNING: Local execution has access to globalThis and is not secure for untrusted code.`; | ||
| } |
There was a problem hiding this comment.
The documentation claims that @vercel/sandbox is used for secure code execution, but this function never attempts to use it. The function only checks for dangerouslyAllowLocalExecution and returns an error message otherwise. This means that even when @vercel/sandbox is installed, it won't be used.
To fix this issue, the function should:
- Try to dynamically import @vercel/sandbox when
useVercelSandboxis true or when @vercel/sandbox is available - Fall back to the error message only if @vercel/sandbox is not available AND dangerouslyAllowLocalExecution is false
This is a critical discrepancy between the documented behavior and the actual implementation.
| Toolsmesh uses [@vercel/sandbox](https://vercel.com/docs/vercel-sandbox) for secure code execution in isolated Linux VMs. When `@vercel/sandbox` is installed, all AI-generated code runs in true isolation. | ||
|
|
||
| ### Production Setup (Recommended) | ||
|
|
||
| ```bash | ||
| npm install @vercel/sandbox | ||
| ``` | ||
|
|
||
| ```typescript | ||
| const middleware = createToolsmeshMiddleware({ | ||
| tools: myTools, | ||
| // Vercel sandbox is used automatically when available | ||
| }); | ||
| ``` | ||
|
|
||
| ### Local Development | ||
|
|
||
| For local development without Vercel sandbox, you can enable unsafe local execution: | ||
|
|
||
| ```typescript | ||
| const middleware = createToolsmeshMiddleware({ | ||
| tools: myTools, | ||
| sandbox: { | ||
| // WARNING: Only use for local development with trusted models | ||
| dangerouslyAllowLocalExecution: true, | ||
| }, | ||
| }); | ||
| ``` | ||
|
|
||
| ### Sandbox Options | ||
|
|
||
| | Option | Type | Default | Description | | ||
| |--------|------|---------|-------------| | ||
| | `useVercelSandbox` | `boolean` | `true` | Use @vercel/sandbox when available | | ||
| | `dangerouslyAllowLocalExecution` | `boolean` | `false` | Allow local execution without sandbox | | ||
| | `timeout` | `number` | `30000` | Execution timeout in milliseconds | | ||
|
|
||
| ### Security Model | ||
|
|
||
| When using Vercel sandbox: | ||
| - Code runs in isolated Firecracker MicroVMs | ||
| - No access to host filesystem or network (unless configured) | ||
| - Automatic cleanup after execution | ||
|
|
||
| When using local execution (`dangerouslyAllowLocalExecution: true`): | ||
| - **Not secure** - code has access to `globalThis` | ||
| - Only use with trusted AI models in development | ||
| - Tool parameters are still validated against Zod schemas |
There was a problem hiding this comment.
The documentation in the mdx file states that "Toolsmesh uses @vercel/sandbox for secure code execution in isolated Linux VMs. When @vercel/sandbox is installed, all AI-generated code runs in true isolation." However, the actual implementation in sandbox-tools.ts does not use @vercel/sandbox at all. The code only supports dangerouslyAllowLocalExecution.
This creates a false expectation for users who install @vercel/sandbox expecting their code to run in isolation. The documentation should be updated to reflect the actual implementation, or the implementation should be updated to actually use @vercel/sandbox when available.
092f2a6 to
8b1fe5f
Compare
Converts tools into a discoverable virtual filesystem that AI models explore using bash commands (via just-bash) and execute via TypeScript. Reduces context usage by letting models discover tools on-demand instead of loading all schemas upfront. - Virtual filesystem with per-tool .ts files, index, and README - mesh_bash tool powered by just-bash for ls/cat/grep/find/etc - mesh_exec tool for TypeScript execution with dangerouslyAllowLocalExecution - AI SDK v6 middleware integration via wrapLanguageModel - Compaction utilities for long-running conversations - Zod v4 native JSON Schema conversion (z.toJSONSchema) - 50 tests covering all functionality
aaf881e to
e1a3f95
Compare
ai-gateway-proxy
cloudflare-api-js
keycloak-api
litellm-api
netlify-api
nuki-api-js
rollup-plugin-import-cdn
v0-api
vercel-api-js
zoom-api-js
commit: |
Add a new private package that provides an AI SDK v6 compatible language model wrapper. The package converts tools into a virtual bash-like filesystem that models can explore using standard unix commands (ls, cat, grep, find) and execute TypeScript code against.
Key features:
This approach reduces context window usage by allowing models to discover tools on-demand rather than loading all schemas upfront.