feat(filesystem): S3-backed filesystem support for agents#2745
feat(filesystem): S3-backed filesystem support for agents#2745viktormarinho wants to merge 5 commits intomainfrom
Conversation
Add complete filesystem/object storage integration for AI agents: - New FILESYSTEM_BINDING with 5 tools (Read, Write, List, Delete, Metadata) - S3Service singleton with 1MB inline read limit, base64 binary encoding - Path sanitization with null byte/traversal attack protection - Well-known filesystem connection (org-scoped, fixed, HTTP type) - Agent UI toggle to enable/disable filesystem access per agent - Environment-based S3 configuration (single shared bucket per mesh) - Organization isolation via org-ID prefix in S3 keys - MCP server implementation handling both direct calls and MCP protocol Filesystem is a separate connection (not core tools) for selective agent access. Org-ID prefix ensures immutable isolation (unlike org slug which can change). Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
🧪 BenchmarkShould we run the Virtual MCP strategy benchmark for this PR? React with 👍 to run the benchmark.
Benchmark will run on the next push after you react. |
Release OptionsShould a new version be published when this PR is merged? React with an emoji to vote on the release type:
Current version: Deployment
|
There was a problem hiding this comment.
9 issues found across 19 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/mesh/src/env.ts">
<violation number="1" location="apps/mesh/src/env.ts:53">
P2: `S3_FORCE_PATH_STYLE` is parsed with a different boolean format than the rest of `env.ts`, so common values like `0`/`1` will now abort startup.</violation>
</file>
<file name="apps/mesh/src/filesystem/s3-service.ts">
<violation number="1" location="apps/mesh/src/filesystem/s3-service.ts:142">
P2: Validate base64 input before decoding it; malformed payloads are silently written as corrupted files.</violation>
<violation number="2" location="apps/mesh/src/filesystem/s3-service.ts:167">
P2: Recursive `**` glob filters are broken because this always lists S3 one directory level at a time.</violation>
<violation number="3" location="apps/mesh/src/filesystem/s3-service.ts:265">
P2: `**` is translated incorrectly here, so patterns like `docs/**/*.md` skip `docs/readme.md`.</violation>
</file>
<file name="apps/mesh/src/api/app.ts">
<violation number="1" location="apps/mesh/src/api/app.ts:991">
P2: Validate `:connectionId` against the active organization before serving `*_filesystem` requests; right now any suffix-matching ID is accepted and routed to the current org’s filesystem.</violation>
<violation number="2" location="apps/mesh/src/api/app.ts:991">
P1: Add connection/tool authorization before executing filesystem MCP requests; these routes currently bypass the normal AccessControl checks and expose org file operations to any authenticated caller.</violation>
</file>
<file name="apps/mesh/src/filesystem/factory.ts">
<violation number="1" location="apps/mesh/src/filesystem/factory.ts:24">
P2: `initialized` is set too early, so an initial call with missing S3 env vars permanently disables the filesystem service for the rest of the process.</violation>
</file>
<file name="packages/bindings/src/well-known/filesystem.ts">
<violation number="1" location="packages/bindings/src/well-known/filesystem.ts:31">
P2: Restrict `offset` and `limit` to non-negative integers so invalid byte ranges are rejected at the binding layer.</violation>
<violation number="2" location="packages/bindings/src/well-known/filesystem.ts:119">
P2: Enforce the documented `maxKeys <= 1000` limit in the schema.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| const { handleFilesystemMcpRequest } = await import( | ||
| "./routes/filesystem-mcp" | ||
| ); | ||
| return handleFilesystemMcpRequest(c.req.raw, ctx); |
There was a problem hiding this comment.
P1: Add connection/tool authorization before executing filesystem MCP requests; these routes currently bypass the normal AccessControl checks and expose org file operations to any authenticated caller.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/api/app.ts, line 991:
<comment>Add connection/tool authorization before executing filesystem MCP requests; these routes currently bypass the normal AccessControl checks and expose org file operations to any authenticated caller.</comment>
<file context>
@@ -971,6 +971,47 @@ export async function createApp(options: CreateAppOptions = {}) {
+ const { handleFilesystemMcpRequest } = await import(
+ "./routes/filesystem-mcp"
+ );
+ return handleFilesystemMcpRequest(c.req.raw, ctx);
+ },
+ );
</file context>
| S3_REGION: z.string().default("auto"), | ||
| S3_ACCESS_KEY_ID: z.string().optional(), | ||
| S3_SECRET_ACCESS_KEY: z.string().optional(), | ||
| S3_FORCE_PATH_STYLE: z.enum(["true", "false"]).optional().default("true"), |
There was a problem hiding this comment.
P2: S3_FORCE_PATH_STYLE is parsed with a different boolean format than the rest of env.ts, so common values like 0/1 will now abort startup.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/env.ts, line 53:
<comment>`S3_FORCE_PATH_STYLE` is parsed with a different boolean format than the rest of `env.ts`, so common values like `0`/`1` will now abort startup.</comment>
<file context>
@@ -44,6 +44,14 @@ const envSchema = z
+ S3_REGION: z.string().default("auto"),
+ S3_ACCESS_KEY_ID: z.string().optional(),
+ S3_SECRET_ACCESS_KEY: z.string().optional(),
+ S3_FORCE_PATH_STYLE: z.enum(["true", "false"]).optional().default("true"),
+
// Debug / K8s
</file context>
| .replace(/[.+^${}()|[\]\\]/g, "\\$&") | ||
| .replace(/\*\*/g, "{{GLOBSTAR}}") | ||
| .replace(/\*/g, "[^/]*") | ||
| .replace(/{{GLOBSTAR}}/g, ".*") |
There was a problem hiding this comment.
P2: ** is translated incorrectly here, so patterns like docs/**/*.md skip docs/readme.md.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/filesystem/s3-service.ts, line 265:
<comment>`**` is translated incorrectly here, so patterns like `docs/**/*.md` skip `docs/readme.md`.</comment>
<file context>
@@ -0,0 +1,269 @@
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
+ .replace(/\*\*/g, "{{GLOBSTAR}}")
+ .replace(/\*/g, "[^/]*")
+ .replace(/{{GLOBSTAR}}/g, ".*")
+ .replace(/\?/g, "[^/]");
+
</file context>
|
|
||
| let body: Buffer; | ||
| if (encoding === "base64") { | ||
| body = Buffer.from(content, "base64"); |
There was a problem hiding this comment.
P2: Validate base64 input before decoding it; malformed payloads are silently written as corrupted files.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/filesystem/s3-service.ts, line 142:
<comment>Validate base64 input before decoding it; malformed payloads are silently written as corrupted files.</comment>
<file context>
@@ -0,0 +1,269 @@
+
+ let body: Buffer;
+ if (encoding === "base64") {
+ body = Buffer.from(content, "base64");
+ } else {
+ body = Buffer.from(content, "utf-8");
</file context>
| new ListObjectsV2Command({ | ||
| Bucket: this.bucket, | ||
| Prefix: prefix, | ||
| Delimiter: "/", |
There was a problem hiding this comment.
P2: Recursive ** glob filters are broken because this always lists S3 one directory level at a time.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/filesystem/s3-service.ts, line 167:
<comment>Recursive `**` glob filters are broken because this always lists S3 one directory level at a time.</comment>
<file context>
@@ -0,0 +1,269 @@
+ new ListObjectsV2Command({
+ Bucket: this.bucket,
+ Prefix: prefix,
+ Delimiter: "/",
+ MaxKeys: maxKeys,
+ ContinuationToken: input.continuationToken,
</file context>
| Delimiter: "/", | |
| Delimiter: input.pattern?.includes("**") ? undefined : "/", |
| const { handleFilesystemMcpRequest } = await import( | ||
| "./routes/filesystem-mcp" | ||
| ); | ||
| return handleFilesystemMcpRequest(c.req.raw, ctx); |
There was a problem hiding this comment.
P2: Validate :connectionId against the active organization before serving *_filesystem requests; right now any suffix-matching ID is accepted and routed to the current org’s filesystem.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/api/app.ts, line 991:
<comment>Validate `:connectionId` against the active organization before serving `*_filesystem` requests; right now any suffix-matching ID is accepted and routed to the current org’s filesystem.</comment>
<file context>
@@ -971,6 +971,47 @@ export async function createApp(options: CreateAppOptions = {}) {
+ const { handleFilesystemMcpRequest } = await import(
+ "./routes/filesystem-mcp"
+ );
+ return handleFilesystemMcpRequest(c.req.raw, ctx);
+ },
+ );
</file context>
| return handleFilesystemMcpRequest(c.req.raw, ctx); | |
| const connectionId = c.req.param("connectionId"); | |
| if ( | |
| ctx.organization?.id && | |
| connectionId !== WellKnownOrgMCPId.FILESYSTEM(ctx.organization.id) | |
| ) { | |
| return c.json({ error: "Connection not found" }, 404); | |
| } | |
| return handleFilesystemMcpRequest(c.req.raw, ctx); |
| return cachedService; | ||
| } | ||
|
|
||
| initialized = true; |
There was a problem hiding this comment.
P2: initialized is set too early, so an initial call with missing S3 env vars permanently disables the filesystem service for the rest of the process.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mesh/src/filesystem/factory.ts, line 24:
<comment>`initialized` is set too early, so an initial call with missing S3 env vars permanently disables the filesystem service for the rest of the process.</comment>
<file context>
@@ -0,0 +1,58 @@
+ return cachedService;
+ }
+
+ initialized = true;
+
+ const endpoint = process.env.S3_ENDPOINT;
</file context>
| */ | ||
| const FsReadInputSchema = z.object({ | ||
| path: z.string().describe("File path to read (e.g., 'docs/readme.md')"), | ||
| offset: z |
There was a problem hiding this comment.
P2: Restrict offset and limit to non-negative integers so invalid byte ranges are rejected at the binding layer.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/bindings/src/well-known/filesystem.ts, line 31:
<comment>Restrict `offset` and `limit` to non-negative integers so invalid byte ranges are rejected at the binding layer.</comment>
<file context>
@@ -0,0 +1,234 @@
+ */
+const FsReadInputSchema = z.object({
+ path: z.string().describe("File path to read (e.g., 'docs/readme.md')"),
+ offset: z
+ .number()
+ .optional()
</file context>
| .string() | ||
| .optional() | ||
| .describe("Token for pagination from previous response"), | ||
| maxKeys: z |
There was a problem hiding this comment.
P2: Enforce the documented maxKeys <= 1000 limit in the schema.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/bindings/src/well-known/filesystem.ts, line 119:
<comment>Enforce the documented `maxKeys <= 1000` limit in the schema.</comment>
<file context>
@@ -0,0 +1,234 @@
+ .string()
+ .optional()
+ .describe("Token for pagination from previous response"),
+ maxKeys: z
+ .number()
+ .optional()
</file context>
Adopt main's refactored sect/r naming for config logging while keeping the Object Storage section added on this branch. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The index.ts was never imported — filesystem-mcp.ts imports directly from schema.ts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Both sides added new env vars (S3/Object Storage and AI Gateway) - kept both sections. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Kept @aws-sdk/client-s3 from branch, took @ai-sdk/react version bump from main. Regenerated bun.lock. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
What is this contribution about?
Adds first-class filesystem support to Mesh for AI agents. Implements a complete S3-backed filesystem layer with path sanitization, inline content delivery, and org-level isolation. Agents can now read, write, list, and delete files directly through filesystem tools rather than presigned URLs. Includes agent UI toggle to selectively enable filesystem access per agent.
How to Test
Migration Notes
Database schema unchanged (filesystem is a synthetic well-known connection, like dev-assets and self MCPs). S3 environment variables are optional—when unset, the filesystem connection doesn't exist and the UI toggle is hidden. Single shared bucket model with org-ID prefixes for isolation (immutable, unlike org slug).
Review Checklist
Summary by cubic
Adds an S3-backed filesystem for agents with inline read/write/list/delete/metadata and org-level isolation. It’s opt-in via S3 env vars and can be toggled per agent in the UI; also removes an unused barrel export and updates deps (adds
@aws-sdk/client-s3, bumps@ai-sdk/react, lockfile regenerated).New Features
FILESYSTEM_BINDING(FS_READ, FS_WRITE, FS_LIST, FS_DELETE, FS_METADATA) with an MCP server at/mcp/filesystem,{org}_filesystemIDs, and direct call-tool support.@aws-sdk/client-s3with a 1MB inline read limit; text returns utf-8, binary returns base64; glob filtering; strict path sanitization and org-ID prefixes.Migration
Written for commit cb0b273. Summary will update on new commits.