Sandbox Daemon for Agent Command Execution
Rust-powered WebSocket server with Python API for remote command execution and interactive sessions.
- Command Execution - Run shell commands on remote machines with timeout control
- Interactive Sessions - Full PTY sessions with bash for manual work
- Snapshots - Save, tag and restore workspace state on a worker
- Labels & Broadcast - Filter workers by label and run a command on many at once
- Tunnel Mode (VPN) - Secure mesh networking with WireGuard encryption via Tailscale
- High Performance - Rust async runtime handles high-concurrency workloads
- Auto Reconnection - Workers reconnect automatically on network failures
- Cross-Platform - Linux and macOS release binaries; Windows builds from source
┌──────────────────────────────────────────┐
│ Python Agent Application │
│ ┌────────────────────────────────────┐ │
│ │ from sandd import Server │ │
│ │ │ │
│ │ server = Server("0.0.0.0", 8765) │ │
│ │ result = server.exec( │ │
│ │ "daemon-1", "ls -la" │ │
│ │ ) │ │
│ └────────────────────────────────────┘ │
│ ▲ │
│ │ Python bindings (PyO3) │
│ ▼ │
│ ┌────────────────────────────────────┐ │
│ │ Rust WebSocket Server (tokio) │ │
│ │ • Command routing │ │
│ │ • Session management │ │
│ └────────────────────────────────────┘ │
└──────────────────────────────────────────┘
▲
│ WebSocket
│ (ws:// in direct mode, encrypted via VPN in tunnel mode)
│
┌─────────┼─────────┐
│ │ │
┌───▼───┐ ┌───▼───┐ ┌───▼───┐
│Daemon │ │Daemon │ │Daemon │
│ #1 │ │ #2 │ │ #n │
└───────┘ └───────┘ └───────┘
Run a server and a daemon on one machine and execute a command. You need Rust (https://rustup.rs) and Python 3.8+.
1. Build both from source:
git clone https://github.com/InftyAI/SandD
cd SandD
pip install . # server: the `sandd` Python package
make daemon-release # daemon: ./target/release/sandd2. Terminal 1, start the server:
from sandd import Server
server = Server() # listens on 0.0.0.0:8765
print("waiting for worker-1...")
server.wait_for_daemon("worker-1", timeout=120)
result = server.exec("worker-1", "uname -a")
print(result.stdout)3. Terminal 2, start a daemon:
./target/release/sandd --server-url ws://127.0.0.1:8765/ws --daemon-id worker-1Terminal 1 prints the daemon's uname -a. On real machines, run the daemon on each
worker with ws://<server-ip>:8765/ws; workers dial out, so only the server's
port needs to be reachable.
The server is a Python package (from sandd import Server); the daemon is a
separate binary, also named sandd, that runs on each worker.
From PyPI:
pip install sanddFrom source (needs Rust):
git clone https://github.com/InftyAI/SandD
cd SandD
pip install .# Direct mode (no tunnel)
curl -fsSL https://raw.githubusercontent.com/InftyAI/SandD/main/hack/scripts/install.sh | sudo bash
# Tunnel mode (with Tailscale)
curl -fsSL https://raw.githubusercontent.com/InftyAI/SandD/main/hack/scripts/install.sh | sudo bash -s -- --tunnelThis installs the latest release to /usr/local/bin/sandd. To pin a specific version:
curl -fsSL https://raw.githubusercontent.com/InftyAI/SandD/main/hack/scripts/install.sh | sudo bash -s -- --version v0.2.1Install from crates.io:
cargo install sanddBuild from source:
git clone https://github.com/InftyAI/SandD
cd SandD
make daemon-release # binary at ./target/release/sandd
# or put it on your PATH (~/.cargo/bin/sandd):
cargo install --path sanddBuild from the main branch:
cargo install --git https://github.com/InftyAI/SandD sanddfrom sandd import Server
server = Server() # Direct mode (default)
server.wait_for_daemon("worker-1", timeout=30)
result = server.exec("worker-1", "hostname")
print(result.stdout)sandd --server-url ws://server-ip:8765/ws --daemon-id worker-1For secure multi-cloud deployments with mesh VPN (no TLS setup needed):
from sandd import Server, TunnelConfig
config = TunnelConfig(
authkey="YOUR_KEY",
server="http://headscale:8080",
)
server = Server(connect="tunnel", tunnel_config=config)
# ✓ Encrypted with WireGuard (no TLS needed)
# ✓ Works across NAT/firewalls
# ✓ No public IPs requiredsandd --server-url ws://10.200.0.1:8765/ws \
--daemon-id worker-1 \
--tunnel \
--tunnel-authkey YOUR_KEY \
--tunnel-server http://headscale:8080In a cluster, don't bake a key into your image — mint one per start from a key
broker ($SANDD_KEYBROKER_URL) and take the headscale URL from
$SANDD_TUNNEL_SERVER:
from sandd import Server, tunnel_config_from_env
server = Server(connect="tunnel", tunnel_config=tunnel_config_from_env())See Tunnel Mode Guide for setup instructions.
SandD does not authenticate daemons yet. The server accepts any client that
speaks the sandd.v1 protocol, and a daemon can register under any id, so anyone who
can reach the server's port can join and receive commands meant for that id.
- Direct mode: run the server only on a trusted network.
- Tunnel mode: only nodes admitted to your Headscale mesh can reach the
server, and traffic is encrypted with WireGuard. The
authkeyis a Headscale key for joining the mesh, not a SandD credential.
Daemon authentication and authorization with Keycloak is planned (see Architecture).
- Quick Start Guide
- Architecture Details
- Protocol Specification
- Tunnel Mode Guide
- Development Guide
- Examples
- Authentication & Authorization - Keycloak-issued tokens for daemons (see Security)
- File Transfer - Upload and download files between server and workers
- Audit Logging - Track all commands, sessions, and file transfers
- Metrics - Prometheus-compatible metrics for monitoring
- Resource Limits - CPU/memory/timeout controls per daemon
- Multi-tenancy - Isolated workspaces with access control
- Rate Limiting - Prevent abuse and resource exhaustion
- Command Allowlist - Restrict allowed commands per daemon
We welcome any kind of contributions, feedback, and suggestions! See DEVELOP.md for development setup and guidelines.
Apache-2.0 — see LICENSE. Third-party components (e.g. Tailscale, used by tunnel mode) are listed in THIRD_PARTY_NOTICES.md.