Sandbox & Security
Workflow steps that execute arbitrary code or shell commands — the code node and the agent's terminal/write_file/read_file/list_files tools — never run on the API server or worker host. They run inside a dedicated, isolated Docker container per execution, managed by internal/sandbox (sandbox.Manager).
Lifecycle
- Lazy creation — the first time an execution needs the sandbox,
ensureSandbox(ininternal/tools/terminal.go) callsManager.Create, which starts a container from thegoagents-sandbox:latestimage withsleep infinityas its entrypoint and a working directory of/workspace. - Reuse within an execution — the container is tracked in-memory by execution ID (
sandboxes map[string]string), so everycode/terminal/file-tool call within the same workflow execution reuses the same container and shares its/workspacefilesystem — useful for multi-step pipelines (e.g. download a file, then process it, then read the result). - Teardown —
DestroyExecutionstops and force-removes the container once the workflow completes (CompleteWorkflowActivity).CleanupStaleruns on worker startup and removes any containers left over from a previous crash, identified by thegoagents.execution_idlabel.
Isolation
Each container is created with:
| Setting | Value | Effect |
|---|---|---|
| Image | goagents-sandbox:latest | A purpose-built image (build it via the sandbox Makefile target) — not the worker's own filesystem. |
| Memory | 512 MB | Hard memory limit; OOM kills the container, not the worker process. |
| CPU quota | 100000 (1 full CPU) | Caps CPU usage per container. |
| PIDs limit | 256 | Prevents fork-bombs from exhausting host resources. |
| Network mode | bridge | Outbound internet access (for pip install, curl, etc.) but no access to the host network namespace. |
| Labels | goagents.execution_id, goagents.tenant_id | Used for tracking and stale-container cleanup; also makes containers attributable per tenant for auditing. |
:::info Per-plan limits
internal/billing/plans.go defines sandbox CPU/memory ceilings per plan tier — Enterprise deployments can raise the per-container limits beyond the defaults above.
:::
Command execution
Manager.Exec / ExecStreaming run bash -c "<command>" inside the container via the Docker exec API:
- Timeout: every command has a timeout (default 30s, capped at 300s/5 minutes). On timeout, the partial output is returned with a
[TIMEOUT: command exceeded time limit]marker rather than hanging the workflow indefinitely. - Output limit: stdout and stderr are each capped at 1 MB (
MaxOutputBytes) — additional output is silently discarded so a runaway command can't blow up step output storage. - Streaming:
ExecStreamingcalls aprogressFncallback for each output chunk, used to stream live terminal output to the UI via SSE during long-running agent/code steps.
File access
WriteFile/ReadFile/ListFiles operate purely through bash -c commands (cat > file << EOF, cat file, find /workspace -type f | head -100) inside the container — there's no direct host bind-mount, so the only way in or out of the sandbox filesystem is through these tool calls or the workspace export step below.
Workspace export
When an execution finishes, ExportWorkspace uses docker cp (CopyFromContainer) to tar up /workspace, then extracts each file (up to 50 MB each) and uploads it to workspace storage (S3/MinIO), making files created by code/terminal steps (reports, generated images, processed CSVs, etc.) downloadable from the execution detail page. See Executions & Monitoring — Workspace files.
Other security measures
- Secrets & OAuth tokens are encrypted at rest with AES-256-GCM, keyed from
JWT_SECRET(see Secrets & Credentials) — they're decrypted only at the point of use and are redacted from execution step input/output before being returned over the API. - Temporal payload encryption — set
TEMPORAL_ENCRYPTION_KEYto encrypt step inputs/outputs as stored by Temporal (see Temporal Execution Model). - Tenant isolation — every database query is scoped by
tenant_id; see Multi-Tenancy & Data Model. - Public routes (
/webhooks/{id},/run/{id}) are gated by either an unguessable workflow UUID, a workflow-scoped API key, or both.