Skip to content

feat(filesystem): S3-backed filesystem support for agents#2745

Open
viktormarinho wants to merge 5 commits intomainfrom
viktormarinho/object-storage
Open

feat(filesystem): S3-backed filesystem support for agents#2745
viktormarinho wants to merge 5 commits intomainfrom
viktormarinho/object-storage

Conversation

@viktormarinho
Copy link
Contributor

@viktormarinho viktormarinho commented Mar 17, 2026

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

  1. Configure S3: Set S3_ENDPOINT, S3_BUCKET, S3_REGION, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY env vars
  2. Create/edit an agent and verify "Filesystem Access" toggle appears below the Skills section
  3. Toggle ON to enable filesystem tools for that agent
  4. Test agent in chat with file operations (read/write/list/delete)

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

  • PR title is clear and descriptive
  • Changes are tested and working
  • No breaking changes (feature is opt-in via env vars)
  • Path sanitization prevents traversal/null byte attacks
  • Glob filtering works correctly with directories
  • Input validation on direct call-tool endpoints

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

    • Introduces FILESYSTEM_BINDING (FS_READ, FS_WRITE, FS_LIST, FS_DELETE, FS_METADATA) with an MCP server at /mcp/filesystem, {org}_filesystem IDs, and direct call-tool support.
    • Adds S3 integration using @aws-sdk/client-s3 with a 1MB inline read limit; text returns utf-8, binary returns base64; glob filtering; strict path sanitization and org-ID prefixes.
    • Auto-injects a well-known filesystem connection when S3 is configured; adds a “Filesystem Access” toggle per agent.
  • Migration

    • No database changes.
    • Optional S3 env vars: S3_ENDPOINT, S3_BUCKET, S3_REGION (default: auto), S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_FORCE_PATH_STYLE (default: true).
    • If unset, the connection is hidden and the toggle does not appear. Uses a single shared bucket with org-ID prefixes for isolation.

Written for commit cb0b273. Summary will update on new commits.

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>
@github-actions
Copy link
Contributor

🧪 Benchmark

Should we run the Virtual MCP strategy benchmark for this PR?

React with 👍 to run the benchmark.

Reaction Action
👍 Run quick benchmark (10 & 128 tools)

Benchmark will run on the next push after you react.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 17, 2026

Release Options

Should a new version be published when this PR is merged?

React with an emoji to vote on the release type:

Reaction Type Next Version
👍 Prerelease 2.178.2-alpha.1
🎉 Patch 2.178.2
❤️ Minor 2.179.0
🚀 Major 3.0.0

Current version: 2.178.1

Deployment

  • Deploy to production (triggers ArgoCD sync after Docker image is published)

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic

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"),
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic

.replace(/[.+^${}()|[\]\\]/g, "\\$&")
.replace(/\*\*/g, "{{GLOBSTAR}}")
.replace(/\*/g, "[^/]*")
.replace(/{{GLOBSTAR}}/g, ".*")
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic


let body: Buffer;
if (encoding === "base64") {
body = Buffer.from(content, "base64");
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic

new ListObjectsV2Command({
Bucket: this.bucket,
Prefix: prefix,
Delimiter: "/",
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
Delimiter: "/",
Delimiter: input.pattern?.includes("**") ? undefined : "/",
Fix with Cubic

const { handleFilesystemMcpRequest } = await import(
"./routes/filesystem-mcp"
);
return handleFilesystemMcpRequest(c.req.raw, ctx);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
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);
Fix with Cubic

return cachedService;
}

initialized = true;
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic

*/
const FsReadInputSchema = z.object({
path: z.string().describe("File path to read (e.g., 'docs/readme.md')"),
offset: z
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic

.string()
.optional()
.describe("Token for pagination from previous response"),
maxKeys: z
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic

viktormarinho and others added 4 commits March 17, 2026 16:27
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant