Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/git-sandbox-network.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"sql-fs-api": major
---

Add a sandbox `git` command backed by just-git, export server `GITHUB_TOKEN` into sandbox GitHub-compatible Git/curl env, and let MCP-created sandboxes request network access for clone/fetch/push.
15 changes: 15 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,21 @@ SESSION_IDLE_MS=600000 # evict idle sessions after 10 min
MAX_CONCURRENT_PYTHON=5 # cap CPython WASM workers (~80 MB each)
MAX_CONCURRENT_JS=5 # cap QuickJS workers (~64 MB each)

# ── Optional: sandbox git/GitHub defaults ─────────────────────────────────────

# Shared deployment-wide GitHub identity exported into network:true sandboxes
# as GITHUB_TOKEN (for curl GitHub API calls), plus GIT_HTTP_USER=x-access-token
# and GIT_HTTP_PASSWORD=<token> for git HTTPS auth. Readable by network-enabled
# sandbox code; use only with trusted agents.
# GITHUB_TOKEN=

# Optional commit identity defaults exported into every sandbox. Per-request env
# overrides these values for that exec.
# GIT_AUTHOR_NAME=
# GIT_AUTHOR_EMAIL=
# GIT_COMMITTER_NAME=
# GIT_COMMITTER_EMAIL=

# ── Optional: Redis (required for multi-replica deployments) ──────────────────

# REDIS_URL=redis://localhost:6379
Expand Down
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,8 @@ const TABLE = Object.assign(Object.create(null) as Record<string, string>, {
| `SESSION_IDLE_MS` | No (default: 600000) | Idle timeout before session eviction (ms) |
| `MAX_CONCURRENT_PYTHON` | No (default: 5) | Max concurrent Python executions across all sessions. CPython WASM workers cost ~80MB each (EXIT_RUNTIME per invocation); the semaphore caps concurrency to prevent OOM. Excess scripts queue FIFO. |
| `MAX_CONCURRENT_JS` | No (default: 5) | Max concurrent JavaScript (`js-exec`/`node`) executions across all sessions. QuickJS executions cap at 64MB each. Excess scripts queue FIFO. Note: just-bash currently serializes `js-exec` internally through a single worker, so this cap is an upper bound that may not be binding today. |
| `GITHUB_TOKEN` | No | Optional shared GitHub token. When set, exported into `network:true` sandbox shell env as `GITHUB_TOKEN` for `curl` GitHub API calls, plus `GIT_HTTP_USER=x-access-token` and `GIT_HTTP_PASSWORD=<token>` for GitHub-compatible `git` HTTPS auth. This is a deployment-wide identity readable by network-enabled sandbox code; use only with trusted agents. Per-request `env` overrides it. |
| `GIT_AUTHOR_NAME`, `GIT_AUTHOR_EMAIL`, `GIT_COMMITTER_NAME`, `GIT_COMMITTER_EMAIL` | No | Optional git identity values to export into every sandbox shell env so `git commit` has defaults. Per-request `env` overrides them. |
| `REDIS_URL` | No | Redis connection string. Required for multi-replica deployments. When absent, distributed exec lock and all Redis caches are disabled — only in-process `session.mutex` protects execution. |
| `REDIS_EXEC_LOCK_LEASE_MS` | No (default: 60000) | Distributed exec lock lease duration (ms). Lock auto-expires if the holder dies. Must be > `REDIS_EXEC_LOCK_RENEW_MS`. |
| `REDIS_EXEC_LOCK_RENEW_MS` | No (default: 20000) | Heartbeat interval for exec lock renewal (ms). Must be strictly less than `REDIS_EXEC_LOCK_LEASE_MS` to guarantee renewal fires before expiry. |
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"ioredis": "^5.4.0",
"jose": "^6.0.0",
"just-bash": "^3.0.1",
"just-git": "^1.7.1",
"lru-cache": "^11.0.0",
"mssql": "^11.0.0",
"mysql2": "^3.12.0",
Expand Down
16 changes: 14 additions & 2 deletions plugins/sql-fs/skills/api/ref/bash.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,16 +75,28 @@ js-exec script.js
node script.js # alias for js-exec
```
- QuickJS WASM — fast startup, TypeScript supported
- No `npm`, no network, no `require('fs')` (use sandbox FS via bash instead)
- No `npm`, no `require('fs')` (use sandbox FS via bash instead)
- Server-wide concurrency cap: **5 concurrent js-exec calls**

### Network — `network: true`

Network access is opt-in at sandbox creation. With `network: true`:

- `curl` can reach outbound HTTPS endpoints
- `git clone`, `git fetch`, and `git push` can reach HTTPS remotes
- `js-exec` `fetch()` can reach outbound HTTP(S) when `javascript: true` is also set

With `network: false` (the default), outbound access is blocked. Local git
operations such as `git init`, `git add`, `git commit`, `git status`, and
`git log` still work.

---

## NOT supported

| Command | Why |
|---------|-----|
| `curl`, `wget`, `nc`, `ssh` | No network access of any kind |
| `wget`, `nc`, `ssh` | Unsupported network tools/transports |
| `apt`, `pip`, `npm`, `brew` | No package managers |
| `vi`, `vim`, `nano`, `less`, `more` | No interactive/terminal-control commands |
| `&` (background jobs) | No process control |
Expand Down
19 changes: 12 additions & 7 deletions plugins/sql-fs/skills/api/ref/endpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ All body fields are **optional**:
|---|---|---|---|
| `python` | boolean | false | Enable CPython WASM — registers `python3` (and `python` alias), stdlib only, isolated per call |
| `javascript` | boolean | false | Enable QuickJS WASM (`js-exec`/`node`) |
| `network` | boolean | false | Enable outbound `fetch()` from `js-exec` (see note below) |
| `network` | boolean | false | Enable outbound HTTP for `js-exec` fetch, Bash `curl`, and git clone/fetch/push (see note below) |
| `files` | `Record<absPath, string>` | — | Seed files (absolute path → plain text) |
| `env` | `Record<string, string>` | — | Default env vars for all exec calls |

Expand All @@ -147,13 +147,18 @@ Response `201`:

**Important:** `python`/`javascript`/`network` must be set at creation. They cannot be changed later.

**`network: true` — outbound fetch() from js-exec**
**`network: true` — outbound HTTP**

When `network: true` is set (requires `javascript: true`), `fetch()` inside
`js-exec` scripts can reach external HTTP endpoints. The js-exec timeout
extends to 60 s automatically. The Bash shell itself remains air-gapped — no
`curl`, `wget`, DNS, or raw socket access — only `fetch()` inside `js-exec`
gains outbound HTTP. Defaults to `false` (secure-by-default, no egress).
When `network: true` is set at sandbox creation, supported commands can reach
external HTTPS endpoints:

- `fetch()` inside `js-exec` scripts when `javascript: true` is also set
- Bash `curl`
- `git clone`, `git fetch`, and `git push`

The js-exec timeout extends to 60 s automatically. `wget`, raw sockets,
package managers, compilers, and SSH remain unsupported. Defaults to `false`
(secure-by-default, no egress); local git operations still work without network.

### GET /v1/sandboxes/:id — Get sandbox info

Expand Down
20 changes: 12 additions & 8 deletions plugins/sql-fs/skills/py-sdk/ref/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,17 +117,21 @@ sb = fs.sandboxes.create(
files={"/home/user/seed.txt": "..."}, # text-only seed (use ingest_files for many/binary)
python=False, # enable CPython WASM runtime
javascript=False, # enable QuickJS runtime
network=False, # enable outbound fetch() from js-exec (opt-in)
network=False, # enable outbound HTTP/curl/git remote ops (opt-in)
)
```

All keyword args are optional — `fs.sandboxes.create()` is valid and creates
an anonymous sandbox.

**`network=True` — enabling outbound fetch()**
**`network=True` — enabling outbound HTTP**

Pass `network=True` together with `javascript=True` to allow `fetch()` calls
inside `js-exec` scripts to reach external HTTP endpoints:
Pass `network=True` at sandbox creation to allow outbound HTTP from supported
commands. It enables:

- `fetch()` inside `js-exec` scripts when `javascript=True` is also set
- Bash `curl`
- `git clone`, `git fetch`, and `git push`

```python
sb = fs.sandboxes.create(javascript=True, network=True)
Expand All @@ -139,11 +143,11 @@ r = sb.exec("""js-exec -c '
print(r.stdout) # origin: <your-ip>
```

- **Bash remains air-gapped.** Even with `network=True`, the Bash shell has
no `curl`, `wget`, DNS, or raw socket access. Only `fetch()` inside `js-exec`
gains outbound HTTP.
- **Bash gains HTTP tools.** With `network=True`, `curl` is available and git
remote operations can reach HTTPS remotes. `wget`, raw sockets, package
managers, compilers, and SSH remain unsupported.
- **Opt-in, default `False`.** Omitting `network` (or passing `network=False`)
produces a fully isolated sandbox.
blocks outbound access; local git operations still work.
- **js-exec timeout extends to 60 s** when network is enabled (documented in
the `node` alias help text).

Expand Down
20 changes: 12 additions & 8 deletions plugins/sql-fs/skills/ts-sdk/ref/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,17 +113,21 @@ const sb = await client.sandboxes.create({
files: { "/home/user/seed.txt": "..." }, // text-only seed (use ingestFiles for many/binary)
python: false, // enable CPython WASM runtime
javascript: false, // enable QuickJS runtime
network: false, // enable outbound fetch() from js-exec (opt-in)
network: false, // enable outbound HTTP/curl/git remote ops (opt-in)
});
```

All options are optional — `client.sandboxes.create()` is valid and creates
an anonymous sandbox.

**`network: true` — enabling outbound fetch()**
**`network: true` — enabling outbound HTTP**

Pass `network: true` together with `javascript: true` to allow `fetch()` calls
inside `js-exec` scripts to reach external HTTP endpoints:
Pass `network: true` at sandbox creation to allow outbound HTTP from supported
commands. It enables:

- `fetch()` inside `js-exec` scripts when `javascript: true` is also set
- Bash `curl`
- `git clone`, `git fetch`, and `git push`

```typescript
const sb = await client.sandboxes.create({ javascript: true, network: true });
Expand All @@ -135,11 +139,11 @@ const r = await sb.exec(`js-exec -c '
console.log(r.stdout); // origin: <your-ip>
```

- **Bash remains air-gapped.** Even with `network: true`, the Bash shell has
no `curl`, `wget`, DNS, or raw socket access. Only `fetch()` inside `js-exec`
gains outbound HTTP.
- **Bash gains HTTP tools.** With `network: true`, `curl` is available and git
remote operations can reach HTTPS remotes. `wget`, raw sockets, package
managers, compilers, and SSH remain unsupported.
- **Opt-in, default `false`.** Omitting `network` (or passing `network: false`)
produces a fully isolated sandbox.
blocks outbound access; local git operations still work.
- **js-exec timeout extends to 60 s** when network is enabled.

### `client.sandboxes.get(sandboxId): Promise<SandboxInfo>`
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 18 additions & 6 deletions src/api/mcp/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,20 @@ const MAX_EXPORT_CONCURRENCY = positiveIntEnv(process.env.MAX_EXPORT_CONCURRENCY
export function registerTools(server: McpServer, sessionManager: SessionManager, owner: string, tenant: string): void {
server.tool(
"sandbox_create",
"Create an isolated bash sandbox with a virtual filesystem. Optional runtime flags opt in to python3/python (CPython WASM, stdlib only) and js-exec/node (QuickJS WASM) commands. Optional name for human-readable identification.",
"Create an isolated bash sandbox with a virtual filesystem. Optional runtime flags opt in to python3/python (CPython WASM, stdlib only), js-exec/node (QuickJS WASM), and outbound HTTPS for curl plus git clone/fetch/push. Optional name for human-readable identification.",
{
name: z.string().max(255).optional().describe("Human-readable name for the sandbox"),
python: z.boolean().optional(),
javascript: z.boolean().optional(),
network: z.boolean().optional().describe("Grant outbound HTTPS (enables curl + git clone/fetch/push)"),
},
async (args) => {
const id = randomUUID();
const name = args.name ?? null;
const runtimeOptions = {
python: args.python ?? false,
javascript: args.javascript ?? false,
network: false,
network: args.network ?? false,
};
try {
const session = await sessionManager.getOrCreate(tenant, id, runtimeOptions, owner);
Expand All @@ -59,7 +60,13 @@ export function registerTools(server: McpServer, sessionManager: SessionManager,
content: [
{
type: "text" as const,
text: JSON.stringify({ id, name, python: runtimeOptions.python, javascript: runtimeOptions.javascript }),
text: JSON.stringify({
id,
name,
python: runtimeOptions.python,
javascript: runtimeOptions.javascript,
network: runtimeOptions.network,
}),
},
],
};
Expand Down Expand Up @@ -97,6 +104,7 @@ export function registerTools(server: McpServer, sessionManager: SessionManager,
createdAt: s.createdAt.toISOString(),
python: s.python,
javascript: s.javascript,
network: s.network,
})),
}),
},
Expand Down Expand Up @@ -167,16 +175,20 @@ export function registerTools(server: McpServer, sessionManager: SessionManager,
"environment variables, conditionals (if/else), loops (for/while), functions, arithmetic,",
"base64, md5sum, sha256sum, tar, gzip, jq, yq, xan, sqlite3.",
"",
"NOT supported: curl/wget (no network), apt/pip/npm (no package managers),",
"Network: sandboxes are air-gapped by default. If created with network:true,",
"curl and outbound git clone/fetch/push over HTTPS are available. wget is not supported.",
"",
"NOT supported: apt/pip/npm (no package managers),",
"vi/vim/nano (no interactive), background jobs (&), kill/ps/top (no process control),",
"/proc /sys /dev (no special filesystems), ln -s (symlinks off by default),",
"gcc/make/rustc (no compilers), network access of any kind.",
"gcc/make/rustc (no compilers).",
"",
"Optional runtimes (only if sandbox was created with python:true or javascript:true):",
"- python3 / python — CPython WASM, stdlib only (no pip, no network, no os.system).",
" Concurrent python3 executions across the server are capped to prevent OOM; excess",
" scripts queue until a slot frees.",
"- js-exec / node — QuickJS WASM. TypeScript supported. No npm, no network.",
"- js-exec / node — QuickJS WASM. TypeScript supported. No npm. fetch is available",
" only when the sandbox was created with network:true.",
].join("\n");

server.tool(
Expand Down
11 changes: 9 additions & 2 deletions src/api/openapi-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ const sandboxSchema = {
createdAt: { type: "string", format: "date-time" },
python: { type: "boolean", example: false },
javascript: { type: "boolean", example: false },
network: { type: "boolean", example: false },
},
required: ["id", "name", "owner", "createdAt", "python", "javascript"],
required: ["id", "name", "owner", "createdAt", "python", "javascript", "network"],
} as const;

const sandboxInfoSchema = {
Expand Down Expand Up @@ -295,7 +296,8 @@ export const openapiSpec = {
post: {
tags: ["Sandboxes"],
summary: "Create sandbox",
description: "Create a new isolated bash sandbox. Optionally seed with files and enable Python/JS runtimes.",
description:
"Create a new isolated bash sandbox. Optionally seed with files and enable Python/JS runtimes or outbound HTTPS for curl and git clone/fetch/push.",
requestBody: {
required: false,
content: {
Expand All @@ -320,6 +322,11 @@ export const openapiSpec = {
},
python: { type: "boolean", default: false, description: "Enable CPython WASM runtime" },
javascript: { type: "boolean", default: false, description: "Enable QuickJS runtime" },
network: {
type: "boolean",
default: false,
description: "Grant outbound HTTPS for curl and git clone/fetch/push",
},
},
},
},
Expand Down
Loading
Loading