Files & Exec
The Files and Exec APIs are how you (or an AI agent) interact with a Sandbox. Together they’re the substrate for everything else: generating code, running builds, starting servers.
Files
Write
Parent directories are created as needed. Existing files are overwritten.
Read
const content = await sandbox.files.read("/workspace/package.json")
const parsed = JSON.parse(content) curl https://api.miosa.ai/api/v1/sandboxes/$SBX/files/read?path=/workspace/package.json
-H "Authorization: Bearer $MIOSA_API_KEY" List
const entries = await sandbox.files.list("/workspace")
for (const e of entries) {
console.log(e.type, e.path, e.size)
} curl https://api.miosa.ai/api/v1/sandboxes/$SBX/files/list?path=/workspace
-H "Authorization: Bearer $MIOSA_API_KEY" Watch
Stream filesystem events:
for await (const event of sandbox.files.watch("/workspace")) {
console.log(event.type, event.path)
// event.type: "create" | "modify" | "delete"
} Watch is implemented as Server-Sent Events. See Events for the SSE event shape.
Filesystem layout
| Path | Purpose |
|---|---|
/workspace | Working directory; this is where agents write app code |
/home/sandbox | User home |
/tmp | Temporary scratch (cleared on sandbox destroy) |
/opt/venv | Pre-installed Python virtualenv (miosa-sandbox and Python templates) |
/usr/local/bin | System binaries |
Generally, write inside /workspace and let your build tooling handle node_modules, virtualenvs, etc. Production publish freezes /workspace (excluding cache dirs) as the source snapshot — see Publishing.
Exec
Run commands inside the sandbox.
One-shot
Blocks until the command exits (or timeoutMs elapses). Returns exit_code, stdout, stderr, and duration_ms.
Detached (long-running servers)
await sandbox.exec({
cmd: "node",
args: ["server.js"],
cwd: "/workspace",
detached: true,
}) Returns immediately with a job_id. The server keeps running. Use Previews to expose its port.
Streaming
For real-time stdout/stderr (e.g. tail an agent’s compile output):
for await (const chunk of sandbox.exec.stream({
cmd: "pnpm",
args: ["dev"],
})) {
if (chunk.type === "stdout") process.stdout.write(chunk.data)
if (chunk.type === "stderr") process.stderr.write(chunk.data)
if (chunk.type === "exit") console.log("exit:", chunk.exit_code)
} Implemented as SSE. See Streaming Exec.
Terminal
Need an interactive PTY (for remote dev or AI agents driving CLIs)?
const terminal = await sandbox.terminal()
terminal.send("ls /workspace\n")
for await (const out of terminal.output) {
process.stdout.write(out)
} Terminals are WebSocket-based with xterm.js-compatible ANSI passthrough. See API Reference: Terminal for protocol details.
Process management
For detached exec jobs:
const job = await sandbox.exec({ cmd: "npm", args: ["start"], detached: true })
await sandbox.jobs.get(job.id) // current state
await sandbox.jobs.stop(job.id) // SIGTERM, then SIGKILL after grace
await sandbox.jobs.list() // all detached jobs Detached jobs survive until you explicitly stop them or the sandbox is destroyed.
Idempotency
Exec is not idempotent — running npm install twice runs it twice. If you need idempotent retries, store a marker file:
const marker = "/workspace/.deps-installed"
const exists = await sandbox.files.exists(marker)
if (!exists) {
await sandbox.exec({ cmd: "npm", args: ["ci"] })
await sandbox.files.write(marker, new Date().toISOString())
} File writes are idempotent — same path, same content, same result.
Limits
| Limit | Default | Tunable |
|---|---|---|
| Max file size per write | 100 MB | Yes (plan-dependent) |
| Max files per sandbox | 1,000,000 | Yes |
| Max exec stdout buffer | 10 MB | Stream mode bypasses this |
| Max exec duration | timeout_ms parameter, default 30s | Yes; detached has no limit |
| Max concurrent detached jobs | 100 | Yes |
See also
- Sandboxes — the VM you’re acting on
- Previews — expose ports started via exec
- API Reference: Streaming Exec — SSE wire format
- API Reference: Files — file API wire format