---
title: "Storage Path Scoping: Multi-Tenant Data Isolation for AI Agents"
description: "Control exactly which subdirectories AI agents can access in your S3, GCS, or Azure buckets. Three levels of path constraints - connection, API key, and per-request - enforce least-privilege access with zero application code."
url: https://baponi.ai/docs/guides/storage-path-scoping
lastUpdated: 2026-03-17
---
# Storage Path Scoping: Multi-Tenant Data Isolation for AI Agents
Baponi is a sandboxed code execution platform for AI agents. When you connect cloud storage (S3, GCS, or Azure) to Baponi, your AI agents read and write files through local directory mounts - no cloud SDK, no credentials in code. Storage path scoping controls exactly which subdirectories each agent can access, enforced server-side with zero trust in the client.

## Why path scoping matters

AI agents operating on shared data need boundaries. Without path scoping, any agent with access to a bucket can read and write anything in it. This creates problems in three common scenarios:

**Multi-tenant SaaS.** You run AI agents on behalf of multiple customers. Each customer's data lives in the same bucket under a per-tenant prefix (`tenants/acme/`, `tenants/widgets/`). An agent processing Acme's data must never see Widgets' data, even accidentally.

**Partner data sharing.** A partner grants you access to their bucket for a joint project, and you agree to only use a specific folder (`shared/your-company/`). You need a way to enforce that agreement at the platform level so no API key or agent can accidentally wander outside that folder.

**Least-privilege agents.** You have one bucket with production data, staging data, and backups. Different agents should see different slices. The data analysis agent gets `analytics/`, the backup agent gets `backups/`, and neither can touch the other's data.

Baponi solves all three with a single mechanism: three levels of path constraints, each narrowing the scope of the level above it.

## Three levels of path constraints

Every storage mount in Baponi passes through three constraint checkpoints. Each level can only **narrow** the path - never widen it. If no constraint is set at a level, it inherits the constraint from the level above.

| Level | Where it's configured | Who configures it | Scope |
|-------|----------------------|-------------------|-------|
| **1. Connection prefix** | Admin console, on the storage connection | Organization admin | All API keys using this connection |
| **2. API key prefix** | Admin console, on each API key | Organization admin | All executions using this key |
| **3. Request sub_paths** | `sub_paths` field in the request body | Developer / AI agent | Single execution |

The principle is simple: the person configuring the connection sets the broadest boundary, the admin narrows it per key, and the developer narrows it per request. No level can escape the constraint set by the level above.

### Default behavior

If no `sub_paths` are passed in a request, the tightest configured constraint becomes the mount scope automatically. If the API key has a prefix, that's used. If not, the connection prefix is used. If neither is set, the entire bucket is accessible (subject to the connection's read/write permissions).

## Level 1: Connection prefix

When you add a storage connection (S3, GCS, or Azure bucket) in the admin console, you can set an optional **sub-path prefix**. This is the broadest boundary - it confines every API key and every request using this connection to a specific subtree of the bucket.

**When to use it:**
- You've agreed with a partner to only access a specific folder in their bucket and want to enforce that at the platform level
- You have a shared bucket and want to guarantee that no API key can ever access certain directories
- Compliance requires that a connection's scope is locked down at the infrastructure level, not left to per-key configuration

**Example:** A partner gives you access to their GCS bucket `partner-analytics` and you've agreed to only use `shared/acme/`. You add the connection in the admin console with:

- **Bucket:** `partner-analytics`
- **Connection name:** `partner-analytics` (becomes the mount slug)
- **Sub-path prefix:** `shared/acme/`

Every API key using this connection now mounts at `/data/partner-analytics/`, but only the `shared/acme/` subtree is accessible. An API key cannot be created with a prefix outside this boundary.

## Level 2: API key prefix

When you create an API key in the admin console, you can set a **sub-path prefix** per storage connection attached to that key. This prefix must start with the connection prefix (if one is set) - it can only go deeper.

**When to use it:**
- Per-tenant isolation: issue different keys for different customers, each confined to their tenant folder
- Per-team isolation: the data science team gets `shared/acme/analytics/`, the engineering team gets `shared/acme/logs/`
- Per-agent least-privilege: the reporting agent gets read-only access to `shared/acme/reports/`, the ingestion agent gets write access to `shared/acme/raw/`

**Example:** Using the `partner-analytics` connection from Level 1 (connection prefix: `shared/acme/`), you create two API keys:

| API Key | Key prefix | Effective mount scope |
|---------|-----------|----------------------|
| `sk-us-team-alpha` | `shared/acme/team-alpha/` | Only `team-alpha/` subtree |
| `sk-us-team-beta` | `shared/acme/team-beta/` | Only `team-beta/` subtree |

An agent using `sk-us-team-alpha` sees `/data/partner-analytics/` as its mount point, but only files under `shared/acme/team-alpha/` are accessible. It cannot access `shared/acme/team-beta/` or anything else in the bucket.

Attempting to create an API key with prefix `other-company/` would be rejected because it does not start with the connection prefix `shared/acme/`.

## Level 3: Request sub_paths

Developers can pass `sub_paths` in the request body to narrow the mount to a specific subdirectory for a single execution. The path must respect both the connection prefix and the API key prefix.

**When to use it:**
- An agent processing a batch job needs access to only one day's data: `shared/acme/team-alpha/2026-03-17/`
- A multi-step workflow where each step operates on a different subdirectory
- Dynamically scoping access based on user input or workflow context

```bash
# Narrow to a specific day's data within team-alpha's scope
curl -X POST https://api.baponi.ai/v1/sandbox/execute \
  -H "Authorization: Bearer sk-us-team-alpha" \
  -H "Content-Type: application/json" \
  -d '{
    "code": "import os; print(os.listdir(\"/data/partner-analytics/\"))",
    "sub_paths": ["/data/partner-analytics/shared/acme/team-alpha/2026-03-17"]
  }'
```

```python
from baponi import Baponi

client = Baponi()

# Narrow to a specific day's data within team-alpha's scope
result = client.execute(
    "import os; print(os.listdir('/data/partner-analytics/'))",
    sub_paths=["/data/partner-analytics/shared/acme/team-alpha/2026-03-17"],
)
print(result.stdout)  # Only files from 2026-03-17
```

The sandbox sees `/data/partner-analytics/` as its mount point, but only the `2026-03-17` subtree is accessible. The agent cannot list or read files from any other date.

If the request tried `/data/partner-analytics/shared/acme/team-beta/2026-03-17`, it would be rejected because `team-beta/` is outside the API key's prefix `shared/acme/team-alpha/`.

`sub_paths` is deliberately excluded from the MCP tool schema. MCP tool calls are typically auto-generated by LLMs, and an LLM can be manipulated by its end user to request paths outside the intended scope, silently breaking multi-tenant isolation. By excluding `sub_paths` from MCP, the path boundary is always set by an admin in the console, not by an LLM at runtime. For MCP use cases, configure the appropriate path constraints on the API key in the admin console.

## The full narrowing chain

Here is the complete constraint chain for a single execution, showing how all three levels combine:

```
Bucket: partner-analytics
  └─ Connection prefix: shared/acme/              (Level 1: set by whoever configures the connection)
       └─ API key prefix: shared/acme/team-alpha/  (Level 2: admin's per-key constraint)
            └─ Request sub_path: shared/acme/team-alpha/2026-03-17/  (Level 3: per-request)
```

At execution time, the gateway validates the request sub_path against both the API key prefix and the connection prefix. If either check fails, the request is rejected with `400 validation_error`. If validation passes, the executor mounts only the requested subtree. The sandbox process sees `/data/partner-analytics/` but can only access files under `shared/acme/team-alpha/2026-03-17/`.

## Security properties

Baponi's path scoping is designed so that no client-side code, misconfiguration at a lower level, or creative path construction can escape the boundaries set by a higher level.

| Property | How it works |
|----------|-------------|
| **Segment-aware matching** | Prefix matching respects `/` boundaries. `tenants/a` matches `tenants/a/subdir` but does NOT match `tenants/ab`. This prevents a key scoped to `tenants/a` from accessing `tenants/abc/`. |
| **Path traversal rejection** | `..`, `./`, URL-encoded sequences (`%2e`, `%2f`, `%5c`), null bytes, and control characters are all rejected at parse time. |
| **Fail-closed validation** | Any validation failure rejects the request. There is no fallback, no silent correction, no partial mount. |
| **Deepen-never-widen** | An API key prefix must start with the connection prefix. A request sub_path must start with the API key prefix. Violations are rejected at key creation time (Level 2) or request time (Level 3). |
| **Server-side enforcement** | All validation happens at the gateway before the execution reaches the sandbox. The sandbox process never sees paths outside its scope. |

## Common patterns

### Multi-tenant SaaS

One bucket, one connection, per-tenant API keys:

| Connection prefix | API key | Key prefix | Mount scope |
|------------------|---------|-----------|-------------|
| (none) | `sk-us-acme` | `tenants/acme/` | Only Acme's data |
| (none) | `sk-us-widgets` | `tenants/widgets/` | Only Widgets' data |

Each tenant's agent uses a different API key. No tenant can access another tenant's folder.

### Partner data sharing

Connection prefix locks down the outer boundary, API key prefixes scope per-team access:

| Connection prefix | API key | Key prefix | Mount scope |
|------------------|---------|-----------|-------------|
| `shared/your-company/` | `sk-us-analytics` | `shared/your-company/analytics/` | Analytics data only |
| `shared/your-company/` | `sk-us-exports` | `shared/your-company/exports/` | Exports only |

The connection-level constraint (`shared/your-company/`) cannot be removed or widened by any API key. To change it, you'd need to reconfigure the connection itself.

### Per-request dynamic scoping

API key gives broad access, individual requests narrow to what's needed:

```python
from baponi import Baponi

client = Baponi(api_key="sk-us-data-pipeline")

# Process each customer's data in isolation
for customer_id in ["cust-001", "cust-002", "cust-003"]:
    result = client.execute(
        f"import os; files = os.listdir('/data/lake/'); print(len(files))",
        sub_paths=[f"/data/lake/customers/{customer_id}/inbox"],
    )
    print(f"{customer_id}: {result.stdout.strip()} files")
```

Each iteration mounts only that customer's inbox. The agent cannot access other customers' data even though the API key allows the entire `customers/` tree.

## Validation rules reference

| Constraint | Limit |
|------------|-------|
| Max `sub_paths` entries per request | 10 |
| Entry format | `/data/{slug}/{path}` where `{slug}` matches a mounted storage connection |
| Entry length | Max 512 characters |
| Connection/key prefix length | Max 512 characters |
| Path traversal | `..`, `./`, URL-encoded sequences, null bytes, and control characters are rejected |
| Prefix matching | Segment-aware: checked at `/` boundaries |
| Duplicate slugs | Only one `sub_paths` entry per storage connection per request |

## FAQ

**What if I don't set any prefix at any level?**
The full bucket is accessible (subject to the connection's read/write permissions). Path scoping is opt-in at each level. Many use cases don't need it - a single-tenant deployment with one agent per bucket works fine without prefixes.

**Can I widen a path at a deeper level?**
No. Each level can only narrow, never widen. An API key prefix must start with the connection prefix. A request sub_path must start with the API key prefix. Attempting to widen returns a validation error.

**Does path scoping work with managed volumes?**
Yes. Managed volumes support the same three-level path scoping as BYOB connections. The mechanism is identical.

**Does this work with MCP?**
`sub_paths` is deliberately excluded from MCP. MCP tool calls are typically auto-generated by LLMs, and exposing path selection to an LLM creates a prompt injection risk: an end user could manipulate the LLM into requesting paths outside the intended scope, silently breaking multi-tenant isolation. By keeping path selection out of the MCP tool schema, the boundary is always set by an admin, not by an LLM at runtime. Configure the path constraints you need on the API key in the admin console.

**What error do I get if a sub_path violates a constraint?**
`400 validation_error` with a message identifying the mount and the prefix that was violated. For example: `"sub_path for '/data/partner-analytics/' does not match prefix 'shared/acme/team-alpha/'"`.

**Is there any performance cost to path scoping?**
No. Path validation is a string prefix check at the gateway. It adds no measurable latency to execution.

**Can the sandbox process discover what prefix is enforced?**
No. The sandbox sees the mount point (e.g., `/data/partner-analytics/`) and the files within its allowed scope. It has no visibility into the prefix configuration or what other paths exist in the bucket.