SSD Nodes Learn 🎉 VPS from $5.50/mo
How to do am Matt ConnorBy Matt Connor

How to self-host SandBase agent runtime

Run SandBase Harness v0.3.2 for your VPS with tagged install, agent YAML, MCP servers, sandbox modes, and Anthropic SDK pointing to your own box.

Wetin you go get when you self-host SandBase agent runtime

Self-hosting SandBase agent runtime mean say you dey run SandBase Harness for server wey belong to you. So sessions, credentials, memory, and audit trails dey your disk instead of another person own. Na Node service be this. E dey listen for 127.0.0.1:3000, e dey serve /v1 HTTP API and web console, and e dey keep e state for SQLite beside your agent files.

Dem shape /v1 API after Claude Managed Agents (CMA), wey be hosted managed-agent API. Na this make the runtime interesting for both sides: you fit write code with Anthropic SDK and point e baseURL to your own server. Later, you fit move the same code go hosted deployment.

SandBase Harness no ship any model. E dey call model. As of August 2026, e support OpenAI, Anthropic, and OpenAI-compatible endpoints. This one cover self-hosted gateways and providers like DeepSeek V4. You still need bring API key, or local server wey dey speak OpenAI API.

Wetin you need before you start

  • One VPS wey dey run Ubuntu 24.04 and get at least 2 GB RAM. The TypeScript build na the heaviest step for the installation.
  • Node.js 22 or newer, and npm 10 or newer. The project set both as hard minimums.
  • git, plus API key for any model provider wey you plan to use.
  • Docker, but na only if you want per-session container sandboxes.

Ubuntu 24.04 get Node 18.19 for im own repository, but e below the minimum. So use NodeSource to install Node instead.

curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs git
node -v
npm -v

node -v suppose print v22 or higher, and npm -v suppose print 10 or higher. If node -v still print v18.19.1, the distribution package still dey installed and e dey win for PATH. Remove am before you continue, because the build go use whichever node the shell find.

Install SandBase from the v0.3.2 tag

Install from tag, never from branch wey dey change. Bare clone of main go give you anything wey land one hour ago, and config keys below fit no match am. v0.3.2 na current tag as of 16 August 2026.

sudo install -d -o "$USER" -g "$USER" /opt/sandbase
cd /opt/sandbase
git clone --branch v0.3.2 --depth 1 https://github.com/sandbaseai/sandbase-harness.git
cd sandbase-harness
npm ci
npm run build

Use npm ci, no be npm install. ci dey install the exact versions wey committed lockfile record, so your tree go match the tree wey maintainers test. npm install fit resolve newer versions, and na so pinned tag fit quietly stop to remain pinned.

Now create workspace. Workspace na separate directory wey hold your agent files and all runtime state. If you keep am outside source checkout, you fit pull newer tag without touching your data.

mkdir -p /opt/sandbase/workspace
cd /opt/sandbase/workspace
node /opt/sandbase/sandbase-harness/dist/index.js init
node /opt/sandbase/sandbase-harness/dist/index.js start

init dey write .managed-agents/ directory inside workspace. start dey bring console up for http://127.0.0.1:3000/dashboard and API for http://127.0.0.1:3000/v1. Your laptop no fit reach either one yet, and that one correct; we cover am further down. For now, reach console through SSH:

ssh -N -L 3000:127.0.0.1:3000 you@your-server

That long node .../dist/index.js path dey tire, so give am name.

alias sandbase='node /opt/sandbase/sandbase-harness/dist/index.js'

The commands below dey written as sandbase <command> based on that.

No install am from npm

The project talk am for its own installation doc: the unscoped managed-agents package wey dey visible for npm no be this project. So npx managed-agents and npm install -g managed-agents go fetch something wey no relate to the runtime wey you want. Install from the tagged GitHub source until the maintainers announce official scoped package. This no be small footnote for the project history: v0.3.1 dey mainly to replace the old npm quick start with the pinned tagged-source path.

Workspace make use model provider

init dey write .managed-agents/config.yaml. You configure one provider for the whole workspace, then individual agents choose the exact model IDs.

model:
  provider: openai
  api_key: ${OPENAI_API_KEY}
storage:
  metadata:
    provider: sqlite
    options: {}
  artifacts:
    provider: local
    options:
      base_path: files

${OPENAI_API_KEY} form dey take value from process environment. So the key no go dey inside config file or any backup of that file. Put am inside environment file wey only root fit read, because systemd dey read EnvironmentFile= as root before e drop privileges.

sudo install -d -m 750 /etc/sandbase
sudo touch /etc/sandbase/runtime.env
sudo chmod 600 /etc/sandbase/runtime.env

Open that file with editor and add one line, OPENAI_API_KEY=sk-.... Provider keys belong for here. Secrets wey an agent use during session belong for runtime credential vaults instead. Na different matter be that, with different blast radius. Read how to keep secrets away from AI agents before you paste production token into either place.

YAML wey define agent: mcp_servers, tools and permission policies

Agents dey defined as YAML files inside workspace agents/ directory. Na this part of runtime you go spend time on.

name: Incident commander
description: Triages alerts and coordinates response.
model: gpt-4o
system: |-
  You are an on-call incident commander.
mcp_servers:
  - name: sentry
    type: url
    url: https://mcp.sentry.dev/mcp
tools:
  - type: agent_toolset_20260401
    default_config:
      permission_policy: { type: always_ask }
    configs:
      - name: bash
        permission_policy: { type: always_ask }
  - type: mcp_toolset
    mcp_server_name: sentry
metadata:
  template: incident-commander

Load am and check say e enter correctly:

sandbase reload
sandbase list
sandbase chat agent_assistant --message "hello"

reload dey import seed YAML go SQLite. list suppose don show the agent with ID now. If list no show am, the file no parse, and .managed-agents/logs/runtime.log na where dem write the reason.

mcp_servers dey declare MCP (model context protocol) endpoints. type: url mean say runtime dey use HTTP talk to server wey dey run for another place, so anything wey you already operate fit work here, including MCP servers wey dey hosted for the same VPS as the runtime.

To declare server no mean say agent don get access to its tools. Na tools list dey do that, through an mcp_toolset entry wey its mcp_server_name match the name above. If agent dey behave as if MCP tools no dey exist, compare those two strings character by character before you check anywhere else.

agent_toolset_20260401 na the built-in tool set. The dated suffix na schema version, so agent wey pin to am go keep the tool definitions wey dem write am for. default_config dey set policy for every tool inside the set, and each entry under configs dey override one tool by name, bash for the example.

permission_policy na where runtime show say e better pass bare model call. always_ask go pause the session and wait for human to approve the call before e run. always_allow go allow am pass. If you set bash to always_ask, agent no fit run shell command without you first seeing the exact command, na the same control you go use when you dey run Claude Code safely for VPS.

Sandbox mode por tin, and when each one make sense

Tool calls wey dey execute code dey run inside sandbox. Backend dey selected per environment, through sandbox_provider for the environment config object, or under Settings then Sandbox for the console. Environments dey created through API for POST /v1/environments.

local dey run the code as runtime child process, for the host, with runtime own user. Na the default, and e reasonable while na only you dey use am and the agent only dey read files wey you own. E no be isolation. Tool call wey delete files go delete your files, and tool call wey read /etc/sandbase/runtime.env go read your provider key.

docker dey start one container for each session.

{
  "sandbox_provider": "docker",
  "image": "node:22-slim",
  "resources": { "memory": "1g", "cpu": 1 }
}

The session get its own filesystem, its own memory ceiling, and its own CPU share, and dem dey remove the container when session end. Switch go this mode immediately once agent dey run code wey you no write. The cost be say runtime user need access to Docker socket, and membership for docker group dey equivalent to root for the host. Per-session containers get the same structure as self-hosted agent sandboxes wey get one container for each run, so the same reasoning about wetin escaped process fit reach still apply here.

kubernetes dey run session workload as pod and dey control am with kubectl exec and kubectl cp. Runtime image need make kubectl dey present, and its ServiceAccount need RBAC (role-based access control) permission to create, delete, get, list, and watch pods for the target namespace, plus the exec subresource. This mode only worth the setup if you already dey run cluster.

Why runtime dey bound to 127.0.0.1?

Because e dey start with authentication off. Runtime dey enable bearer-token authentication when at least one API key dey exist, and fresh init no dey create any. If you bind am to 0.0.0.0 with that default setting, unauthenticated agent runtime, wey dey hold shell tools and your provider key, go dey public internet.

So when you want make people reach am, leave bind address as e be and do two other things.

First, turn authentication on. Set MANAGED_AGENTS_API_KEY for service environment file, or create key with POST /v1/api-keys. E go return one secret_key field and e no go show am again. Clients go then send Authorization: Bearer <key> for every request.

Second, put reverse proxy in front and terminate TLS (transport layer security) there. Runtime dey serve plain HTTP by design, and e expect another service to handle certificates.

server {
    listen 443 ssl;
    server_name agents.example.com;

    ssl_certificate     /etc/letsencrypt/live/agents.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/agents.example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Connection "";
        proxy_buffering off;
        proxy_read_timeout 3600s;
    }
}

Two of those lines no be decoration. proxy_buffering off matter because sessions dey stream through server-sent events (SSE). When buffering dey on, nginx go hold response until e fill buffer. The console no go show anything while agent dey work, then e go dump everything at the end. proxy_read_timeout 3600s matter because default na 60 seconds. So if stream stay quiet for more than one minute, proxy go close am in the middle of one turn, and e go look like runtime crash.

For firewall, open 22 and 443. Leave 3000 closed because proxy dey reach am through loopback, and nothing outside the server suppose reach am.

Point the Anthropic SDK go your own box

The runtime dey implement a CMA-shaped /v1 surface, so Anthropic SDK client fit talk to am after you change one field.

import Anthropic from '@anthropic-ai/sdk';

const client = new Anthropic({
  apiKey: process.env.MANAGED_AGENTS_API_KEY ?? 'local-dev-key',
  baseURL: 'http://127.0.0.1:3000'
});

E also accept the beta headers wey Claude Managed Agents clients dey send, anthropic-beta: managed-agents-2026-04-01 and anthropic-beta: agent-memory-2026-07-22. Dem no compulsory for local runtime. Dem dey there so code wey dem write for hosted deployment go run here without change.

Compatibility close, but e no complete. Read docs/api-matrix.md for the checkout before you assume say one surface dey available, because the project document the gaps wey e get there. This include client-side custom tools, wey still need named registration above the current event-result protocol.

Plain HTTP work too, and na the fastest way to confirm say the runtime dey alive:

curl -N -X POST http://127.0.0.1:3000/v1/sessions/SESSION_ID/messages \
  -H "Content-Type: application/json" \
  -d '{"content": "Hello", "stream": true}'

Healthy response na stream of events wey dey continue to arrive. If connection drop, resume from the last event wey you see instead of replaying the whole turn:

curl -N http://127.0.0.1:3000/v1/sessions/SESSION_ID/events/stream \
  -H "Last-Event-ID: EVENT_ID"

Na this resumable stream make session fit survive when laptop close. Server dey persist the events, so client dey replay a log instead of holding the only copy.

Credentials, memory and audit trails dey for disk

Everything wey the runtime own dey under .managed-agents/ for the workspace.

.managed-agents/
├── config.yaml
├── data.db
├── logs/runtime.log
├── files/
├── skills/
├── snapshots/
└── sandbox/
  • data.db na the SQLite metadata: agents, sessions, credential vault entries, memory store entries and API keys.
  • files/ dey hold uploaded file bytes, while skills/ dey hold uploaded skill packages.
  • snapshots/ dey hold session workspace snapshots, while sandbox/ dey hold working directories for local-mode sessions.
  • logs/runtime.log na the first place to check whenever something silently no do anything.

Credential vaults na groups of secrets. You add each one with an auth_type like environment_variable, then attach am to a session through vault_ids when you create the session. Memory stores dey hold named entries wey you mount inside a session as a memory_store with its own access setting and instructions. Both dey inside data.db. Na exactly this one be the difference between this and a raw model call: the runtime dey remember across sessions, and e dey record wetin happen.

Because na one directory, back am up as one unit.

sudo systemctl stop sandbase
sudo tar czf /root/sandbase-$(date +%F).tgz -C /opt/sandbase/workspace .managed-agents
sudo systemctl start sandbase

Stop the service first. If you copy a SQLite database while the runtime dey write to am, the copy fit capture a file wey no go open when you restore am. You fit only discover this on the day wey you need the backup. If you prefer keep agent YAML for git and state for another place, the deployment doc support pinning the state location with --data-dir for start.

To restore am, do the reverse: check out the same tag for a fresh box, unpack the archive inside the workspace, then start the service. Your provider key no dey inside the archive if you use the ${OPENAI_API_KEY} form, so keep the key for another place wey you go still get access to.

Run am under systemd

Give the runtime e own user so one tool call for local sandbox mode no fit act like you.

sudo adduser --system --group --no-create-home --home /opt/sandbase sandbase
sudo chown -R sandbase:sandbase /opt/sandbase

Save dis as /etc/systemd/system/sandbase.service.

[Unit]
Description=SandBase Harness runtime
After=network-online.target

[Service]
User=sandbase
Group=sandbase
WorkingDirectory=/opt/sandbase/workspace
EnvironmentFile=/etc/sandbase/runtime.env
ExecStart=/usr/bin/node /opt/sandbase/sandbase-harness/dist/index.js start --host 127.0.0.1 --port 3000
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

The project own deployment example dey call a managed-agents binary for PATH. Tagged-source install no create one, so ExecStart dey run node against the built entry point instead.

sudo systemctl daemon-reload
sudo systemctl enable --now sandbase
sudo systemctl status sandbase
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3000/dashboard

Healthy result na active (running) from status and 200 from curl. If anything else happen, read journalctl -u sandbase -n 50 first and .managed-agents/logs/runtime.log second. enable --now na the part wey matter, because process wey you start by hand go disappear after the next reboot.

Wetin fit break, and message wey you go see

npm run build dey die without error from npm. For 1 GB VPS, kernel out-of-memory killer dey stop the TypeScript compile. E dey report am for kernel log, no be to npm. Confirm am with journalctl -k | grep -i "out of memory". E go print line wey name the killed node process. Add swap, or build am for bigger instance then copy dist/ go across.

Error: listen EADDRINUSE: address already in use 127.0.0.1:3000. Another process don already hold the port. sudo ss -lntp | grep 3000 go name am. Either stop that process, or start the runtime with --port 3001 and update the proxy.

Dashboard no go load from your laptop. Na the intended behaviour, because runtime dey bind to loopback. Use the SSH tunnel wey dey above, or complete the reverse proxy. No use --host 0.0.0.0 repair am, because authentication dey off until key dey.

Docker sandboxes dey fail with permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock. sandbase user no dey inside docker group. Fix am with sudo usermod -aG docker sandbase and restart the service. But understand wetin you grant: that group get root power for the host, so e remove part of the reason wey make you give the runtime e own user.

Kubernetes sandboxes dey fail with Error from server (Forbidden). ServiceAccount no get pod permissions or exec subresource. Check am directly with kubectl auth can-i create pods/exec -n <namespace>. E go answer yes or no.

Every request dey return 401 after you add API key. Authentication dey switch on when first key dey, and e apply to console plus API. Send Authorization: Bearer <key>. If you lose the key, create another one, because secret_key dey return once and system no store am for readable form.

Tools from MCP server no dey ever show for session. Check the mcp_server_name inside tools block against the name inside mcp_servers. Then check say runtime fit reach the URL from the server itself with curl -i <url>. URL-type MCP server na network dependency, and VPS dey resolve names and route network traffic differently from your laptop.

FAQ

I fit run SandBase Harness without OpenAI or Anthropic key?

Yes, if you get OpenAI-compatible endpoint. The runtime support OpenAI, Anthropic and OpenAI-compatible providers, so local server wey speak OpenAI API go work. Set the workspace provider for .managed-agents/config.yaml and point api_key plus the endpoint to am. The runtime no get any model of its own, so something must answer the calls.

E safe to expose the runtime for public port?

No, as e dey installed. E bind to 127.0.0.1:3000 and start with authentication off, and the fix no be to use another bind address. Create API key, or set MANAGED_AGENTS_API_KEY, so bearer-token authentication go turn on. Then put nginx or Caddy for front for TLS, and keep port 3000 closed for firewall so na only through the proxy people fit enter.

Wetin be the difference between local, Docker and Kubernetes sandboxes?

local dey run tool code as child process of the runtime for the host, with the runtime user's permissions and no isolation. docker dey give every session its own container with its own filesystem, memory limit and CPU share, and e remove am when the session end. kubernetes dey run the session as pod and control am with kubectl exec. E need kubectl inside the runtime image, plus RBAC on pods and the exec subresource for the target namespace.

Wetin exactly I need back up?

The .managed-agents/ directory inside the workspace. E hold config.yaml, the data.db SQLite database wey get agents, sessions, credential vault entries and memory entries, plus uploaded files, skill packages and session snapshots. Stop the service before you copy am so SQLite no dey write while archive dey happen. Provider API keys wey ${OPENAI_API_KEY} reference no dey inside the backup, so store dem separately.

Why clone the v0.3.2 tag instead of main?

Tag na fixed tree, so the config keys and CLI commands wey you read about na the same ones wey you go get. main dey change, and dem fit rename config key between when guide dey written and when you run am. The project still warn say the unscoped managed-agents package for npm no be this project, so npx managed-agents go install something unrelated. Release v0.3.1 mainly dey replace that npm quick start with the pinned tagged-source path.