Run untrusted code. Trust the runtime.
- Node.js 20+
- Docker Desktop running
- pnpm installed globally
- Clone repo
pnpm install(from root — installs backend deps)cd web && pnpm install(installs frontend deps)cp .env.example .env→ fill in valuespnpm docker:up→ starts Postgres + Redispnpm docker:images→ builds srai-node and srai-python sandbox imagespnpm db:migrate→ runs DB migrationpnpm db:seed→ prints your dev API key, paste into .env
Terminal 1: pnpm dev (API server on :3001)
Terminal 2: pnpm dev:worker (BullMQ worker)
Terminal 3: cd web && pnpm dev (Next.js on :3000)
curl -X POST http://localhost:3001/execute \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"language":"python","code":"print(\"hello from sandbox\")"}'→ returns { jobId }
curl http://localhost:3001/job/JOB_ID \
-H "Authorization: Bearer YOUR_KEY"→ returns { status: "success", stdout: "hello from sandbox\n", ... }
flowchart TD
A[Next.js Dashboard<br/>Execution History • Logs • Metrics]
B[Express API Server<br/>REST • WebSocket • MCP Endpoints]
C[Redis<br/>Queue • Pub/Sub • Session Cache]
D[BullMQ Worker Pool<br/>Job Execution Layer]
E[Docker Sandbox Layer<br/>Node • Python • Go Runtimes]
F[PostgreSQL<br/>Executions • Sessions • Telemetry]
A <-->|HTTP / WebSocket| B
B -->|Queue Jobs| C
C --> D
D -->|Runs Code in Containers| E
E -->|Execution Results| D
B <-->|Pub/Sub Streaming| C
B -->|Store Logs & Metadata| F
D -->|Execution Logs| F
To provide a fast, responsive interface for AI agents and users, SRAI replaces polling with active real-time WebSocket streaming.
- Client Connection: When an execution is initiated, the web client opens a WebSocket connection to
ws://localhost:3001/stream?jobId=JOB_ID. - Worker Streaming: The BullMQ worker running in a separate process starts the Docker container and demultiplexes its stdout/stderr streams.
- Redis Pub/Sub: As chunks are generated, the worker publishes them to a Redis channel dedicated to the job (
execution:{jobId}). - Broadcast: The API server receives the pub/sub events and immediately forwards them to all connected WebSockets for that job.
Persistent sessions allow subsequent execution jobs to access the same directory state (e.g., writing a file in run 1 and reading/modifying it in run 2).
- Docker Volumes: Creating a session creates a named Docker volume (
srai-session-CUID). - Read-Write Mounts: During execution, if a
sessionIdis provided, the API server retrieves the volume and the worker mounts it to the container at/sessionasrw(read-write), while/sandbox(containing the user code) remainsro(read-only) for safety. - File Explorer: Files within a session's volume can be inspected using the file explorer API (
GET /session/:id/files), which spins up an ephemeral helper container to safely list files and return their metadata. - Cleanup Cron: A background cron job runs every hour, detecting expired sessions in the database and purging their corresponding Docker volumes automatically.
Because the API server and the BullMQ worker run as separate processes (and potentially separate machines in production). The API server maintains the WebSocket connection with the client, but only the worker is directly attached to the container's output streams. Redis Pub/Sub serves as a high-performance, lightweight message broker that bridges the gap between these isolated processes.
Docker Volumes provide a platform-independent abstraction managed entirely by the Docker daemon. If we used host-bound folders, we would have to manage platform-specific paths (e.g., Windows vs Linux paths), handle complex file permission mappings, and risk exposing host file structures. Docker Volumes are secure, isolated, easy to clean up programmatically via Dockerode, and perform highly under heavy disk I/O.
By default, official images like node:alpine and python:slim assign different UID/GID numbers to their respective non-root users. When a single Docker Volume is mounted to containers of different languages, files written by one container may be inaccessible (Permission Denied) to another due to differing owner IDs. Standardizing the sandbox user to UID/GID 2000:2000 across all runtime Dockerfiles solves this permission mismatch natively.
4. Why spin up an ephemeral container to list files in a session's volume instead of reading the volume directly from the host filesystem?
Docker volumes are stored in internal directories managed by the Docker daemon (e.g., /var/lib/docker/volumes on Linux), which are inaccessible or extremely complex to locate on Windows (WSL2 VM) and macOS (Hyperkit/Virtualization framework). Running a short-lived, lightweight container (like Alpine) bound to the volume and running find /data -type f provides a reliable, secure, and cross-platform way to extract file lists.
By default, Python buffers stdout when writing to non-interactive streams (like Docker's stdout pipe). This causes all print output to be held in memory and printed only when the buffer fills or the script ends. However, subprocess commands (like os.system) are executed immediately. This mismatch results in out-of-order logs (e.g. system commands executing before previous print outputs are flushed). Running Python with the -u flag disables output buffering, guaranteeing chronological log ordering in the live terminal feed.
- Phase 5 — MCP-Compatible API
- Phase 6 — Resource Telemetry
- Phase 7 — Full Dashboard
- Phase 8 — Kubernetes Deployment
| System | Status |
|---|---|
| REST API Layer | ✅ Completed |
| WebSocket Streaming | ✅ Completed |
| Docker Sandbox Runtime | ✅ Completed |
| Persistent Sessions | ✅ Completed |
| Redis Queue & Pub/Sub | ✅ Completed |
| PostgreSQL Persistence | ✅ Completed |
| Real-Time Terminal UI | ✅ Completed |
| Runtime Package Installation | ✅ Completed |
| MCP Integration | ⏳ Planned |
| Analytics & Telemetry | ⏳ Planned |
| Kubernetes Deployment | ⏳ Planned |