Project home: https://safeagent.ca
This repository is a reference scaffold for running an OpenClaw-style coding agent inside a disposable sandbox with constrained networking, a simple policy check, and audit logging.
It is intended for people who want to experiment with coding agents more safely than running them directly on a host machine or a valuable working copy. It is not a production-ready security boundary, but it is a practical starting point for reducing blast radius during testing and exploration.
safeagent.ca currently redirects here so the project is easier to share and reference.
docker-compose.yml-- control plane stack for the sample APIcontrol-plane/app.py-- minimal FastAPI control planecontrol-plane/requirements.txt-- Python dependenciescontrol-plane/Dockerfile-- control plane image buildsandbox-images/Dockerfile-- non-root sandbox image with a small toolsetconfigs/policy.yaml-- inspect-only default policyconfigs/policy.execution.yaml-- opt-in policy for repo-defined test and build executionconfigs/nftables-agent.conf-- host-side egress examplescripts/launch-sandbox.sh-- disposable sandbox launcherscripts/apply-nftables.sh-- helper to load nftables rules.env.example-- basic environment variables
User/Job -> Control Plane -> Disposable Sandbox -> Result
With separate audit logging for every tool request and policy decision.
The default layout aims to reduce blast radius by using:
- one sandbox per job
- non-root sandbox user
- read-only container root filesystem
- writable workspace only
- no privileged mode
- no Docker socket in the sandbox
- host-enforced egress filtering
- no long-lived secrets in the sandbox
- approval required for risky commands
- It provides a safer place to experiment than running an agent directly on a primary machine or a non-disposable repository.
- It shows how to separate a control plane from a disposable job sandbox.
- It demonstrates a simple pattern for running one sandbox per job with a non-root user, a read-only root filesystem, and a writable workspace.
- It includes an inspect-only default policy and a separate opt-in execution policy so readers can choose a safer starting point.
- It validates the source repository before copying it into the job workspace and rejects symlinks or other special file types by default.
- It shows how outbound access can be narrowed with Docker network profiles and host-side
nftablesrules. - It records policy decisions, approval requests, approval decisions, sandbox launches, and command output in an audit log, which makes the flow easier to inspect and test.
- It is small enough to read end to end, which makes it useful for experimentation, review, and adaptation.
- It does not provide high-assurance isolation.
- It does not prevent repo-defined commands such as
pytest,make test, or similar build and test commands from executing arbitrary code inside the sandbox. - It does not replace careful host firewalling, credential handling, or repo-specific policy design.
- It does not remove the need for a stronger runtime, such as a microVM, if the threat model includes a determined rogue agent.
In short: this repository is useful for cautious experimentation and learning. It is meaningfully safer than running an agent directly on a host, but it is not a complete defence against a determined rogue agent.
The default policy in configs/policy.yaml is inspect-only. It allows read and inspection commands such as ls, find, cat, grep, git status, and git diff, but it does not allow repo-defined test or build commands to run automatically.
If you want to allow commands such as pytest, python3 -m pytest, or make test, set POLICY_PATH=/app/configs/policy.execution.yaml before starting the stack. That policy is explicitly less conservative because those commands can execute arbitrary code from the repository being analysed.
The sandbox image included here provides a small base toolset, including pytest and make. If you want to run other language-specific toolchains, extend sandbox-images/Dockerfile and update the policy to match.
The control plane enforces the policy's path rules for a subset of file-oriented commands, truncates API and audit output to max_output_kb per stream, and records the number of changed workspace files. If max_file_writes is exceeded, the job is marked as failed after execution and the audit log records the policy violation.
Before a job workspace is created, the control plane validates the source repository tree. By default it rejects symlinks and special file types such as device nodes, sockets, and named pipes. This keeps the copied workspace simpler and reduces the chance that a repository can smuggle unexpected filesystem behaviour into the sandbox.
If a repository fails this validation, the job is rejected and the reason is written to the audit log.
When a command matches require_approval, the control plane creates a pending approval record instead of running the job immediately. The approval record stores the reviewed command, the repository path, and the requested network profile.
The minimal approval API is:
GET /approvals/{approval_id}to inspect a pending or completed approval recordPOST /approvals/{approval_id}/approveto approve and execute the exact reviewed commandPOST /approvals/{approval_id}/denyto reject the request without running it
Approval records are stored on disk under APPROVALS_ROOT. This is intentionally simple and easy to inspect, not a full multi-user approval system.
The approval endpoints require an X-Approval-Token header that matches APPROVAL_TOKEN. This is intentionally minimal and suitable for local experimentation, but it is still not a full production approval system.
- Copy
.env.exampleto.envand adjust values. SetAPPROVAL_TOKENto a non-default value before exposing the API beyond a local test environment. - Review
configs/policy.yamland keep it as the default unless you deliberately want to allow repo-defined test or build execution. - Review
configs/nftables-agent.confand replace placeholder IPs and interfaces. - Create a source repository inside the mounted workspace root so it is visible both to the control plane container and to the host Docker daemon:
mkdir -p ./workspaces/example-repo
printf 'hello\n' > ./workspaces/example-repo/hello.txt
printf 'delete me\n' > ./workspaces/example-repo/delete-me.txt- Build the sandbox image:
docker build -t agent-safe-sandbox:latest ./sandbox-images- Build and start the stack:
docker compose up --build -d- Apply host firewall rules from the host, not from inside a container:
sudo ./scripts/apply-nftables.sh- Submit an inspect-only job to the control plane:
curl -X POST http://localhost:8080/jobs \
-H 'Content-Type: application/json' \
-d '{
"repo_path": "/opt/agent-stack/workspaces/example-repo",
"command": "ls",
"network_profile": "none"
}'- Submit a command that requires approval:
curl -X POST http://localhost:8080/jobs \
-H 'Content-Type: application/json' \
-d '{
"repo_path": "/opt/agent-stack/workspaces/example-repo",
"command": "rm delete-me.txt",
"network_profile": "none"
}'- Inspect the approval request:
curl http://localhost:8080/approvals/<approval_id> \
-H 'X-Approval-Token: change-me-approval-token'- Approve the request and run the reviewed command:
curl -X POST http://localhost:8080/approvals/<approval_id>/approve \
-H 'Content-Type: application/json' \
-H 'X-Approval-Token: change-me-approval-token' \
-d '{
"reviewer": "operator",
"note": "approved for test run"
}'- The control plane is intentionally small. It reads the policy from
POLICY_PATH, normalises the requested command, rejects shell control operators, checks it against the selected policy, and launches the sandbox without a shell. repo_pathmust point to a location that exists inside the control plane container. In the default compose setup, that means a path under/opt/agent-stack/workspaces.- Before copying the repository into
/workspace, the control plane validates the source tree and rejects symlinks and special file types. - The control plane enforces path restrictions on
ls,find,cat,grep,rm,pytest, andpython3 -m pytest, and denies dangerousfindactions such as-execor-delete. git diff --no-indexis denied so thatgit diffcannot be used as an unrestricted file comparison escape hatch outside/workspace.- The control plane uses
HOST_WORKSPACES_ROOTwhen it asks the host Docker daemon to bind-mount a job workspace into the sandbox. In the default compose setup, this should point to the absolute host path for./workspaces. - Approval records are stored on disk under
APPROVALS_ROOT, and the approval endpoints execute the exact normalised command that was reviewed. - Approval endpoints require
X-Approval-Token, which must matchAPPROVAL_TOKEN. - Network profiles map to Docker networks through
SANDBOX_NETWORK_MODEL_ONLY,SANDBOX_NETWORK_GIT_READ, andSANDBOX_NETWORK_PACKAGE_MIRROR. The control plane creates a missing profile network on demand;noneuses Docker's built-innonenetwork. - The sample compose file includes only the control plane. The sandbox image is built separately and launched by the control plane through the host Docker socket.
max_output_kbis applied per stream to API responses and audit records.max_file_writesis checked after the command finishes, so it detects excessive workspace changes rather than preventing the first extra write.- The host-side
nftablesexample is part of the security model. Review and adapt it before relying on outbound restrictions. - The control plane has access to the Docker socket in order to launch disposable sandboxes. Treat the control plane as a high-trust component.
- Use it as a starting point for cautious experimentation, discussion, or prototyping.
- Read
configs/policy.yamlas a conservative default andconfigs/policy.execution.yamlas an explicit opt-in for repo-defined execution. - Treat the workspace copy step as intentionally conservative: repositories with symlinks or special files are rejected rather than partially copied.
- Read
configs/nftables-agent.confas an example egress control, not as a drop-in firewall policy. - Assume that stronger controls are required before exposing this pattern to untrusted workloads.
/opt/agent-stack/
control-plane/
sandbox-images/
audit/
approvals/
workspaces/
configs/
scripts/
This package is a reference scaffold. It may reduce risk, but it does not eliminate risk.