# Baponi: Full Documentation > Baponi is a sandboxed code execution platform for AI agents. This file contains the complete text of all Baponi developer documentation pages. > Generated at build time. Source: https://baponi.ai/docs/ --- # Quickstart: First Execution in 60 Seconds URL: https://baponi.ai/docs/getting-started/quickstart/ Baponi is a sandboxed code execution platform for AI agents. Send code, get results. No containers to manage, no infrastructure to provision. This guide gets you from zero to your first execution in under 60 seconds. **What you'll need:** A web browser and a terminal (or Python installed). ## 3 steps to your first execution 1. **Sign up at [console.baponi.ai](https://console.baponi.ai)** Create a free account. No credit card required. You get 1,000 free credits per month, unlimited team seats, and audit logs on every tier. 2. **Create an API key** In the admin console, go to **API Keys** → **Create API Key**. Give it a name and copy the key. It starts with `sk-` and looks like `sk-us-...`. Copy the key now. You won't be able to see it again. Store it somewhere safe. 3. **Run your first code** ```bash curl -X POST https://api.baponi.ai/v1/sandbox/execute \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "code": "print(\"Hello from Baponi!\")", "language": "python" }' ``` ```bash pip install baponi ``` ```python from baponi import Baponi client = Baponi(api_key="YOUR_API_KEY") result = client.execute("print('Hello from Baponi!')") print(result.stdout) # Hello from Baponi! ``` Or set the `BAPONI_API_KEY` environment variable and skip the `api_key` parameter: ```bash export BAPONI_API_KEY="YOUR_API_KEY" ``` ```python from baponi import Baponi client = Baponi() # reads BAPONI_API_KEY from env result = client.execute("print('Hello from Baponi!')") ``` ```python import requests result = requests.post( "https://api.baponi.ai/v1/sandbox/execute", headers={"Authorization": "Bearer YOUR_API_KEY"}, json={"code": "print('Hello from Baponi!')", "language": "python"}, ).json() print(result["stdout"]) # Hello from Baponi! ``` That's it. Your code ran in a fully isolated sandbox with multi-layer security. The sandbox booted in under 20ms, ran your code, and returned the result. ## The response Every execution returns the same structure: ```json { "success": true, "stdout": "Hello from Baponi!\n", "stderr": "", "exit_code": 0 } ``` | Field | Description | |-------|-------------| | `success` | `true` if exit code is 0 | | `stdout` | Standard output from your code | | `stderr` | Standard error from your code | | `exit_code` | Process exit code (0 = success) | ## Try all 3 languages Baponi supports Python 3.14, Node.js 25, and Bash out of the box. ```bash curl -X POST https://api.baponi.ai/v1/sandbox/execute \ -H "Authorization: Bearer $BAPONI_API_KEY" \ -H "Content-Type: application/json" \ -d '{"code": "import sys; print(f\"Python {sys.version}\")", "language": "python"}' ``` ```bash curl -X POST https://api.baponi.ai/v1/sandbox/execute \ -H "Authorization: Bearer $BAPONI_API_KEY" \ -H "Content-Type: application/json" \ -d '{"code": "console.log(`Node.js ${process.version}`)", "language": "node"}' ``` ```bash curl -X POST https://api.baponi.ai/v1/sandbox/execute \ -H "Authorization: Bearer $BAPONI_API_KEY" \ -H "Content-Type: application/json" \ -d '{"code": "echo \"Bash $BASH_VERSION\" && uname -a", "language": "bash"}' ``` Need a custom runtime? Import any OCI-compatible image through the admin console. Baponi auto-discovers available interpreters and system libraries. ## Persist state between calls with thread_id By default, every execution is fully ephemeral. Add `thread_id` to keep the home directory (`/home/baponi`) across calls. Installed packages, working files, and in-progress work all persist: ```bash # Call 1: install cowsay curl -X POST https://api.baponi.ai/v1/sandbox/execute \ -H "Authorization: Bearer $BAPONI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "code": "pip install --user cowsay && echo done", "language": "bash", "thread_id": "quickstart-demo-1" }' # Call 2: cowsay is already installed curl -X POST https://api.baponi.ai/v1/sandbox/execute \ -H "Authorization: Bearer $BAPONI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "code": "import cowsay; cowsay.cow(\"Hello from Baponi!\")", "language": "python", "thread_id": "quickstart-demo-1" }' ``` Between calls, nothing is running. Baponi saves only the diff to cloud storage and restores it on the next call. No idle billing. Pick it up minutes or days later. ## Connect to Claude, Cursor, or any MCP client Baponi is a native [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server. If your tool supports MCP, add Baponi with a URL and API key. No SDK, no tool definitions, no glue code. Add to your `claude_desktop_config.json`: ```json { "mcpServers": { "baponi": { "url": "https://api.baponi.ai/mcp", "headers": { "Authorization": "Bearer YOUR_API_KEY" } } } } ``` ```bash claude mcp add baponi \ --transport http \ --url https://api.baponi.ai/mcp \ --header "Authorization: Bearer YOUR_API_KEY" ``` Add to your Cursor MCP settings (`.cursor/mcp.json`): ```json { "mcpServers": { "baponi": { "url": "https://api.baponi.ai/mcp", "headers": { "Authorization": "Bearer YOUR_API_KEY" } } } } ``` Once configured, the MCP client auto-discovers Baponi's tools (`sandbox_execute`, `files_download`). Ask Claude or Cursor to write and run code, and it calls Baponi automatically. ## Give Claude code execution with 4 lines of Python Using the Python SDK with Anthropic's `tool_runner`: ```python from anthropic import Anthropic from baponi.anthropic import code_sandbox client = Anthropic() response = client.beta.messages.tool_runner( model="claude-sonnet-4-6", tools=[code_sandbox], messages=[{"role": "user", "content": "Calculate the first 20 Fibonacci numbers"}], ).get_final_response() print(response.content[0].text) ``` ```bash pip install baponi[anthropic] ``` Claude writes Python, Baponi runs it in a sandbox, Claude reads the result. The same pattern works with OpenAI, Gemini, LangChain, and CrewAI: ```python from baponi.openai import code_sandbox # OpenAI Agents SDK from baponi.google import code_sandbox # Google Gemini from baponi.langchain import code_sandbox # LangChain from baponi.crewai import code_sandbox # CrewAI ``` ## API parameters at a glance | Parameter | Required | Default | Description | |-----------|----------|---------|-------------| | `code` | Yes | - | Code to execute (1 byte to 1 MB) | | `language` | No | `python` | `python`, `node`, or `bash` | | `timeout` | No | `60` | Max execution time in seconds (1-3600, plan-dependent) | | `thread_id` | No | - | Persist `/home/baponi` across calls | | `metadata` | No | - | Key-value pairs for audit logging (not sent to sandbox) | | `env_vars` | No | - | Environment variables injected into the sandbox | One required parameter. Everything else (the runtime image, CPU, RAM, network policy, storage mounts, injected credentials) is configured once in the admin console and attached to your API key. See the [full Execute API reference](/docs/api/execute) for all parameters, response fields, error codes, and worked examples. ## What to explore next - **[How Baponi Works](/docs/getting-started/how-it-works)**: deep dive into the execution model, BYOB storage, connectors, and the full API surface - **[API Reference](/docs/api/overview)**: full request/response reference for [Execute](/docs/api/execute), [Files](/docs/api/files), and [MCP](/docs/api/mcp) - **[Admin Console](https://console.baponi.ai)**: configure sandboxes, storage connections, connectors, and API keys - **[Pricing](/pricing)**: 1,000 free credits/month, unlimited seats, no credit card required ## FAQ **How many free credits do I get?** 1,000 credits per month on the free tier. One credit = 60 seconds of execution at 1 CPU + 1 GiB RAM. That's roughly 1,000 minutes of compute per month at the base allocation, enough for evaluation and prototyping workloads. **What does it cost after the free tier?** $1.00 per 1,000 additional credits. Or upgrade to Pro ($97/month) for higher resource limits (4 CPU, 4 GiB RAM), 1-hour max timeout, 100 concurrent executions, and 30-day audit retention. **Do I need the Python SDK?** No. The API is a single HTTP endpoint. Any language with an HTTP client works. The SDK adds convenience: automatic retries, typed responses, and ready-made LLM framework integrations. **Is my code executed securely?** Every execution runs in a fresh sandbox with multi-layer isolation. Sandboxes have no access to other sandboxes, the host system, or other customers' data. **Can I self-host Baponi?** Yes. Baponi deploys to any Kubernetes cluster with a single Helm chart. Same platform, same API, same admin console, running on your infrastructure, in your VPC, with your identity provider. --- # How Baponi Works URL: https://baponi.ai/docs/getting-started/how-it-works/ Baponi is a sandboxed code execution platform for AI agents. You send code, it runs in an isolated sandbox with multi-layer security, and you get results back. One HTTP call. Sub-20ms overhead. No container lifecycle to manage. Baponi runs as a cloud service or deploys entirely inside your own infrastructure. Same platform, same Helm chart. For organizations where no data can leave the VPC, Baponi runs on your Kubernetes cluster, with your identity provider, your database, and your monitoring. This guide covers the execution model, every API parameter, how state persistence works, and how to integrate Baponi with your AI agent. ## Your first execution ```bash curl -X POST https://api.baponi.ai/v1/sandbox/execute \ -H "Authorization: Bearer sk-your-api-key" \ -H "Content-Type: application/json" \ -d '{ "code": "print(sum(range(100)))", "language": "python" }' ``` ```python result = requests.post( "https://api.baponi.ai/v1/sandbox/execute", headers={"Authorization": "Bearer sk-your-api-key"}, json={"code": "print(sum(range(100)))", "language": "python"}, ).json() print(result["stdout"]) # "4950\n" ``` ```json { "success": true, "stdout": "4950\n", "stderr": "", "exit_code": 0 } ``` That's a real execution in a fully isolated sandbox. Sub-20ms to set up the isolation. The rest is your code running. ## No container lifecycle to manage ### What you already know If you've built an AI agent, you've probably used your LLM provider's built-in code execution: [OpenAI's `code_interpreter`](https://platform.openai.com/docs/assistants/tools/code-interpreter), [Anthropic's `code_execution`](https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/code-execution), and [Google's `code_execution`](https://ai.google.dev/gemini-api/docs/code-execution). They're great for getting started: ```python # Most LLM providers make code execution a one-liner response = client.messages.create( model="claude-sonnet-4-6", max_tokens=4096, tools=[{"type": "code_execution_20250825", "name": "code_execution"}], messages=[{ "role": "user", "content": "Plot a sine wave and save it as plot.png", }], ) ``` Simple. The LLM writes code, runs it, returns the result. For a quick calculation, this is perfect. But the moment your agent needs to do real work, you hit walls: - **No file access.** Your data lives in S3 or GCS. The sandbox can't see it. You end up base64-encoding CSVs into the prompt or building upload/download plumbing around every call. - **No internet access.** These sandboxes are fully locked down. Your agent wants to `pip install` a package, call an API, or fetch a URL? Can't do it. With Baponi, you choose per sandbox: `blocked` (default) or `unrestricted` (full outbound internet). - **No control over the runtime or resources.** You get whatever Python version, packages, CPU, and memory the provider decided on. Need pandas 2.2, a custom ML model, Node.js, a longer timeout, or more compute? Can't change any of it. - **No access to your infrastructure.** The sandbox can't reach your databases, cloud services, or internal APIs, and you'd never want to paste credentials into LLM-generated code anyway. With Baponi, connectors inject credentials securely at runtime. Your agent just runs `psql` or `bq query`, no passwords in code. - **Container lifecycle headaches.** The provider's sandbox stays alive for a few minutes after your call, but you never really know how long. Is it still running? Can you resume it? If it timed out, did you lose the files in it? You end up writing defensive code to pull results out before the container dies, or re-uploading everything to start over. With Baponi, there's no container to keep alive. Every call gets a fresh sandbox in under 20ms, state is snapshotted automatically, and nothing runs between calls. No lifecycle to manage, no idle billing, no state to lose. ### What the market offers So you look at third-party sandbox providers. Now you get control, but the simplicity is gone and the bill starts climbing: ```python # Third-party sandbox SDKs: control, but you manage the lifecycle from sandbox_provider import Sandbox sandbox = Sandbox(image="python:3.14") # boot a container (200-500ms) sandbox.upload("data.csv", "/home/data.csv") # upload files one by one sandbox.run("pip install pandas") # install packages result = sandbox.run("import pandas; ...") # finally, actual work output = sandbox.download("/home/output.csv") # download results sandbox.close() # don't forget this ``` You're now managing container lifecycle. What happens when your agent makes 50 tool calls in a conversation? What happens when the sandbox crashes mid-session? When you forget to close it? When you need to handle files going in and out of every execution? Your agent code is suddenly more about sandbox management than about what the agent actually does. ### What we built instead We asked a simple question: why should you manage containers at all? With Baponi, your API key already knows everything: what image to use, how much CPU and RAM to allocate, what storage to mount, what credentials to inject, what network policy to apply. You set that up once in the [admin console](https://console.baponi.ai). After that, your code just calls `execute`. There's no sandbox object to create in code. No container setup. No state to track. No lifecycle to manage. When you call the API, Baponi spins up a fresh isolated environment, runs your code, and tears it down. This works because our isolation boots in under 20ms, fast enough that there's no reason to keep anything alive between calls. Here's how you give Claude code execution with our Python SDK: ```python from anthropic import Anthropic from baponi.anthropic import code_sandbox client = Anthropic() response = client.beta.messages.tool_runner( model="claude-sonnet-4-6", tools=[code_sandbox], messages=[{ "role": "user", "content": "Analyze the PDF I uploaded and give me a summary." }], ).get_final_response() print(response.content[0].text) ``` That's the entire integration. Claude writes code, Baponi runs it, Claude analyzes the result, all handled automatically. Same pattern for every major framework: ```python from baponi.openai import code_sandbox # OpenAI Agents SDK from baponi.google import code_sandbox # Google Gemini from baponi.langchain import code_sandbox # LangChain from baponi.crewai import code_sandbox # CrewAI ``` No container setup. No files to upload. No state to track. No cleanup. ### Your data is already there This is where the approach really pays off. With other sandbox providers, getting data in and out means upload APIs, download APIs, temporary URLs, and file-transfer code around every call. With Baponi, your data is already mounted: ```python # Your GCS bucket is mounted at /data/my-bucket/ # LLM-generated code just uses it: df = pd.read_csv("/data/my-bucket/customers.csv") result = df.groupby("region").agg({"revenue": "sum"}) result.to_csv("/data/my-bucket/reports/summary.csv") ``` **Bring Your Own Bucket (BYOB):** Connect your S3, GCS, or Azure bucket in the admin console. It mounts into every sandbox automatically. Your code reads and writes with normal file I/O. No upload API, no download API, no temporary URLs. **Managed volumes:** Baponi-hosted storage that works exactly like BYOB, no cloud bucket setup required. First 10 GB free, expandable as you grow. Your agent writes files during execution, you upload via the API's signed-URL endpoints, and everything is available in your sandboxes. No file-transfer code. No base64 encoding in prompts. The LLM writes `open("/data/bucket/file.csv")` and it works. ### Statefulness without lifecycle management Need the sandbox environment to persist between calls (installed packages, working files, in-progress computations)? Add one parameter: ```python from baponi import Baponi client = Baponi(api_key="sk-...") # Generate a unique thread_id per session: descriptive prefix + random suffix thread_id = "data-analysis-x8k2m9p4z1" # Call 1: install packages and start work result = client.execute( "pip install --user pandas scikit-learn && echo done", language="bash", thread_id=thread_id, ) # Call 2: everything from call 1 is still there result = client.execute( "import pandas, sklearn; print(pandas.__version__)", language="python", thread_id=thread_id, ) ``` ```bash # Call 1: install packages curl -X POST https://api.baponi.ai/v1/sandbox/execute \ -H "Authorization: Bearer sk-your-api-key" \ -H "Content-Type: application/json" \ -d '{ "code": "pip install --user pandas scikit-learn && echo done", "language": "bash", "thread_id": "data-analysis-x8k2m9p4z1" }' # Call 2: everything from call 1 is still there curl -X POST https://api.baponi.ai/v1/sandbox/execute \ -H "Authorization: Bearer sk-your-api-key" \ -H "Content-Type: application/json" \ -d '{ "code": "import pandas, sklearn; print(pandas.__version__)", "language": "python", "thread_id": "data-analysis-x8k2m9p4z1" }' ``` Each `thread_id` should be unique. We recommend a descriptive prefix plus a random suffix (like `data-analysis-x8k2m9p4z1`) so it's easy to identify in logs. Baponi saves only the diff of the home directory to cloud storage after each call and restores it on the next. Between calls, nothing is running. It's a saved state, not a running process. Diffs are typically small (installed packages, working files), so thread snapshots barely cost anything. Pick it up five minutes later or five days later, same result. You only pay per execution, not for idle time. Other sandbox providers bill every second the sandbox is alive, even while your LLM is thinking or your user is away. With Baponi, the sandbox runs your code, saves the state, and shuts down. Pick it up minutes or days later. No idle billing. No timeout surprises. No lost state. And here's the key insight: **most use cases don't even need `thread_id`**. If your data lives in mounted storage (`/data/*`), sandboxes are fully ephemeral while your data is always persistent. Mount your bucket, keep sandboxes stateless. You get the simplicity of ephemeral execution with the durability of cloud storage. ### The configuration chain All of this (the image, resources, storage mounts, credentials, network policy) is configured once in the admin console and attached to a sandbox. Your API key points to that sandbox. When your code calls `execute`, it inherits everything automatically. The developer writing the agent never thinks about infrastructure. The admin who set up the sandbox never thinks about agent logic. Clean separation. For self-hosted deployments, the same admin console runs inside your cluster. Same configuration, same API, same developer experience, just deployed on infrastructure you control. ## The execution request | Parameter | Required | Default | Description | |-----------|----------|---------|-------------| | `code` | Yes | - | The code to execute. 1 byte to 1 MB. | | `language` | No | `python` | `python`, `node`, or `bash`. Must be supported by the sandbox's image. | | `timeout` | No | `60` | Maximum execution time in seconds (1–3600, plan-dependent). | | `thread_id` | No | - | Enables persistent `/home` directory across calls. Alphanumeric, hyphens, underscores, max 128 chars. See [stateful execution](#ephemeral-by-default-stateful-when-you-need-it). | | `metadata` | No | - | Key-value pairs for audit logging. Up to 10 keys. **Not sent to the sandbox**, purely for your records. | Five parameters. One required. See the [Execute API reference](/docs/api/execute) for the full request/response specification, error codes, and worked examples. Everything else (the runtime image, CPU, RAM, network policy, storage mounts, injected credentials, environment variables) lives in the sandbox configuration behind your API key. Your execute call doesn't need to know about any of it. ### Why these parameters and nothing else We deliberately kept the request surface minimal. The reasoning: - **`code` + `language`** are obviously required to run anything. - **`timeout`** belongs on the request because different code snippets need different time budgets. A quick calculation needs 5 seconds; a data pipeline might need 10 minutes. The caller knows this at invocation time; the sandbox admin doesn't. - **`thread_id`** is per-conversation state, not per-sandbox config. The AI agent decides whether this execution belongs to an ongoing session. It would be wrong to configure this on the sandbox. - **`metadata`** is caller context (which user triggered this, which model, which session). It's audit data that the sandbox has no business seeing. Everything else (image, CPU, RAM, network, storage, credentials) is an infrastructure decision. It doesn't change between requests and shouldn't be in the hot path. Configure it once, forget about it. ### The response | Field | Description | |-------|-------------| | `success` | `true` if exit code is 0. | | `stdout` | Captured standard output. | | `stderr` | Captured standard error. | | `exit_code` | Process exit code (0 = success). | ## Ephemeral by default, stateful when you need it Every execution has three filesystem zones with different persistence rules: | Path | Without `thread_id` | With `thread_id` | |------|---------------------|-------------------| | `/tmp` | Ephemeral (RAM disk) | Ephemeral (RAM disk) | | `/home/baponi` | Ephemeral (RAM disk) | **Persistent**, synced to cloud storage | | `/data/*` | **Always persistent** (cloud storage) | **Always persistent** (cloud storage) | ### With `thread_id`: persistent home directory Use `thread_id` when you need the sandbox *environment* to survive between calls: installed packages, downloaded files, in-progress work in the home directory. ```bash # Call 1: install pandas curl -X POST .../v1/sandbox/execute \ -d '{"code": "pip install --user pandas && echo done", "language": "bash", "thread_id": "session-1"}' # Call 2: pandas is already there curl -X POST .../v1/sandbox/execute \ -d '{"code": "import pandas; print(pandas.__version__)", "language": "python", "thread_id": "session-1"}' ``` Between these calls, nothing is running. Baponi saves only the diff of `/home/baponi`, not the entire image, to cloud storage after the first call and restores it before the second. The thread isn't a live process sitting idle. It's a saved filesystem state. Diffs are typically small (installed packages, config files, working data), so thread snapshots add very little to your storage usage. Only one execution can use a `thread_id` at a time. A second request with the same `thread_id` will fail if one is already running. Different thread IDs execute fully in parallel. How long does thread state last? That's configured on the sandbox when you create it in the admin console. Options: **24 hours**, **7 days**, **30 days**, or **forever** (the default). Thread snapshots count toward your storage billing. First 10 GB is free. ### Without `thread_id`: fully ephemeral If your data lives in mounted storage (`/data/*`), you often don't need `thread_id` at all: ```python # No thread_id needed - /data/my-bucket/ is always there data = json.load(open("/data/my-bucket/input.json")) result = process(data) json.dump(result, open("/data/my-bucket/output/result.json", "w")) ``` The sandbox is fully ephemeral. Your data lives in the bucket. Baponi mounts it fresh on every call and syncs writes back before returning. Mount your data via BYOB or managed volumes. Keep sandboxes ephemeral. Use `thread_id` only when you need installed packages or working files to persist between calls. ### Metadata: audit trail, not execution context The `metadata` field accepts up to 10 key-value pairs: ```json { "metadata": { "user_id": "user_abc", "session_id": "chat_xyz", "model": "claude-sonnet-4" } } ``` These are stored in your execution logs for auditing and filtering. They are **not** sent to the sandbox. Your code never sees them. Use them to trace which user, session, or model triggered each execution. ## What your API key controls Your API key points to one sandbox configuration. That configuration determines the full execution environment: | Setting | Where it's configured | What it does | |---------|----------------------|--------------| | **Runtime image** | Sandbox | Python 3.14, Node.js 25, Bash, or your own OCI image with custom runtimes and packages. | | **CPU** | Sandbox | 0.5–4 cores. RAM scales 1:1 (2 CPU = 2 GiB RAM). | | **Max timeout** | Plan tier | 60s (Free), 3600s (Pro), unlimited (Enterprise). | | **Network policy** | Sandbox + API key | `blocked` (default) or `unrestricted`. API key can restrict further, never relax. | | **Storage connections** | Admin console | Which S3/GCS/Azure buckets mount where, with per-key path scoping. | | **Connectors** | Admin console | Which database, cloud, or API credentials to inject. | | **Environment variables** | Sandbox, API key, or request | Custom key-value pairs injected into the sandbox (e.g., `API_BASE_URL`, `LOG_LEVEL`). Set at sandbox level (applies to all keys), API key level (applies to all requests with that key), or per-request in the `env_vars` body field. Most specific scope wins when the same key appears at multiple levels. | All set-and-forget. Configure once, use the API key forever. Per-request `env_vars` are the exception - they let callers pass execution-specific configuration without changing the admin console setup. The API key can **narrow** what the sandbox allows (restrict storage to a sub-path, override network to `blocked`, limit connector access) but can never **broaden** it. This lets you issue scoped keys for different agents or tenants from a single sandbox configuration. ## Storage: your data stays in your bucket Baponi gives you two ways to mount persistent data into sandboxes. ### Managed volumes Don't want to manage your own cloud bucket? Baponi gives every organization storage that works exactly like BYOB, with the first 10 GB free and expandable as you grow. Same mount behavior, same file I/O, same persistence. Files get there the same way they get anywhere: your agent writes them during execution, or you upload via the [Files API](/docs/api/files) signed-URL endpoints. The admin console lets you browse and manage what's stored. Your data is private to your organization. ### BYOB: Bring Your Own Bucket Connect your own S3, GCS, or Azure Blob Storage bucket in the admin console. It mounts into every sandbox automatically, so your code uses normal file operations: ```python # GCS bucket "company-data" mounted at /data/company-data/ df = pd.read_csv("/data/company-data/customers/q1-2026.csv") summary = df.groupby("region").agg({"revenue": "sum"}) summary.to_csv("/data/company-data/reports/q1-summary.csv") ``` No cloud SDK in your code. No credentials in your code. Just file paths. Writes sync back to your bucket automatically before the execution response returns. ### Multi-tenant path scoping Baponi enforces path constraints at three levels. Each level can only **narrow** the scope - never widen it: 1. **Connection prefix** - When you add a bucket in the admin console, you can set an optional prefix that constrains all access to a specific subtree. This is the broadest boundary. Use it to lock down a connection to a specific folder - for example, when you've agreed with a partner to only access a specific path in their bucket, or when you want to guarantee no API key can wander outside a designated area. 2. **API key prefix** - When you create an API key, you can set a deeper prefix per storage connection. It must start with the connection prefix. Use it for per-agent or per-tenant isolation. 3. **Request sub_paths** - Callers can pass `sub_paths` in the request body to narrow the mount to a specific subdirectory for a single execution. It must respect both the connection and API key prefixes. Example: a partner shares their bucket `partner-data` restricted to `shared/acme/`. You issue per-tenant API keys: | Level | Prefix | What the sandbox sees at `/data/partner-data/` | |-------|--------|------------------------------------------------| | Connection | `shared/acme/` | All keys confined to `shared/acme/` | | API key for Tenant A | `shared/acme/tenant-a/` | Only Tenant A's data | | API key for Tenant B | `shared/acme/tenant-b/` | Only Tenant B's data | Path scoping is enforced server-side. The AI agent can't break out of its assigned prefix. It doesn't even know other prefixes exist. Prefix matching is segment-aware (`tenants/a` matches `tenants/a/subdir` but not `tenants/ab`), and path traversal attempts (`..`, URL-encoded sequences) are rejected. For the full constraint model, worked examples, and security properties, see the [Storage Path Scoping guide](/docs/guides/storage-path-scoping). BYOB storage is always free and unlimited. You pay your cloud provider for the bucket; Baponi doesn't charge for the mount. ## Connectors: credentials your code never sees Connectors solve a common problem: how do you let an AI agent query your database or access cloud services without exposing credentials in code? Configure connectors in the admin console: PostgreSQL, MySQL, BigQuery, Redis, MongoDB, S3, GCS, Azure, GitHub (9 types today). Attach them to an API key. Baponi injects the credentials as standard config files that CLI tools expect: | Connector | What gets injected | What your code runs | |-----------|-------------------|---------------------| | PostgreSQL | `.pgpass` + `PGPASSFILE` | `psql -h host -d db -c "SELECT ..."` | | AWS S3 | `.aws/credentials` + `AWS_PROFILE` | `aws s3 cp s3://bucket/file .` | | BigQuery | ADC JSON + `GOOGLE_APPLICATION_CREDENTIALS` | `bq query "SELECT ..."` | | GitHub | `GH_TOKEN` | `gh repo clone org/repo` | | Redis | `REDIS_HOST` + `REDISCLI_AUTH` | `redis-cli GET key` | Your code uses standard tools and standard environment variables. No Baponi-specific SDK or wrapper. Credentials exist only in a RAM-based mount during execution. They're never written to the sandbox filesystem, never visible in environment variable dumps, and destroyed the moment execution completes. Even with `thread_id`, credentials are injected fresh on every call and never persisted in thread state. Self-hosted deployments can leverage your existing Workload Identity and IAM, no service account keys to manage at all. ## Network access **Blocked** (default): No network interface at all. No DNS, no outbound connections. The sandbox processes data from storage mounts and returns results. This is the right choice for most AI agent workloads. **Unrestricted**: Full outbound internet access. Internal networks (private IPs, cloud metadata endpoints) are always blocked at the network layer. Bandwidth scales with CPU allocation: 200 Mbps per core. The network policy is set on the sandbox configuration. An API key can downgrade `unrestricted` to `blocked`, but never the reverse, so you can issue a "read-only analysis" key from a sandbox that normally has network access. ## Using Baponi as a tool for LLMs ### With the Python SDK The SDK has ready-made integrations for every major LLM framework. Here's a complete example with Anthropic using `tool_runner`. Claude writes code, Baponi runs it, Claude analyzes the result, all in one call: ```python from anthropic import Anthropic from baponi.anthropic import code_sandbox client = Anthropic() response = client.beta.messages.tool_runner( model="claude-sonnet-4-6", tools=[code_sandbox], messages=[{ "role": "user", "content": "Analyze the PDF I uploaded and give me a summary." }], ).get_final_response() print(response.content[0].text) ``` Same pattern for OpenAI Agents SDK, Google Gemini, LangChain, and CrewAI. Each has a `code_sandbox` tool you import and plug in: ```python from baponi.openai import code_sandbox # Agent(tools=[code_sandbox]) from baponi.google import code_sandbox # config={"tools": [code_sandbox]} from baponi.langchain import code_sandbox # create_react_agent(llm, tools=[code_sandbox]) from baponi.crewai import code_sandbox # Agent(role="coder", tools=[code_sandbox]) ``` For custom configuration (API key, default thread_id, base URL for self-hosted), each module has a `create_code_sandbox()` factory. ### With plain HTTP No SDK needed. The API is a single endpoint. Any HTTP client works: ```python BAPONI_URL = "https://api.baponi.ai/v1/sandbox/execute" BAPONI_KEY = "sk-..." # Define the tool yourself tool = { "name": "run_code", "description": "Run Python, Node.js, or Bash code in a sandboxed environment with access to data files at /data/.", "input_schema": { "type": "object", "properties": { "code": {"type": "string", "description": "Code to execute"}, "language": {"type": "string", "enum": ["python", "node", "bash"], "default": "python"}, }, "required": ["code"], }, } # When the LLM calls the tool: result = requests.post( BAPONI_URL, headers={"Authorization": f"Bearer {BAPONI_KEY}"}, json={"code": tool_call.input["code"], "language": tool_call.input.get("language", "python")}, ).json() # result["stdout"], result["stderr"], result["success"] ``` ### MCP: zero-code integration Baponi is a native [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server. If your tool supports MCP (Claude Desktop, Claude Code, Cursor, Windsurf, or any MCP-compatible agent framework), add Baponi with a server URL and an API key. The client discovers available tools automatically (`sandbox_execute`, `files_download`) and the LLM calls them directly. No tool definitions. No forwarding code. No SDK. The LLM talks to Baponi directly. See the [MCP Protocol reference](/docs/api/mcp) for client configuration, transport details, and all available methods. ## FAQ **What languages does Baponi support?** Python 3.14, Node.js 25, and Bash out of the box. Import your own OCI image through the admin console for custom runtimes. **How is this different from running code in Docker?** Docker gives you containers you manage: pull images, start, stop, clean up. Baponi gives you an API endpoint. You send code, get results. No images to pull, no lifecycle to track. The isolation is deeper than Docker's defaults (multi-layer sandboxing, seccomp syscall filtering, cgroups resource limits, zero capabilities) with less operational work on your end. **What happens if two requests use the same `thread_id` at once?** The second request fails. Only one execution can use a given `thread_id` at a time to prevent filesystem conflicts. Different `thread_id` values execute fully in parallel. **How long is `thread_id` state retained?** Retention is configured on the sandbox when you create it in the admin console. Options: 24 hours, 7 days, 30 days, or forever (the default). Only the diff is stored, not the full image, so thread snapshots are typically very small. They count toward your storage billing (first 10 GB free). **Can sandboxes access the internet?** Only when configured with `unrestricted` network policy. The default is `blocked`, no outbound connections at all. **What's the maximum execution time?** 60 seconds on Free, 1 hour on Pro, configurable on Enterprise. See [pricing](/pricing) for full tier details. **Can I use my own Docker image?** Yes. Import any [OCI-compatible](https://github.com/opencontainers/image-spec) image through the admin console. Baponi auto-discovers available interpreters, system libraries, and architecture. **Are credentials safe?** Credentials exist only in a RAM-based mount during execution. They're never written to disk, never persisted in thread state, and destroyed when execution completes. Only allowlisted environment variables (like `PGPASSFILE` or `AWS_PROFILE`) are injected. No raw passwords in the environment. **What's the overhead per execution?** Typically 12–18ms for sandbox setup. This is the time to create the isolated environment and set up mounts. Everything after that is your code running. **Can I run Baponi in my own infrastructure?** Yes. Baponi deploys to any Kubernetes cluster with a single Helm chart. Your cloud, your VPC, your rules. Bring your own SSO/OIDC provider, your own PostgreSQL, your own monitoring stack. The same platform, same API, same admin console, just running on infrastructure you control. We also support air-gapped deployments for the most restricted environments. **Is there a Python or Node.js SDK?** The Python SDK (`pip install baponi`) provides `execute()` for inline results, `execute_stream()` for real-time NDJSON streaming, and `execute_webhook()` for async webhook delivery. It also includes ready-made tool integrations for Anthropic, OpenAI, Google Gemini, LangChain, and CrewAI. The API is also a single HTTP endpoint, any HTTP client works without the SDK. For MCP-compatible tools, integration is zero-code. --- # LangChain Deep Agents URL: https://baponi.ai/docs/guides/deep-agents/ Give your LangChain Deep Agent a full sandbox environment with file operations, code execution, and persistent state in 3 lines of code. ## Get started 1. **Install** ```bash pip install baponi[deepagents] ``` 2. **Set your API keys** ```bash export BAPONI_API_KEY=sk-your-key-here export ANTHROPIC_API_KEY=your-anthropic-key-here ``` Get a Baponi key from [console.baponi.ai](https://console.baponi.ai). 1,000 free credits/month, no credit card. 3. **Create and run your agent** ```python from baponi.deepagents import BaponiSandbox from deepagents import create_deep_agent with BaponiSandbox() as sandbox: agent = create_deep_agent("anthropic:claude-sonnet-4-6", backend=sandbox) result = agent.invoke({"messages": [("human", "Use Python to calculate the SHA-256 hash of 'hello world'")]}) print(result["messages"][-1].content) ``` The agent writes a Python script, executes it inside an isolated sandbox, and returns the result. Files, installed packages, and environment changes persist across calls. ## What your agent can do Your agent gets these tools automatically: | Tool | Example | |------|---------| | **execute** | Run `pip install pandas`, compile code, start servers | | **read_file** | Inspect source files, configs, logs | | **write_file** | Generate code, create configs, save results | | **edit_file** | Refactor code, fix bugs, update settings | | **ls** | Explore project structure | | **grep** | Find function definitions, error patterns, TODOs | | **glob** | Find all `*.py` files, match naming patterns | ## Persistent sessions Files and installed packages persist automatically across calls within the same sandbox. Your agent can install dependencies, write code, then test it, all building on previous steps. To resume a session from a separate process (e.g., a new API request or a cron job), pass the same `thread_id`: ```python # Process A with BaponiSandbox(thread_id="my-project") as sandbox: agent = create_deep_agent("anthropic:claude-sonnet-4-6", backend=sandbox) agent.invoke({"messages": [("human", "Install pandas and write a CSV parser")]}) # Process B - picks up right where A left off with BaponiSandbox(thread_id="my-project") as sandbox: agent = create_deep_agent("anthropic:claude-sonnet-4-6", backend=sandbox) agent.invoke({"messages": [("human", "Add error handling to the CSV parser you wrote")]}) ``` ## Configuration ```python sandbox = BaponiSandbox( thread_id="data-pipeline-run-42", # session ID for persistent state timeout=60, # default 60s, Pro plan supports up to 3600 metadata={"user": "agent-1"}, # attached to every execution for audit env_vars={"ENV": "prod"}, # environment variables injected into every execution sub_paths=["/data/shared"], # storage path scoping for BYOB mounts ) ``` For self-hosted Baponi deployments: ```python sandbox = BaponiSandbox( base_url="https://baponi.internal.company.com", api_key="sk-your-enterprise-key", ) ``` ```python # Before agent = create_deep_agent(model, backend=OtherSandbox()) # After from baponi.deepagents import BaponiSandbox agent = create_deep_agent(model, backend=BaponiSandbox()) ``` ## Per-call overrides Override constructor defaults on individual `execute()` or `aexecute()` calls. Pass `None` (the default) to use the constructor value. Pass an explicit value to replace the constructor default for that call only - values are replaced, not merged. ```python sandbox = BaponiSandbox( env_vars={"ENV": "prod"}, metadata={"team": "data"}, sub_paths=["/data/shared"], ) # Override env_vars for this call only result = sandbox.execute("echo $ENV", env_vars={"ENV": "staging"}) # Override metadata for this call only result = sandbox.execute("cmd", metadata={"step": "cleanup"}) # Narrow storage scope to a specific subdirectory for this call only result = sandbox.execute("ls /data/shared", sub_paths=["/data/shared/2026-03-22"]) # Override timeout for this call only result = sandbox.execute("long-running-task", timeout=300) ``` All four parameters (`timeout`, `env_vars`, `metadata`, `sub_paths`) are available as per-call overrides on `execute()`, `aexecute()`, and `execute_stream()`. ## Context managers `BaponiSandbox` supports `with` and `async with` to ensure HTTP connections are properly closed: ```python # Sync with BaponiSandbox(api_key="sk-...", thread_id="session-1") as sandbox: result = sandbox.execute("echo hello") # connections closed automatically # Async async with BaponiSandbox(api_key="sk-...") as sandbox: result = await sandbox.aexecute("echo hello") ``` You can also call `sandbox.close()` or `await sandbox.aclose()` manually. Both are idempotent. ## Streaming `execute_stream()` provides real-time NDJSON streaming for long-running commands. Events arrive as they happen rather than waiting for the full result. ```python stream = sandbox.execute_stream("pip install -r requirements.txt && python train.py") with stream: for event in stream: print(event) result = stream.get_final_result() ``` `execute_stream()` accepts the same per-call overrides as `execute()`: ```python stream = sandbox.execute_stream( "python train.py", timeout=600, env_vars={"CUDA_VISIBLE_DEVICES": "0"}, ) ``` Streaming requires the Pro plan or higher. See [pricing](/pricing) for details. ## Sandbox provider `BaponiSandboxProvider` manages multiple sandbox instances by ID. Use it when your application needs to create, reuse, and clean up sandboxes programmatically - for example, one sandbox per user session in a server. ```python from baponi.deepagents import BaponiSandboxProvider provider = BaponiSandboxProvider(api_key="sk-...") # Create or reuse sandbox by ID sandbox = provider.get_or_create(sandbox_id="user-123") result = sandbox.execute("echo hello") # Different user gets a different sandbox sandbox_b = provider.get_or_create(sandbox_id="user-456") # Clean up when done provider.delete("user-123") ``` The provider also has async variants: `await provider.aget_or_create()` and `await provider.adelete()`. Constructor arguments (`timeout`, `metadata`, `env_vars`, `sub_paths`) are passed through to every sandbox the provider creates. Configuration falls back to `BAPONI_API_KEY` and `BAPONI_BASE_URL` environment variables when not provided explicitly. ## FAQ ### Do I need to manage sandbox lifecycles? For most use cases, no. Create a `BaponiSandbox` and use it - Baponi handles provisioning, isolation, and cleanup. If you need to manage multiple sandboxes programmatically (e.g., one per user in a multi-tenant server), use `BaponiSandboxProvider`. ### What languages can my agent run? Python, Node.js, and Bash are available in the default sandbox image. ### Does it support async and streaming? Yes. `BaponiSandbox` works with both sync (`execute`) and async (`aexecute`) out of the box. For real-time output, use `execute_stream()` to get NDJSON events as they arrive (Pro plan required). ### Can I override settings per execution? Yes. `execute()`, `aexecute()`, and `execute_stream()` accept `timeout`, `env_vars`, `metadata`, and `sub_paths` as keyword arguments. Passing a value overrides the constructor default for that call only. --- # Remote Claude Code Sessions URL: https://baponi.ai/docs/guides/claude-code/ Run Claude Code as a remote headless agent inside an isolated Baponi sandbox. Credentials for Anthropic and GitHub are injected automatically through [connectors](/features#connectors). Your code never handles API keys or tokens directly. ## Prerequisites 1. **Baponi account and API key** Sign up at [console.baponi.ai](https://console.baponi.ai) and create an API key. 1,000 free credits/month, no credit card. 2. **Anthropic connector** In the Baponi admin UI, go to **Connectors → Add → Anthropic**. Paste your Anthropic API key. Attach the connector to the API key you'll use for executions. This injects `ANTHROPIC_API_KEY` as an environment variable into every sandbox execution, and Claude Code picks it up automatically. 3. **GitHub connector** (optional, for private repos and pushing) Go to **Connectors → Add → GitHub**. Paste a [fine-grained personal access token](https://github.com/settings/tokens?type=beta) with **Contents: Read and write** permission on the repos you want the agent to access. Attach it to the same API key. This injects a `.gitconfig` that transparently rewrites `https://github.com/` URLs to include your token, plus a `GH_TOKEN` environment variable for the `gh` CLI. `git clone`, `git push`, and `gh` commands work on private repos without any token handling in code. See [Connectors](/features#connectors) for the full list of supported integrations (databases, cloud storage, developer tools). 4. **Install the Python SDK** (optional, for the Python examples) ```bash pip install baponi ``` ## Quick start: one-shot agent task Run a single Claude Code task inside a sandbox: ```bash curl -X POST https://api.baponi.ai/v1/sandbox/execute \ -H "Authorization: Bearer $BAPONI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "code": "claude -p \"Research the current price of Bitcoin and summarize your findings\" --dangerously-skip-permissions", "language": "bash", "timeout": 300 }' ``` ```python from baponi import Baponi client = Baponi() result = client.execute( code='claude -p "Research the current price of Bitcoin and summarize your findings" --dangerously-skip-permissions', language="bash", timeout=300, ) print(result.stdout) ``` Claude Code runs inside the sandbox with full system access: it can make HTTP requests, install packages, read and write files, and run code. ## Autonomous GitHub workflow With both the Anthropic and GitHub connectors attached, Claude Code can autonomously discover repositories, make changes, and push, all within a single sandbox execution. ```python from baponi import Baponi client = Baponi() result = client.execute( code='''claude -p "List the GitHub repos available to you. Pick the least important one you can write to. Add a '## Fun Fact' section to its README with a funny sandbox coding joke. Commit and push." \ --dangerously-skip-permissions \ --max-turns 20''', language="bash", timeout=600, thread_id="github-agent-session", ) print(result.stdout) ``` Claude Code handles the entire workflow: it calls `gh repo list` to discover available repositories, evaluates which one to target, clones it, reads the README, makes the edit, commits, and pushes, all with credentials injected transparently through [connectors](/features#connectors). ## Multi-step workflow: clone, agent, push For more control, split the workflow into persistent steps using `thread_id`. Each call builds on the previous one. Cloned repos, installed packages, and generated files all persist. ```python from baponi import Baponi client = Baponi() thread = "feature-auth-refactor" # Step 1: Clone the repo (uses GitHub connector credentials automatically) clone = client.execute( code="git clone https://github.com/your-org/your-repo.git /home/baponi/repo", language="bash", timeout=60, thread_id=thread, ) assert clone.exit_code == 0, f"Clone failed: {clone.stderr}" # Step 2: Run Claude Code on the codebase agent = client.execute( code='''cd /home/baponi/repo && claude -p "Refactor the auth module to use async/await. Update all callers. Run the test suite and fix any failures." \ --dangerously-skip-permissions \ --output-format stream-json \ --max-turns 50''', language="bash", timeout=3600, # 1 hour for complex tasks thread_id=thread, ) print(agent.stdout) # Step 3: Push the changes push = client.execute( code='''cd /home/baponi/repo \ && git add -A \ && git commit -m "refactor: migrate auth module to async/await" \ && git push origin HEAD''', language="bash", timeout=60, thread_id=thread, ) assert push.exit_code == 0, f"Push failed: {push.stderr}" ``` Each step uses the same `thread_id`, so the cloned repo from step 1 is still on disk when step 2 runs Claude Code, and the changes from step 2 are still there when step 3 pushes. ## Claude Code flags reference Key flags for headless sandbox execution: | Flag | Purpose | |------|---------| | `-p "prompt"` | Non-interactive mode. Runs the prompt and exits. **Required in sandboxes.** | | `--dangerously-skip-permissions` | Auto-approve all tool calls. **Required in sandboxes** (no TTY for prompts). | | `--output-format stream-json` | Structured JSON output for parsing agent actions programmatically. | | `--max-turns 50` | Safety rail: stop after N tool-use turns to prevent runaway loops. | | `--max-budget-usd 10.00` | Cost ceiling for the Anthropic API calls within the session. | | `--allowedTools "Bash,Read,Edit,Write"` | Pre-approve specific tools (alternative to `--dangerously-skip-permissions`). | ## Using the Claude Agent SDK The `claude-agent-sdk` gives you programmatic control over the agent loop. Your backend acts as the control plane, approving or denying every tool call while the agent runs isolated in a Baponi sandbox. The Claude Agent SDK ships a bundled binary with its own internal sandbox runtime. Because the code is already running inside a Baponi sandbox, the SDK's internal sandbox triggers operations that are blocked by Baponi's security policy, and the outer sandbox prevents the inner one from initializing. Set `cli_path` to the pre-installed Node.js CLI to bypass this. All SDK features (hooks, streaming, tool approval) work identically: ```python options = ClaudeAgentOptions( cli_path="/home/baponi/.local/bin/claude", ) ``` The example below runs a minimal Flask control plane inside the sandbox to demonstrate the pattern. In production, your control plane is a server you run anywhere (your own infrastructure, a cloud function, etc.) and the SDK hooks call out to it over the network. Since `flask` is not in the default image, install it first. Using the same `thread_id` across calls means installed packages and files persist between steps. The control plane server below is a minimal example that approves every tool call. In production, this would be your own server running anywhere outside the sandbox, enforcing whatever approval policy you need: ```python title="control_plane.py" from flask import Flask, request, jsonify app = Flask(__name__) @app.post("/approve-tool") def approve(): tool = request.json["tool"] print(f"[control-plane] Approved: {tool}") return jsonify(approved=True) # Your policy logic goes here if __name__ == "__main__": app.run(host="127.0.0.1", port=5100) ``` The agent script starts the control plane in the background, then runs the SDK with a `PreToolUse` hook that routes every tool call through it for approval: ```python title="agent.py" from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions from claude_agent_sdk.types import ( AssistantMessage, ResultMessage, TextBlock, HookInput, HookMatcher, ) # Start the control plane in the background server = subprocess.Popen(["python3", "/home/baponi/control_plane.py"]) time.sleep(2) async def approve_hook(input_data, tool_use_id, context): async with httpx.AsyncClient() as http: resp = await http.post("http://127.0.0.1:5100/approve-tool", json={ "tool": input_data["tool_name"], "input": input_data["tool_input"], }) if not resp.json().get("approved"): return {"hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", }} return {} async def main(): options = ClaudeAgentOptions( cli_path="/home/baponi/.local/bin/claude", allowed_tools=["Bash", "Read", "Write", "Edit"], hooks={"PreToolUse": [HookMatcher(matcher=".*", hooks=[approve_hook])]}, ) async with ClaudeSDKClient(options=options) as sdk: await sdk.query("Write a haiku to /home/baponi/haiku.txt, then read it back") async for msg in sdk.receive_response(): if isinstance(msg, AssistantMessage): for block in msg.content: if isinstance(block, TextBlock): print(block.text) elif isinstance(msg, ResultMessage): break asyncio.run(main()) server.terminate() ``` Tie it together with the Baponi SDK. Write both files to the sandbox, then run the agent: ```python from baponi import Baponi client = Baponi() thread = "sdk-controlled-session" # Install Flask (persists via thread_id) client.execute(code="pip install flask", language="bash", timeout=60, thread_id=thread) # Write both scripts to the persistent filesystem CONTROL_PLANE_PY = "..." # Contents of control_plane.py above AGENT_PY = "..." # Contents of agent.py above client.execute( code=f"import pathlib; pathlib.Path('/home/baponi/control_plane.py').write_text('''{CONTROL_PLANE_PY}''')", language="python", timeout=10, thread_id=thread, ) client.execute( code=f"import pathlib; pathlib.Path('/home/baponi/agent.py').write_text('''{AGENT_PY}''')", language="python", timeout=10, thread_id=thread, ) # Run the agent result = client.execute( code="python3 /home/baponi/agent.py", language="bash", timeout=300, thread_id=thread, ) print(result.stdout) ``` Every tool call (`Write`, `Read`, `Bash`) hits `POST /approve-tool` before execution. The sandbox handles isolation; your backend handles policy. ## How connector credential injection works Baponi [connectors](/features#connectors) inject credentials into the sandbox at execution time without exposing them in your code: | Connector | What gets injected | How Claude Code uses it | |-----------|-------------------|------------------------| | **Anthropic** | `ANTHROPIC_API_KEY` env var | Claude Code reads it automatically for API calls | | **GitHub** | `.gitconfig` with URL rewriting + `GH_TOKEN` env var | `git clone/push` and `gh` CLI work on private repos transparently | Your wrapper code never touches credentials. Configure them once in the admin UI, attach to your API key, and every execution gets them automatically. See [Connectors](/features#connectors) for supported integrations including PostgreSQL, MySQL, BigQuery, AWS S3, GCS, and more. ## FAQ ### What timeout should I set? For simple tasks (write a function, fix a bug), 300 seconds (5 minutes) is usually enough. For complex multi-file refactors, set up to 3600 seconds (1 hour). The Pro plan supports timeouts up to 3600s. ### Can I run multiple Claude Code sessions in parallel? Yes. Use different `thread_id` values for each session. Each gets its own persistent filesystem. Baponi handles concurrency, and there's no shared state between threads. ### What happens if Claude Code exceeds the timeout? The sandbox terminates the process and returns what was captured so far. Use `--max-turns` as an additional safety rail to prevent Claude Code from running too many iterations. ### Why does the Agent SDK need `cli_path`? The Claude Agent SDK's bundled binary includes its own internal sandbox runtime (bubblewrap) for local development safety. Inside a Baponi sandbox, the code is already running in an isolated environment with multi-layer security, so the SDK's internal sandbox is redundant. When it tries to initialize, it triggers low-level operations that conflict with Baponi's security policy. Setting `cli_path` to the Node.js CLI (`/home/baponi/.local/bin/claude`) bypasses this entirely. The Node.js CLI provides the exact same functionality (hooks, streaming, tool approval) without the internal sandbox layer. --- # Storage Path Scoping: Multi-Tenant Data Isolation for AI Agents URL: https://baponi.ai/docs/guides/storage-path-scoping/ 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. --- # Workload Identity Federation URL: https://baponi.ai/docs/guides/workload-identity-federation/ ## What is Workload Identity Federation? Workload Identity Federation (WIF) lets Baponi access your cloud storage buckets without storing any secrets. Instead of sharing service account keys or access credentials, Baponi's cluster proves its identity to your cloud provider using short-lived OIDC tokens. Your cloud verifies the token and grants scoped access -- no keys to rotate, no secrets to store. The trust is cross-project (GCP) or cross-account (AWS), so your data stays in your environment while Baponi gets the minimum access it needs. ## Prerequisites - A Baponi account with **admin** access - **GCP**: A project with a GCS bucket and the `gcloud` CLI installed - **AWS**: An account with an S3 bucket and the `aws` CLI installed ## Setup 1. **Get Baponi's cluster identity** In the Baponi console, go to **Settings > WIF Configuration**. Note the **OIDC issuer URL** and **audience** values. You will use these in the next steps. 2. **Create a Workload Identity Pool** ```bash gcloud iam workload-identity-pools create baponi-pool \ --location="global" \ --display-name="Baponi Code Execution" ``` 3. **Add Baponi as a trusted OIDC provider** Replace `OIDC_ISSUER_URL` and `AUDIENCE` with the values from step 1. ```bash gcloud iam workload-identity-pools providers create-oidc baponi-provider \ --workload-identity-pool="baponi-pool" \ --location="global" \ --issuer-uri="OIDC_ISSUER_URL" \ --allowed-audiences="AUDIENCE" \ --attribute-mapping="google.subject=assertion.sub" ``` 4. **Create a service account for Baponi** ```bash gcloud iam service-accounts create baponi-storage \ --display-name="Baponi Storage Access" ``` 5. **Grant the service account access to your bucket** Replace `YOUR_BUCKET` with your GCS bucket name. ```bash gcloud storage buckets add-iam-policy-binding gs://YOUR_BUCKET \ --member="serviceAccount:baponi-storage@YOUR_PROJECT.iam.gserviceaccount.com" \ --role="roles/storage.objectUser" ``` 6. **Allow Baponi to impersonate the service account** Replace `YOUR_PROJECT`, `YOUR_PROJECT_NUMBER`, `YOUR_NAMESPACE`, and `YOUR_SERVICE_ACCOUNT` with your values. For Baponi Cloud, use `baponi` for both namespace and service account. For self-hosted deployments, check the namespace and service account name from your Helm release: `kubectl get sa -n `. ```bash gcloud iam service-accounts add-iam-policy-binding \ baponi-storage@YOUR_PROJECT.iam.gserviceaccount.com \ --role="roles/iam.workloadIdentityUser" \ --member="principalSet://iam.googleapis.com/projects/YOUR_PROJECT_NUMBER/locations/global/workloadIdentityPools/baponi-pool/attribute.sub/system:serviceaccount:YOUR_NAMESPACE:YOUR_SERVICE_ACCOUNT" ``` 7. **Configure in Baponi** Go to **Storage Connections**, click **New Connection**, select **GCS**, choose **Cross-Account WIF**, and enter the pool provider resource name and service account email. 1. **Get Baponi's cluster identity** In the Baponi console, go to **Settings > WIF Configuration**. Note the **OIDC issuer URL** and **audience** values. You will use these in the next steps. 2. **Create an OIDC identity provider in AWS** Replace `OIDC_ISSUER_URL` and `AUDIENCE` with your values. Get the OIDC issuer's TLS thumbprint first: ```bash # Extract the TLS certificate thumbprint from the OIDC issuer ISSUER_HOST=$(echo "OIDC_ISSUER_URL" | sed 's|https://||') THUMBPRINT=$(echo | openssl s_client -servername "$ISSUER_HOST" \ -showcerts -connect "$ISSUER_HOST:443" 2>/dev/null \ | openssl x509 -fingerprint -sha1 -noout \ | sed 's/://g' | cut -d= -f2) echo "$THUMBPRINT" ``` Then create the OIDC provider: ```bash aws iam create-open-id-connect-provider \ --url "OIDC_ISSUER_URL" \ --client-id-list "AUDIENCE" \ --thumbprint-list "$THUMBPRINT" ``` 3. **Create an IAM role with a trust policy** Create a file called `trust-policy.json` with the following content. Replace `YOUR_ACCOUNT_ID`, `OIDC_ISSUER_HOST`, and `AUDIENCE` with your values. ```json { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::YOUR_ACCOUNT_ID:oidc-provider/OIDC_ISSUER_HOST" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "OIDC_ISSUER_HOST:aud": "AUDIENCE", "OIDC_ISSUER_HOST:sub": "system:serviceaccount:YOUR_NAMESPACE:YOUR_SERVICE_ACCOUNT" } } }] } ``` Then create the role: ```bash aws iam create-role \ --role-name baponi-storage-access \ --assume-role-policy-document file://trust-policy.json ``` 4. **Attach S3 access policy** Replace `YOUR_BUCKET` with your S3 bucket name. ```bash aws iam put-role-policy \ --role-name baponi-storage-access \ --policy-name BaponiS3Access \ --policy-document '{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": [ "s3:GetObject", "s3:PutObject", "s3:ListBucket", "s3:DeleteObject" ], "Resource": [ "arn:aws:s3:::YOUR_BUCKET", "arn:aws:s3:::YOUR_BUCKET/*" ] }] }' ``` 5. **Configure in Baponi** Go to **Storage Connections**, click **New Connection**, select **AWS S3**, choose **Cross-Account WIF**, and enter the IAM Role ARN. ## Verify the Connection Run a quick test to confirm the connection works. Execute code in a Baponi sandbox that reads from or writes to your bucket: ```python # List files in the mounted storage files = os.listdir("/data") print(f"Connected. Found {len(files)} files in /data") # Write a test file with open("/data/wif-test.txt", "w") as f: f.write("WIF connection verified.") print("Write succeeded.") ``` If the execution completes without errors, WIF is configured correctly. ## Troubleshooting Verify the WIF pool (GCP) or OIDC identity provider (AWS) trusts the correct OIDC issuer URL and audience. These must match the values shown in **Settings > WIF Configuration** exactly. WIF authentication succeeded, but the service account (GCP) or IAM role (AWS) lacks the required bucket permissions. Check that `roles/storage.objectUser` (GCP) or the S3 policy (AWS) is attached and references the correct bucket. The `allowed-audiences` in your WIF provider (GCP) or `client-id-list` in your OIDC provider (AWS) does not match the audience configured in Baponi. Copy the exact audience string from **Settings > WIF Configuration**. The OIDC issuer URL does not match between Baponi and your cloud provider setup. Ensure you used the full issuer URL (including `https://`) when creating the WIF provider or OIDC identity provider. ## FAQ **How long do tokens last?** Default 1 hour. Kubernetes automatically rotates tokens at 80% of expiry -- no manual renewal needed. **How do I revoke access?** Delete the WIF pool provider (GCP) or remove the OIDC identity provider trust (AWS). Access is revoked immediately. **What information does Baponi store?** Only the WIF pool provider resource name (GCP) or IAM Role ARN (AWS). No secrets, no keys, no credentials. **Can I audit token usage?** Yes. GCP Cloud Audit Logs and AWS CloudTrail both log STS token exchanges, giving you full visibility into when and how tokens are used. --- # API Overview URL: https://baponi.ai/docs/api/overview/ Baponi is a sandboxed code execution platform for AI agents. Authenticate with an API key, send code, get results. ## Base URL | Surface | Base URL | Auth | |---------|----------|------| | Execution API | `https://api.baponi.ai` | API key (Bearer token) | | MCP Protocol | `https://api.baponi.ai/mcp` | API key (Bearer token) | All requests must use HTTPS. HTTP requests are rejected. ## Authentication ### API key authentication Every request to the Execution API requires an API key in the `Authorization` header: ```bash curl -X POST https://api.baponi.ai/v1/sandbox/execute \ -H "Authorization: Bearer sk-us-YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"code": "print(1+1)", "language": "python"}' ``` API keys start with `sk-` followed by a region code and a base64-encoded token. Create keys in the [admin console](https://console.baponi.ai) under **API Keys**. Each API key is bound to: - A **sandbox** (runtime image, CPU, RAM, network policy) - Optional **storage connections** (S3, GCS, Azure buckets mounted as local directories) - Optional **volumes** (managed persistent storage) - Optional **connectors** (database and service credentials injected at runtime) This means the API key determines the entire execution environment. You configure everything once in the admin console, then every request with that key runs in the same environment. API keys are shown only once at creation time. Store them securely. If lost, revoke the key and create a new one. ## Request format All request bodies are JSON. Set `Content-Type: application/json`. ```bash curl -X POST https://api.baponi.ai/v1/sandbox/execute \ -H "Authorization: Bearer $BAPONI_API_KEY" \ -H "Content-Type: application/json" \ -d '{"code": "print(\"hello\")", "language": "python"}' ``` ## Error responses Baponi returns structured JSON errors with a machine-readable `error` code and a human-readable `message`: ```json { "error": "validation_error", "message": "code must be between 1 byte and 1 MB" } ``` ### HTTP status codes | Status | Error Code | Description | |--------|------------|-------------| | `400` | `validation_error` | Request body failed validation. Check the `message` for details. | | `401` | `unauthorized` | Missing, invalid, or revoked API key. | | `403` | `forbidden` | Valid key but insufficient permissions. | | `404` | `not_found` | Resource does not exist. | | `409` | `conflict` | Resource conflict. Thread is busy, or duplicate name. | | `429` | `rate_limited` | Too many requests or plan limit exceeded. | | `500` | `internal_error` | Server error. Retry with backoff. | | `503` | `service_unavailable` | Execution infrastructure temporarily unavailable. | | `504` | `gateway_timeout` | Execution exceeded the timeout. | ### Thread conflict (409) If two requests use the same `thread_id` simultaneously, the second request returns `409 Conflict`. Only one execution can use a given thread at a time. Use different `thread_id` values for parallel work. ### Rate limiting (429) Rate limiting applies in two cases: 1. **Failed authentication.** 3 failed API key attempts per IP within 60 seconds triggers a temporary block. 2. **Plan limits.** Exceeding your plan's concurrent execution limit or other quotas returns `429` with an upgrade message. Rate-limited responses include the standard error format: ```json { "error": "rate_limited", "message": "concurrent execution limit reached (5/5). Upgrade to Pro for 100 concurrent executions." } ``` ## Plan limits Resource limits depend on your plan tier: | Limit | Free | Pro | Enterprise | |-------|------|-----|------------| | Max CPU per sandbox | 1 core | 4 cores | Unlimited | | Max RAM per sandbox | 1 GiB | 4 GiB | Unlimited | | Concurrent executions | 5 | 100 | Unlimited | | Max timeout | 60 seconds | 1 hour | Unlimited | | API keys | 10 | Unlimited | Unlimited | | Audit log retention | 1 day | 30 days | Unlimited | | Custom OCI images | 1 | Unlimited | Unlimited | | Streaming / async delivery | No | Yes | Yes | All tiers include: unlimited team seats, storage connections, volumes, and connectors. See [Pricing](/pricing) for credit costs and overages. ## SDKs and integrations ### Python SDK ```bash pip install baponi ``` ```python from baponi import Baponi client = Baponi(api_key="sk-us-...") # or set BAPONI_API_KEY env var result = client.execute("print('hello')") print(result.stdout) # hello ``` The SDK supports all three delivery modes: ```python # Inline (default) result = client.execute("print('hello')") # Streaming - real-time stdout/stderr via NDJSON with client.execute_stream("print('hello')") as stream: result = stream.until_done() # Webhook - fire-and-forget, result POSTed to your URL handle = client.execute_webhook("print('hello')", webhook_url="https://...") result = handle.wait() ``` All methods accept `env_vars` for per-request environment variables: ```python result = client.execute( "import os; print(os.environ['API_TOKEN'])", env_vars={"API_TOKEN": "sk-test-123"}, ) ``` Status and cancellation: ```python status = client.get_execution("trc_abc12345") cancel = client.cancel_execution("trc_abc12345") ``` The Python SDK includes ready-made integrations for LLM frameworks: ```python from baponi.anthropic import code_sandbox # Anthropic Claude from baponi.openai import code_sandbox # OpenAI Agents SDK from baponi.google import code_sandbox # Google Gemini from baponi.langchain import code_sandbox # LangChain from baponi.crewai import code_sandbox # CrewAI ``` ### MCP (Model Context Protocol) Baponi is a native MCP server. Connect from any MCP client with a URL and API key, no SDK required: ```json { "mcpServers": { "baponi": { "url": "https://api.baponi.ai/mcp", "headers": { "Authorization": "Bearer sk-us-YOUR_API_KEY" } } } } ``` See the [MCP Protocol](/docs/api/mcp) reference for details. ### HTTP (any language) The API is a standard REST interface. Any HTTP client works: ```python response = requests.post( "https://api.baponi.ai/v1/sandbox/execute", headers={"Authorization": "Bearer sk-us-YOUR_API_KEY"}, json={"code": "print('hello')", "language": "python"}, ).json() ``` ```javascript const response = await fetch("https://api.baponi.ai/v1/sandbox/execute", { method: "POST", headers: { "Authorization": "Bearer sk-us-YOUR_API_KEY", "Content-Type": "application/json", }, body: JSON.stringify({ code: "console.log('hello')", language: "node" }), }); const result = await response.json(); ``` ```go payload := `{"code": "print('hello')", "language": "python"}` req, _ := http.NewRequest("POST", "https://api.baponi.ai/v1/sandbox/execute", strings.NewReader(payload)) req.Header.Set("Authorization", "Bearer sk-us-YOUR_API_KEY") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) ``` ## API endpoints at a glance ### Execution API (`https://api.baponi.ai`) | Method | Endpoint | Description | |--------|----------|-------------| | `POST` | [`/v1/sandbox/execute`](/docs/api/execute) | Execute code in a sandbox (inline or streaming) | | `GET` | [`/v1/executions/{trace_id}`](/docs/api/executions#get-execution-status) | Execution status and result | | `GET` | [`/v1/executions/{trace_id}/output`](/docs/api/executions#reconnect-to-output-stream) | Output chunks (streaming reconnection) | | `POST` | [`/v1/files/list`](/docs/api/files/#list-files) | List files in a thread or volume | | `POST` | [`/v1/files/upload_url`](/docs/api/files/#generate-upload-url) | Get a signed upload URL | | `POST` | [`/v1/files/download_url`](/docs/api/files/#generate-download-url) | Get a signed download URL | | `DELETE` | [`/v1/files`](/docs/api/files/#delete-a-file) | Delete a file | | `GET` | `/health` | Health check | | `GET` | `/ready` | Readiness probe | ### MCP Protocol (`https://api.baponi.ai/mcp`) | Method | Description | |--------|-------------| | `POST` | JSON-RPC 2.0 endpoint (initialize, tools/list, tools/call) | Tools exposed: `sandbox_execute`, `files_download`. See [MCP Protocol](/docs/api/mcp). --- # Execute Code URL: https://baponi.ai/docs/api/execute/ Baponi is a sandboxed code execution platform for AI agents. The Execute Code endpoint runs Python, Node.js, or Bash in a multi-layer isolated sandbox and returns stdout, stderr, and an exit code. Sandbox overhead is typically 12-18ms. One required parameter (`code`), one HTTP call, structured JSON response. ## Endpoint ``` POST https://api.baponi.ai/v1/sandbox/execute ``` **Authentication:** Bearer token with an API key (`sk-us-...`). **Content-Type:** `application/json` ```bash curl -X POST https://api.baponi.ai/v1/sandbox/execute \ -H "Authorization: Bearer sk-us-YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"code": "print(\"Hello from Baponi!\")", "language": "python"}' ``` ## Delivery modes The execute endpoint supports three delivery modes, selected via request headers. The request body is identical for all modes (with an optional `webhook_url` field for webhook mode). | Mode | Header | Response | Plan requirement | |------|--------|----------|------------------| | Inline (default) | `Accept: application/json` | Buffered JSON response after execution completes | All plans | | Streaming | `Accept: application/x-ndjson` | Real-time NDJSON stream of output as it is produced | Pro or Enterprise | | Webhook | `Prefer: respond-async` | 202 Accepted with `trace_id`, result POSTed to webhook URL | Pro or Enterprise | Inline mode is the default. The connection stays open until execution finishes, then returns the full result as a single JSON object. This is the simplest integration path and works for executions that complete in seconds. Streaming mode delivers stdout and stderr line-by-line as the code runs. Use it for long-running executions where you need real-time output, progress updates, or partial results before completion. See [Streaming execution (NDJSON)](#streaming-execution-ndjson) below. Webhook mode returns `202 Accepted` immediately with a `trace_id`. The execution runs in the background and the result is POSTed to your webhook URL on completion. Use it for long-running executions, CI pipelines, or unreliable clients that can't hold a connection open. See [Webhook delivery](#webhook-delivery) below. ## Request body parameters | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `code` | string | Yes | - | Code to execute. 1 byte to 1 MB. | | `language` | string | No | `"python"` | `"python"`, `"node"`, or `"bash"` | | `timeout` | integer | No | `60` | Max execution time in seconds. Range: 1-60 (Free), 1-3600 (Pro/Enterprise). | | `thread_id` | string | No | `null` | Persist `/home/baponi` directory across calls. Max 128 chars, alphanumeric + hyphens/underscores. Must start with an alphanumeric character. | | `metadata` | object | No | `null` | Key-value pairs for audit logging. Max 10 keys, key max 40 chars, value max 256 chars. Not sent to the sandbox. | | `webhook_url` | string | No | `null` | Per-request webhook URL override for webhook delivery mode. HTTPS only. If omitted, uses the webhook URL configured on the API key. | | `env_vars` | object | No | `null` | Environment variables injected into the sandbox for this execution. Keys must be uppercase letters, digits, and underscores (starting with a letter). See [environment variables](#environment-variables) for merge behavior and limits. | | `sub_paths` | string[] | No | `null` | Narrow storage mounts to specific subdirectories. Each entry is `/data/{slug}/{path}`. Must respect connection-level and API key-level path constraints. Max 10 entries, 512 chars each. REST API only - deliberately excluded from MCP to prevent LLM-driven path manipulation. See [storage path scoping](#storage-path-scoping). | The execution environment (runtime image, CPU, RAM, network policy, storage mounts, injected credentials) is configured in the [admin console](https://console.baponi.ai) and bound to your API key. You cannot override sandbox settings per-request. This is by design: the API key is the security boundary, and all configuration is centralized. ## Response body A successful request returns `200 OK` with the execution result: ```json { "success": true, "stdout": "Hello from Baponi!\n", "stderr": "", "exit_code": 0, "error": null } ``` ### Response fields | Field | Type | Description | |-------|------|-------------| | `success` | boolean | `true` if `exit_code` is 0. | | `stdout` | string | Standard output from the executed code. | | `stderr` | string | Standard error from the executed code. | | `exit_code` | integer | Process exit code. 0 means success, non-zero means the code exited with an error. | | `error` | string or null | Error message if execution failed at the platform level (not a code error). `null` on success. | `success: false` with a non-zero `exit_code` means your code ran but exited with an error (e.g., an unhandled exception). The `error` field is for platform-level failures. If `error` is non-null, the sandbox itself failed to run your code. ## Streaming execution (NDJSON) To stream output in real time, set the `Accept` header to `application/x-ndjson`. The request body is the same as inline mode. The response is a chunked NDJSON stream where each line is a self-contained JSON object with a `type` field. ```bash curl -N -X POST https://api.baponi.ai/v1/sandbox/execute \ -H "Authorization: Bearer $BAPONI_API_KEY" \ -H "Content-Type: application/json" \ -H "Accept: application/x-ndjson" \ -d '{"code": "import time\nfor i in range(3):\n print(f\"step {i}\")\n time.sleep(1)", "timeout": 60}' ``` ### NDJSON event types Every event includes a monotonic `seq` number for ordering and reconnection. | `type` | Fields | Description | |--------|--------|-------------| | `status` | `trace_id`, `status`, `seq` | Execution has started. Emitted once at the beginning of the stream. | | `output` | `stream`, `data`, `seq` | A line of output from the sandbox. `stream` is `"stdout"` or `"stderr"`. | | `keepalive` | `seq` | Empty heartbeat sent every 15 seconds to prevent proxy and load balancer connection timeouts. | | `result` | `trace_id`, `status`, `result`, `output_truncated`, `seq` | Execution completed (or was cancelled). `status` is `"success"`, `"failed"`, `"timeout"`, or `"cancelled"`. `result` contains the same fields as the inline JSON response. Always the last event. | ### Example stream ```jsonl {"type":"status","trace_id":"trc_a1b2c3d4","status":"running","seq":1} {"type":"output","stream":"stdout","data":"step 0\n","seq":2} {"type":"output","stream":"stdout","data":"step 1\n","seq":3} {"type":"output","stream":"stdout","data":"step 2\n","seq":4} {"type":"result","trace_id":"trc_a1b2c3d4","status":"success","result":{"success":true,"stdout":"step 0\nstep 1\nstep 2\n","stderr":"","exit_code":0,"error":null},"output_truncated":false,"seq":5} ``` ### Streaming behavior - **Line-buffered output.** Output events are emitted on each newline (`\n`) from the sandbox process. Partial lines without a trailing newline are buffered until the next newline or until execution completes. - **15-second keepalive interval.** The gateway emits a `keepalive` event every 15 seconds on idle streams to prevent proxies (nginx, Cloudflare, AWS ALB) from closing the connection. - **`output_truncated` flag.** If the stream produced more than 10,000 output chunks, older chunks may be evicted from the reconnection buffer. The `result` event sets `output_truncated: true` to indicate this. The final `result.stdout` and `result.stderr` fields always contain the complete buffered output regardless. - **Client disconnect.** If the client disconnects mid-stream, the execution continues to completion. Use the [execution status endpoint](/docs/api/executions#get-execution-status) to retrieve the final result and the [output reconnection endpoint](/docs/api/executions#reconnect-to-output-stream) to retrieve missed output chunks. - **Cancellation.** A running streaming execution can be cancelled via `POST /v1/executions/{trace_id}/cancel`. The process is killed immediately and the stream emits a final `result` event with `status: "cancelled"` and `error: "Cancelled by user"`. See [Cancel a running execution](/docs/api/executions#cancel-a-running-execution). ### Streaming examples ```bash # Stream output in real time (-N disables curl output buffering) curl -N -X POST https://api.baponi.ai/v1/sandbox/execute \ -H "Authorization: Bearer $BAPONI_API_KEY" \ -H "Content-Type: application/json" \ -H "Accept: application/x-ndjson" \ -d '{ "code": "for i in range(5):\n import time; time.sleep(1)\n print(f\"Processing batch {i}...\")", "timeout": 60 }' ``` ```python from baponi import Baponi client = Baponi() with client.execute_stream( "for i in range(5):\n import time; time.sleep(1)\n print(f'Processing batch {i}...')", timeout=60, ) as stream: for event in stream: if isinstance(event, baponi.OutputEvent): print(f"[{event.stream}] {event.data}", end="") result = stream.get_final_result() print(f"\nDone: exit_code={result.exit_code}") ``` ```python response = requests.post( "https://api.baponi.ai/v1/sandbox/execute", headers={ "Authorization": "Bearer sk-us-YOUR_API_KEY", "Content-Type": "application/json", "Accept": "application/x-ndjson", }, json={ "code": "for i in range(5):\n import time; time.sleep(1)\n print(f'Processing batch {i}...')", "timeout": 60, }, stream=True, ) for line in response.iter_lines(): if line: event = json.loads(line) if event["type"] == "output": print(f"[{event['stream']}] {event['data']}", end="") elif event["type"] == "result": print(f"\nDone: exit_code={event['result']['exit_code']}") ``` Streaming execution requires a Pro or Enterprise plan. Free tier requests with `Accept: application/x-ndjson` return `429` with the message: "Streaming execution requires a Pro or Enterprise plan. Upgrade at https://baponi.ai/pricing" ## Webhook delivery Send `Prefer: respond-async` to execute code asynchronously and receive the result via webhook. ```bash curl -X POST https://api.baponi.ai/v1/sandbox/execute \ -H "Authorization: Bearer sk-us-YOUR_API_KEY" \ -H "Content-Type: application/json" \ -H "Prefer: respond-async" \ -d '{"code": "import time; time.sleep(60); print(\"done\")", "language": "python", "timeout": 120}' ``` The response is `202 Accepted` with a `trace_id`: ```json { "trace_id": "trc_a1b2c3d4", "status": "running" } ``` When the execution completes, Baponi POSTs the result to the webhook URL configured on your API key (or the per-request `webhook_url` if provided): ```json { "event": "execution.completed", "trace_id": "trc_a1b2c3d4", "timestamp": "2026-03-16T12:00:00Z", "result": { "success": true, "stdout": "done\n", "stderr": "", "exit_code": 0, "duration_ms": 60142, "error": null }, "output_truncated": false } ``` If the execution is cancelled, the event is `execution.cancelled`. ### Webhook signing If a webhook secret is configured on the API key, the delivery includes an `X-Baponi-Signature` header with an HMAC-SHA256 signature: `sha256=`. Verify it server-side to authenticate the request. ### Webhook retry Delivery is attempted up to 3 times with backoff (1s, 10s, 60s). If all attempts fail, `webhook_delivery_status` is set to `"failed"`. Poll [`GET /v1/executions/{trace_id}`](/docs/api/executions) to retrieve the result. ### Webhook URL configuration Configure a default webhook URL on each API key via the admin console or API. Per-request `webhook_url` in the body overrides the key default. Both must be HTTPS. SSRF protection validates the URL before accepting the request. ## Error responses Error responses use a structured JSON format with a machine-readable `error` code and a human-readable `message`: ```json { "error": "validation_error", "message": "code must be between 1 byte and 1 MB" } ``` ### HTTP status codes | Status | Error Code | When | |--------|------------|------| | `400` | `validation_error` | Invalid `code` length, unrecognized `language`, malformed `thread_id`, or `metadata` exceeds limits. | | `401` | `unauthorized` | Missing, invalid, or revoked API key. | | `409` | `conflict` | Another execution is already using this `thread_id`. Only one execution per thread at a time. | | `429` | `rate_limited` | Concurrent execution limit exceeded for your plan, requested `timeout` exceeds your plan's maximum, or streaming requested on Free tier. | | `503` | `service_unavailable` | No executor pod is available to handle the request. Retry with backoff. | | `504` | `gateway_timeout` | Execution exceeded the specified `timeout`. The process was killed. | Rate-limited responses include an actionable message: ```json { "error": "rate_limited", "message": "concurrent execution limit reached (5/5). Upgrade to Pro for 100 concurrent executions." } ``` See the [API Overview](/docs/api/overview#error-responses) for the full error reference. ## Examples ### Run Python, Node.js, or Bash ```bash # Python (default language) curl -X POST https://api.baponi.ai/v1/sandbox/execute \ -H "Authorization: Bearer $BAPONI_API_KEY" \ -H "Content-Type: application/json" \ -d '{"code": "import sys; print(f\"Python {sys.version}\")"}' # Node.js curl -X POST https://api.baponi.ai/v1/sandbox/execute \ -H "Authorization: Bearer $BAPONI_API_KEY" \ -H "Content-Type: application/json" \ -d '{"code": "console.log(`Node.js ${process.version}`)", "language": "node"}' # Bash curl -X POST https://api.baponi.ai/v1/sandbox/execute \ -H "Authorization: Bearer $BAPONI_API_KEY" \ -H "Content-Type: application/json" \ -d '{"code": "echo \"Bash $BASH_VERSION\" && uname -a", "language": "bash"}' ``` ```python from baponi import Baponi client = Baponi(api_key="sk-us-YOUR_API_KEY") # or set BAPONI_API_KEY env var # Python (default) result = client.execute("import sys; print(f'Python {sys.version}')") print(result.stdout) # Node.js result = client.execute("console.log(`Node.js ${process.version}`)", language="node") print(result.stdout) # Bash result = client.execute("echo \"Bash $BASH_VERSION\" && uname -a", language="bash") print(result.stdout) ``` ### Persist state across calls with thread_id Without `thread_id`, every execution is fully ephemeral, nothing persists. With `thread_id`, the `/home/baponi` directory is saved to cloud storage after execution and restored on the next call with the same `thread_id`. Installed packages, created files, and environment modifications in the home directory all carry over. ```bash # Call 1: Install a package and create a file curl -X POST https://api.baponi.ai/v1/sandbox/execute \ -H "Authorization: Bearer $BAPONI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "code": "pip install --user pandas && echo done", "language": "bash", "thread_id": "analysis-session-1" }' # Call 2: pandas is already installed, pick up where you left off curl -X POST https://api.baponi.ai/v1/sandbox/execute \ -H "Authorization: Bearer $BAPONI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "code": "import pandas as pd; print(pd.__version__)", "language": "python", "thread_id": "analysis-session-1" }' ``` ```python from baponi import Baponi client = Baponi() # Call 1: Install pandas result = client.execute( "pip install --user pandas && echo done", language="bash", thread_id="analysis-session-1", ) print(result.stdout) # done # Call 2: pandas is already installed result = client.execute( "import pandas as pd; print(pd.__version__)", thread_id="analysis-session-1", ) print(result.stdout) # 2.2.3 ``` Between calls, nothing is running and there is no idle billing. Baponi saves only the diff to cloud storage and restores it on the next call. You can resume a thread minutes or days later. ### Attach metadata for audit logging Metadata is attached to the execution record for debugging and audit queries. It is NOT sent to the sandbox. Your code cannot read metadata values. ```bash curl -X POST https://api.baponi.ai/v1/sandbox/execute \ -H "Authorization: Bearer $BAPONI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "code": "print(\"processing order\")", "metadata": { "user_id": "usr_abc123", "request_id": "req_xyz789", "agent": "order-processor-v2" } }' ``` ```python from baponi import Baponi client = Baponi() result = client.execute( "print('processing order')", metadata={ "user_id": "usr_abc123", "request_id": "req_xyz789", "agent": "order-processor-v2", }, ) ``` Query execution history with metadata filters in the [admin console](https://console.baponi.ai). ### Set a custom timeout The default timeout is 60 seconds. Free tier maximum is 60 seconds. Pro tier maximum is 3600 seconds (1 hour). Enterprise is configurable. When the timeout is reached, the process is killed immediately. ```bash # Long-running data processing (Pro tier required for >60s) curl -X POST https://api.baponi.ai/v1/sandbox/execute \ -H "Authorization: Bearer $BAPONI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "code": "import time; time.sleep(120); print(\"done\")", "timeout": 180 }' ``` ```python from baponi import Baponi client = Baponi() result = client.execute( "import time; time.sleep(120); print('done')", timeout=180, ) print(result.stdout) # done ``` If the timeout is exceeded, the response has `exit_code: -1` and `error` describes the timeout: ```json { "success": false, "stdout": "", "stderr": "", "exit_code": -1, "error": "execution timed out after 30s" } ``` ### Handle errors in your code ```bash # Code that raises an exception curl -X POST https://api.baponi.ai/v1/sandbox/execute \ -H "Authorization: Bearer $BAPONI_API_KEY" \ -H "Content-Type: application/json" \ -d '{"code": "raise ValueError(\"something went wrong\")"}' ``` Response: ```json { "success": false, "stdout": "", "stderr": "Traceback (most recent call last):\n File \"/home/baponi/main.py\", line 1, in \n raise ValueError(\"something went wrong\")\nValueError: something went wrong\n", "exit_code": 1, "error": null } ``` ```python from baponi import Baponi client = Baponi() result = client.execute("raise ValueError('something went wrong')") if not result.success: print(f"Code failed with exit code {result.exit_code}") print(f"stderr: {result.stderr}") else: print(result.stdout) ``` Check `success` (or `exit_code`) to determine if your code ran successfully. Check `error` to determine if the platform itself failed. A non-zero `exit_code` with `error: null` means your code ran but exited with an error. This is normal for unhandled exceptions, assertion failures, or `sys.exit(1)`. ### Pass environment variables Environment variables are injected into the sandbox and accessible to your code via standard APIs (`os.environ` in Python, `process.env` in Node.js, `$VAR` in Bash). Use them to pass configuration, API keys, or feature flags without embedding them in code. Per-request `env_vars` are merged with variables set on the sandbox and API key in the admin console, with request values taking highest precedence. See [environment variables](#environment-variables) for the full merge model. ```bash curl -X POST https://api.baponi.ai/v1/sandbox/execute \ -H "Authorization: Bearer $BAPONI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "code": "import os; print(f\"Target: {os.environ[\"TARGET_URL\"]}\")", "env_vars": { "TARGET_URL": "https://api.example.com", "LOG_LEVEL": "debug" } }' ``` ```python from baponi import Baponi client = Baponi() result = client.execute( "import os; print(f'Target: {os.environ[\"TARGET_URL\"]}')", env_vars={ "TARGET_URL": "https://api.example.com", "LOG_LEVEL": "debug", }, ) print(result.stdout) # Target: https://api.example.com ``` See [environment variables](#environment-variables) for the full merge model and validation rules. ### Narrow storage mounts with sub_paths When your API key has storage connections (BYOB buckets or managed volumes), the sandbox mounts them at `/data/{slug}/`. By default, the mount exposes everything the connection and API key allow. Pass `sub_paths` to narrow a mount to a specific subdirectory for this execution only. ```bash # Only mount the Q1 reports subdirectory from the "company-data" bucket curl -X POST https://api.baponi.ai/v1/sandbox/execute \ -H "Authorization: Bearer $BAPONI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "code": "import os; print(os.listdir(\"/data/company-data/\"))", "sub_paths": ["/data/company-data/reports/q1-2026"] }' ``` ```python from baponi import Baponi client = Baponi() result = client.execute( "import os; print(os.listdir('/data/company-data/'))", sub_paths=["/data/company-data/reports/q1-2026"], ) print(result.stdout) # Only files under reports/q1-2026/ ``` The sandbox sees `/data/company-data/` as the mount point, but only the `reports/q1-2026` subtree is accessible. The agent cannot see or access anything outside that path. See [storage path scoping](#storage-path-scoping) for the full three-level constraint model and validation rules. ### Full request with all parameters ```bash curl -X POST https://api.baponi.ai/v1/sandbox/execute \ -H "Authorization: Bearer $BAPONI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "code": "import os, pandas as pd\ndf = pd.read_csv(\"/data/company-data/customers.csv\")\nprint(df.describe().to_json())", "language": "python", "timeout": 60, "thread_id": "data-analysis-session", "metadata": { "user_id": "usr_abc123", "workflow": "csv-analysis", "step": "describe" }, "env_vars": { "OUTPUT_FORMAT": "json", "LOG_LEVEL": "info" }, "sub_paths": ["/data/company-data/customers"] }' ``` ```python from baponi import Baponi client = Baponi() result = client.execute( code='import os, pandas as pd\ndf = pd.read_csv("/data/company-data/customers.csv")\nprint(df.describe().to_json())', language="python", timeout=60, thread_id="data-analysis-session", metadata={ "user_id": "usr_abc123", "workflow": "csv-analysis", "step": "describe", }, env_vars={ "OUTPUT_FORMAT": "json", "LOG_LEVEL": "info", }, sub_paths=["/data/company-data/customers"], ) if result.success: print(result.stdout) else: print(f"Failed: {result.stderr}") ``` ## Behavior reference ### Supported languages and default runtimes | `language` value | Runtime | Version | |------------------|---------|---------| | `"python"` (default) | Python | 3.14 | | `"node"` | Node.js | 25 | | `"bash"` | GNU Bash | Latest | These are the default runtime images. You can import any OCI-compatible image through the [admin console](https://console.baponi.ai) and Baponi auto-discovers available interpreters. Custom images support any language or toolchain. ### thread_id: stateful execution across calls - **Without `thread_id`:** Fully ephemeral. Nothing persists after the call returns. - **With `thread_id`:** The `/home/baponi` directory is saved to cloud storage after execution and restored on the next call with the same `thread_id`. - **One execution per thread at a time.** Concurrent requests to the same `thread_id` return `409 Conflict`. Use different `thread_id` values for parallel work. - **What persists:** Installed packages (pip, npm), created files, environment modifications, anything written to `/home/baponi`. - **What does not persist:** `/tmp` is always ephemeral. System-level changes outside `/home/baponi` are discarded. - **Format:** Max 128 characters. Alphanumeric characters, hyphens (`-`), and underscores (`_`) only. Must start with an alphanumeric character. ### Network access Network policy is determined by the sandbox configuration bound to your API key (configured in the admin console): | Policy | Behavior | |--------|----------| | `blocked` (default) | No outbound network access. DNS, HTTP, and all other protocols are blocked. | | `unrestricted` | Full internet access. Outbound bytes are metered internally for billing. | You cannot change the network policy per-request. To switch between blocked and unrestricted, create separate API keys bound to different sandboxes. ### Timeout limits by plan tier | Plan | Default | Maximum | |------|---------|---------| | Free | 60s | 60s | | Pro ($97/mo) | 60s | 3600s (1 hour) | | Enterprise | 60s | Configurable | Requesting a `timeout` above your plan's maximum returns `429` with an upgrade message. It is not silently capped. ### Metadata constraints | Constraint | Limit | |------------|-------| | Max keys | 10 | | Key length | 1-40 characters | | Value length | Max 256 characters | Metadata is stored on the execution record and queryable in the [admin console](https://console.baponi.ai). It is never sent to the sandbox. Your executed code cannot read metadata values. ### Environment variables Environment variables can be set at three levels. When the same key appears at multiple levels, the most specific scope wins: 1. **Sandbox** (lowest precedence) - configured in the admin console, applied to every execution using that sandbox. 2. **API key** - configured in the admin console on each API key, applied to every execution using that key. 3. **Request** (highest precedence) - passed in the `env_vars` field of the request body, applied to that execution only. All three sources are merged at execution time. If the same key exists at multiple levels, the request value overrides the API key value, which overrides the sandbox value. #### Validation rules | Constraint | Limit | |------------|-------| | Max variables (combined after merge) | 50 | | Key format | Uppercase letters, digits, underscores. Must start with a letter. | | Key length | 1-128 characters | | Value length | Max 4,096 characters | | Total size (all keys + values combined) | 64 KB | | Reserved names | System names (e.g., `PATH`, `HOME`) and platform prefixes are blocked. | Keys and values are validated at each level independently. After the 3-way merge, the combined set is checked against the 50-variable and 64 KB limits. Requests that exceed post-merge limits return `400 validation_error`. ### Storage path scoping Storage mounts support three levels of path constraints: connection prefix, API key prefix, and per-request `sub_paths`. Each level can only narrow the scope, never widen it. Constraints are enforced server-side with segment-aware prefix matching and path traversal rejection. | 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 | | Duplicate slugs | One entry per connection per request | `sub_paths` is deliberately excluded from MCP to prevent prompt injection attacks from manipulating path selection. MCP executions use connection and API key prefixes set by an admin in the console. For the full constraint model, worked examples, common patterns, and security properties, see the [Storage Path Scoping guide](/docs/guides/storage-path-scoping). ## Plan limits that affect this endpoint | Limit | Free | Pro | Enterprise | |-------|------|-----|------------| | Max CPU per sandbox | 1 core | 4 cores | Unlimited | | Max RAM per sandbox | 1 GiB | 4 GiB | Unlimited | | Concurrent executions | 5 | 100 | Unlimited | | Max timeout | 60s | 3600s (1 hour) | Unlimited | | Streaming / async delivery | No | Yes | Yes | See [Pricing](/pricing) for credit costs. One credit = 60 seconds of execution at 1 CPU + 1 GiB RAM. Credits scale proportionally with time and resources. --- # Files API URL: https://baponi.ai/docs/api/files/ Baponi is a sandboxed code execution platform for AI agents. The Files API lets you list, upload, download, and delete files stored in **threads** and **volumes**, the two persistent storage abstractions in Baponi. All file transfers use signed URLs that point directly to cloud storage (GCS, S3, or Azure depending on deployment), so no file data passes through Baponi's servers. Maximum file size: 10 GiB. Base URL: `https://api.baponi.ai` All requests require an API key in the `Authorization` header: ``` Authorization: Bearer sk-us-YOUR_API_KEY ``` ## Threads vs volumes Before using the Files API, understand the two storage sources: | | Thread | Volume | |---|--------|--------| | **Created by** | Executing code with a `thread_id` | Admin console | | **Persistence** | Files in `/home/baponi` are saved between executions | Always persistent | | **Scope** | Tied to a single thread ID | Shared across API keys in the organization | | **Access mode** | Read-write | Read-write or read-only (configurable) | | **Source parameter** | `"thread"` | `"volume"` | | **ID parameter** | The `thread_id` value used during execution | The volume slug | Files written to `/home/baponi` during code execution are automatically persisted to the thread's storage. Use the Files API to access those files outside of a sandbox, for example, to download results or upload input data before execution. ## List files Returns a list of files and directories at the specified path. Use the `path` parameter to filter by prefix, for example, `path: "data/"` returns only files under the `data/` directory. ### Request parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `source` | string | Yes | `"thread"` or `"volume"` | | `id` | string | Yes | Thread ID or volume slug | | `path` | string | No | Path prefix to filter results. Omit to list all files. | ### Response fields | Field | Type | Description | |-------|------|-------------| | `files` | array | List of file objects | | `files[].name` | string | File or directory name | | `files[].path` | string | Full path relative to the storage root | | `files[].size` | integer | File size in bytes (0 for directories) | | `files[].modified` | string | ISO 8601 timestamp of last modification | | `files[].is_directory` | boolean | `true` if the entry is a directory | ### Examples ```bash # List all files in a thread curl -X POST https://api.baponi.ai/v1/files/list \ -H "Authorization: Bearer $BAPONI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "source": "thread", "id": "my-thread-123" }' # List files under a specific directory in a volume curl -X POST https://api.baponi.ai/v1/files/list \ -H "Authorization: Bearer $BAPONI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "source": "volume", "id": "training-data", "path": "datasets/2026/" }' ``` ```python response = requests.post( "https://api.baponi.ai/v1/files/list", headers={"Authorization": f"Bearer {api_key}"}, json={ "source": "thread", "id": "my-thread-123", }, ) files = response.json()["files"] for f in files: if not f["is_directory"]: print(f"{f['path']} ({f['size']} bytes)") ``` ### Response ```json { "files": [ { "name": "data", "path": "data", "size": 0, "modified": "2026-03-04T11:58:00Z", "is_directory": true }, { "name": "output.csv", "path": "data/output.csv", "size": 2048, "modified": "2026-03-04T12:00:00Z", "is_directory": false }, { "name": "model.pkl", "path": "model.pkl", "size": 15728640, "modified": "2026-03-04T12:01:30Z", "is_directory": false } ] } ``` ## Generate upload URL Generates a pre-signed URL for uploading a file directly to cloud storage. The upload bypasses Baponi's servers entirely. Your client sends the file straight to the storage backend (GCS, S3, or Azure). Signed URLs expire after 15 minutes. ### Request parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `source` | string | Yes | `"thread"` or `"volume"` | | `id` | string | Yes | Thread ID or volume slug | | `path` | string | Yes | Destination file path within the storage root | | `content_type` | string | No | MIME type of the file. Default: `application/octet-stream` | | `content_length` | integer | Yes | Exact file size in bytes. Maximum: 10 GiB (10,737,418,240 bytes) | ### Response fields | Field | Type | Description | |-------|------|-------------| | `url` | string | Pre-signed upload URL | | `method` | string | HTTP method to use (always `"PUT"`) | | `headers` | object | Headers to include in the upload request | | `expires_at` | string | ISO 8601 timestamp when the URL expires (15 minutes from creation) | The `Content-Type` and `Content-Length` in the upload request must exactly match the values used when generating the signed URL. Mismatches cause a `403 SignatureDoesNotMatch` error from the storage backend. ### Examples ```bash # Step 1: Get a signed upload URL UPLOAD_RESPONSE=$(curl -s -X POST https://api.baponi.ai/v1/files/upload_url \ -H "Authorization: Bearer $BAPONI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "source": "thread", "id": "my-thread-123", "path": "input.json", "content_type": "application/json", "content_length": 2048 }') # Step 2: Extract the signed URL UPLOAD_URL=$(echo "$UPLOAD_RESPONSE" | jq -r '.url') # Step 3: Upload the file directly to cloud storage curl -X PUT "$UPLOAD_URL" \ -H "Content-Type: application/json" \ --data-binary @input.json ``` ```python from pathlib import Path file_path = Path("input.json") file_size = file_path.stat().st_size content_type = "application/json" # Step 1: Get a signed upload URL from Baponi response = requests.post( "https://api.baponi.ai/v1/files/upload_url", headers={"Authorization": f"Bearer {api_key}"}, json={ "source": "thread", "id": "my-thread-123", "path": "input.json", "content_type": content_type, "content_length": file_size, }, ) upload_info = response.json() # Step 2: Upload the file directly to cloud storage with open(file_path, "rb") as f: upload_response = requests.put( upload_info["url"], headers=upload_info["headers"], data=f, ) upload_response.raise_for_status() print(f"Uploaded {file_size} bytes, URL expires at {upload_info['expires_at']}") ``` ### Response ```json { "url": "https://storage.googleapis.com/baponi-prod-storage/orgs/org_abc/threads/my-thread-123/input.json?X-Goog-Algorithm=...", "method": "PUT", "headers": { "Content-Type": "application/json" }, "expires_at": "2026-03-04T12:15:00Z" } ``` ## Generate download URL Generates a pre-signed URL for downloading a file directly from cloud storage. Like uploads, the download bypasses Baponi's servers. Signed URLs expire after 15 minutes. ### Request parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `source` | string | Yes | `"thread"` or `"volume"` | | `id` | string | Yes | Thread ID or volume slug | | `path` | string | Yes | File path to download | ### Response fields | Field | Type | Description | |-------|------|-------------| | `url` | string | Pre-signed download URL | | `method` | string | HTTP method to use (always `"GET"`) | | `expires_at` | string | ISO 8601 timestamp when the URL expires (15 minutes from creation) | ### Examples ```bash # Step 1: Get a signed download URL DOWNLOAD_RESPONSE=$(curl -s -X POST https://api.baponi.ai/v1/files/download_url \ -H "Authorization: Bearer $BAPONI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "source": "thread", "id": "my-thread-123", "path": "output.csv" }') # Step 2: Download the file directly from cloud storage DOWNLOAD_URL=$(echo "$DOWNLOAD_RESPONSE" | jq -r '.url') curl -o output.csv "$DOWNLOAD_URL" ``` ```python # Step 1: Get a signed download URL from Baponi response = requests.post( "https://api.baponi.ai/v1/files/download_url", headers={"Authorization": f"Bearer {api_key}"}, json={ "source": "thread", "id": "my-thread-123", "path": "output.csv", }, ) download_info = response.json() # Step 2: Download the file directly from cloud storage file_response = requests.get(download_info["url"]) file_response.raise_for_status() with open("output.csv", "wb") as f: f.write(file_response.content) print(f"Downloaded {len(file_response.content)} bytes") ``` ### Response ```json { "url": "https://storage.googleapis.com/baponi-prod-storage/orgs/org_abc/threads/my-thread-123/output.csv?X-Goog-Algorithm=...", "method": "GET", "expires_at": "2026-03-04T12:15:00Z" } ``` ## Delete a file Permanently deletes a file from a thread or volume. This action cannot be undone. Attempting to delete from a read-only volume returns `403 Forbidden`. ### Request parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `source` | string | Yes | `"thread"` or `"volume"` | | `id` | string | Yes | Thread ID or volume slug | | `path` | string | Yes | File path to delete | ### Response fields | Field | Type | Description | |-------|------|-------------| | `deleted` | boolean | `true` if the file was deleted | ### Examples ```bash curl -X DELETE https://api.baponi.ai/v1/files \ -H "Authorization: Bearer $BAPONI_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "source": "thread", "id": "my-thread-123", "path": "output.csv" }' ``` ```python response = requests.delete( "https://api.baponi.ai/v1/files", headers={"Authorization": f"Bearer {api_key}"}, json={ "source": "thread", "id": "my-thread-123", "path": "output.csv", }, ) result = response.json() print(f"Deleted: {result['deleted']}") ``` ### Response ```json { "deleted": true } ``` ## Error responses All Files API endpoints return errors in the standard Baponi format. See the [API Overview](/docs/api/overview) for the full error response structure. | Status | Error Code | When | |--------|------------|------| | `400` | `validation_error` | Missing required parameters, invalid `source` value, or `content_length` exceeds 10 GiB | | `401` | `unauthorized` | Missing or invalid API key | | `403` | `forbidden` | Attempting to write to or delete from a read-only volume | | `404` | `not_found` | Thread, volume, or file path does not exist | | `429` | `rate_limited` | Too many requests. See [rate limiting](/docs/api/overview#rate-limiting-429). | ### Example error ```json { "error": "validation_error", "message": "content_length exceeds maximum of 10737418240 bytes (10 GiB)" } ``` ## Complete workflow: upload, execute, download This Python example demonstrates a full lifecycle: upload an input file, execute code that reads it, then download the output. ```python API_KEY = "sk-us-YOUR_API_KEY" BASE_URL = "https://api.baponi.ai" HEADERS = {"Authorization": f"Bearer {API_KEY}"} THREAD_ID = "analysis-run-42" # --- Step 1: Upload input data to the thread --- input_data = b'{"values": [1, 2, 3, 4, 5]}' upload_resp = requests.post( f"{BASE_URL}/v1/files/upload_url", headers=HEADERS, json={ "source": "thread", "id": THREAD_ID, "path": "input.json", "content_type": "application/json", "content_length": len(input_data), }, ).json() requests.put( upload_resp["url"], headers=upload_resp["headers"], data=input_data, ).raise_for_status() print("Uploaded input.json") # --- Step 2: Execute code that reads input and writes output --- exec_resp = requests.post( f"{BASE_URL}/v1/sandbox/execute", headers={**HEADERS, "Content-Type": "application/json"}, json={ "code": """ with open('/home/baponi/input.json') as f: data = json.load(f) result = {"sum": sum(data["values"]), "count": len(data["values"])} with open('/home/baponi/output.json', 'w') as f: json.dump(result, f) print(f"Processed {result['count']} values, sum = {result['sum']}") """, "language": "python", "thread_id": THREAD_ID, }, ).json() print(f"Execution stdout: {exec_resp['stdout']}") # --- Step 3: Download the output --- download_resp = requests.post( f"{BASE_URL}/v1/files/download_url", headers=HEADERS, json={ "source": "thread", "id": THREAD_ID, "path": "output.json", }, ).json() output = requests.get(download_resp["url"]).json() print(f"Result: {output}") # Result: {"sum": 15, "count": 5} # --- Step 4: Verify files are persisted --- list_resp = requests.post( f"{BASE_URL}/v1/files/list", headers=HEADERS, json={"source": "thread", "id": THREAD_ID}, ).json() for f in list_resp["files"]: print(f" {f['path']} ({f['size']} bytes)") # input.json (27 bytes) # output.json (25 bytes) ``` ## Signed URL architecture Baponi uses pre-signed URLs for all file transfers. This design has three benefits: 1. **No data through Baponi servers.** File bytes travel directly between your client and the storage backend (GCS, S3, or Azure). Baponi only generates the signed URL, which is a lightweight API call. 2. **10 GiB file support.** Because files stream directly to cloud storage, there is no proxy payload limit. Upload files up to 10 GiB per request. 3. **Enterprise data residency.** Self-hosted Baponi deployments generate signed URLs for the customer's own storage buckets. Data never leaves the customer's cloud account. Signed URLs expire after 15 minutes. If an upload or download is interrupted, request a new URL and retry. Partial uploads do not consume storage. Cloud storage providers discard incomplete uploads automatically.