SSD Nodes Learn 🎉 VPS $5.50/月〜
ガイド Matt Connor著者 Matt Connor

SandBase Harness v0.3.2を自分のVPSで動かす方法

SandBase Harness v0.3.2をVPSに導入する手順です。Node.js 22以上の要件、agent YAML、MCPサーバー、sandboxモード、Anthropic SDKの接続先設定を解説します。

自分でホストする SandBase エージェントランタイムで得られるもの

SandBase エージェントランタイムを自分でホストすると、自分が所有するサーバー上で SandBase Harness を実行できます。セッション、認証情報、メモリ、監査証跡を他者の環境ではなく、自分のディスクに保存できます。これは Node サービスです。127.0.0.1:3000 で待ち受け、/v1 HTTP API と Web コンソールを提供し、エージェントファイルの隣にある SQLite に状態を保存します。

/v1 API は、ホスト型のマネージドエージェント API である Claude Managed Agents (CMA) を基に設計されています。この点が、このランタイムを双方向に有用なものにしています。Anthropic SDK を使ってコードを作成し、baseURL を自分のサーバーに向けられます。その後、同じコードをホスト型デプロイメントへ移行できます。

SandBase Harness 自体にはモデルが含まれていません。モデルを呼び出して使用します。2026 年 8 月時点では、OpenAI、Anthropic、OpenAI-compatible エンドポイントをサポートしています。これには、セルフホスト型ゲートウェイや DeepSeek V4 などのプロバイダーが含まれます。API key、または OpenAI API に対応するローカルサーバーは別途用意する必要があります。

開始前に必要なもの

  • 2 GB 以上の RAM を搭載した Ubuntu 24.04 実行中の VPS。インストールで最も負荷が高いのは TypeScript のビルドです。
  • Node.js 22 以降と npm 10 以降。どちらもプロジェクトが定める最低要件です。
  • git と、使用するモデルプロバイダーの API key。
  • Docker。ただし、セッションごとのコンテナサンドボックスを使用する場合だけ必要です。

Ubuntu 24.04 の標準リポジトリに含まれる Node 18.19 は最低要件を満たしません。そのため、NodeSource から Node をインストールしてください。

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

node -vv22 以上、npm -v10 以上を出力する必要があります。node -vv18.19.1 のままなら、ディストリビューションのパッケージがまだインストールされており、PATH で優先されています。続行する前に削除してください。ビルドは、シェルが検出した node を使用して実行されるためです。

v0.3.2 タグから SandBase をインストールする

移動するブランチではなく、必ずタグからインストールしてください。main を bare clone すると、1 時間前に追加された内容が取得される可能性があり、以下の設定キーと一致しないことがあります。2026 年 8 月 16 日時点の最新タグは v0.3.2 です。

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

npm install ではなく npm ci を使用してください。ci は、コミット済みの lockfile に記録された正確なバージョンをインストールするため、メンテナーがテストしたツリーと同じ状態になります。npm install は新しいバージョンを解決できるため、固定されたタグが気付かないうちに固定状態でなくなる原因になります。

次に、ワークスペースを作成します。ワークスペースは、エージェントのファイルとすべてのランタイム状態を保持する独立したディレクトリです。これをソースチェックアウトの外部に置くと、データに触れずに新しいタグを pull できます。

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 はワークスペース内に .managed-agents/ ディレクトリを作成します。starthttp://127.0.0.1:3000/dashboard でコンソールを、http://127.0.0.1:3000/v1 で API を起動します。現時点では、どちらもラップトップから到達できません。これは正しい状態であり、詳細は後述します。ここでは SSH 経由でコンソールに接続します。

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

この長い node .../dist/index.js パスは扱いにくいため、名前を付けます。

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

以下のコマンドは、その前提で sandbase <command> として記述しています。

npm からインストールしないでください

このプロジェクトのインストール手順には、次のように記載されています。npm で公開されているスコープなしの managed-agents パッケージは、このプロジェクトではありません。そのため、npx managed-agentsnpm install -g managed-agents では、使用したいランタイムとは無関係のものが取得されます。メンテナーが公式のスコープ付きパッケージを発表するまで、タグ付きの GitHub ソースからインストールしてください。これはプロジェクトの履歴にある小さな注記ではありません。v0.3.1 は主に、従来の npm クイックスタートを、バージョンを固定したタグ付きソースの手順に置き換えるために存在します。

ワークスペースをモデルプロバイダーに接続する

init.managed-agents/config.yaml を書き込みます。ワークスペース全体で 1 つのプロバイダーを設定し、個々のエージェントが具体的なモデル ID を選択します。

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

${OPENAI_API_KEY} のフォームはプロセス環境から値を取得します。そのため、キーを設定ファイルや、そのファイルのバックアップに保存せずに済みます。systemd は権限を降格する前に EnvironmentFile= を root として読み取るため、root だけが読み取れる環境ファイルにキーを記述してください。

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

そのファイルをエディターで開き、OPENAI_API_KEY=sk-... という 1 行を追加します。プロバイダーのキーはここに記述します。エージェント がセッション中に使用するシークレットは、別の問題であり影響範囲も異なるため、ランタイムの認証情報ボルトに保存してください。いずれかの場所に本番環境のトークンを貼り付ける前に、AI エージェントからシークレットを分離する の一読をお勧めします。

The agent YAML: mcp_servers, tools and permission policies

Agents are defined as YAML files in the workspace agents/ directory. This is the part of the runtime you will actually spend time in.

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 it and check it landed:

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

reload imports the seed YAML into SQLite. list should now print the agent with an ID. If list does not show it, the file was not parsed, and .managed-agents/logs/runtime.log is where the reason is written.

mcp_servers declares MCP (model context protocol) endpoints. type: url means the runtime talks HTTP to a server that runs somewhere else, so anything you already operate works here, including MCP servers hosted on the same VPS as the runtime.

Declaring a server does not hand its tools to the agent. The tools list does that, through an mcp_toolset entry whose mcp_server_name matches the name above. If the agent behaves as though the MCP tools do not exist, compare those two strings character for character before you look anywhere else.

agent_toolset_20260401 is the built-in tool set. The dated suffix is a schema version, so an agent pinned to it keeps the tool definitions it was written against. default_config sets the policy for every tool in the set, and each entry under configs overrides one tool by name, bash in the example.

permission_policy is where a runtime earns its place over a bare model call. always_ask pauses the session and waits for a human to approve the call before it runs. always_allow lets it through. Setting bash to always_ask means the agent cannot run a shell command without you seeing the exact command first, which is the same control you would reach for when running Claude Code safely on a VPS.

3 つのサンドボックスモードと、それぞれに適した場面

コードを実行するツール呼び出しは、サンドボックス内で動作します。バックエンドは、環境の config オブジェクト内にある sandbox_provider、またはコンソールの Settings、Sandbox から環境ごとに選択します。環境は POST /v1/environments から API 経由で作成します。

local は、ホスト上でランタイムの子プロセスとして、ランタイムと同じユーザーでコードを実行します。これがデフォルトです。利用者が自分 1 人で、エージェントが自分の所有するファイルだけを読み取る場合は、これで問題ありません。ただし、これは分離環境ではありません。ファイルを削除するツール呼び出しは実際のファイルを削除し、/etc/sandbase/runtime.env を読み取るツール呼び出しはプロバイダーのキーを読み取ります。

docker はセッションごとに 1 つのコンテナを起動します。

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

セッションには専用のファイルシステム、メモリ上限、CPU 割り当てがあり、セッション終了時にコンテナも削除されます。エージェントが自分の書いていないコードを実行した時点で、このモードに切り替えてください。デメリットは、ランタイムのユーザーが Docker ソケットにアクセスする必要があることです。また、docker グループへの所属は、ホスト上では root と同等の権限を意味します。セッションごとのコンテナは、実行ごとに 1 つのコンテナを使う自己ホスト型エージェントのサンドボックスと同じ構成です。そのため、プロセスがサンドボックスから脱出した場合に到達できる範囲についても、同じ考え方がそのまま適用されます。

kubernetes はセッションのワークロードを pod として実行し、kubectl execkubectl cp で制御します。ランタイムイメージには kubectl が存在する必要があります。また、その ServiceAccount には、対象 namespace 内の pod に対して create、delete、get、list、watch を実行する RBAC(role-based access control)権限に加えて、exec サブリソースへの権限が必要です。すでにクラスターを運用している場合に限り、このモードを導入する価値があります。

ランタイムが127.0.0.1にバインドされるのはなぜですか?

認証を無効にした状態で起動するためです。API keyが少なくとも1つ存在すると、ランタイムはbearer-token認証を有効にします。しかし、新規のinitはAPI keyを作成しません。デフォルト設定のまま0.0.0.0にバインドすると、shellツールとプロバイダーのkeyを保持する未認証のエージェントランタイムが、パブリックインターネット上に公開されます。

そのため、外部から到達できるようにする場合は、バインドアドレスを変更せず、次の2つを行います。

まず、認証を有効にします。サービスの環境変数ファイルでMANAGED_AGENTS_API_KEYを設定するか、POST /v1/api-keysでkeyを作成します。このコマンドはsecret_keyフィールドを1回だけ返し、その後は表示しません。クライアントはすべてのリクエストでAuthorization: Bearer <key>を送信します。

次に、前段にリバースプロキシを配置し、そこでTLS(transport layer security)を終端します。ランタイムは設計上、平文のHTTPを提供し、証明書の処理は別のコンポーネントに任せます。

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;
    }
}

この2行は単なる装飾ではありません。proxy_buffering offが重要なのは、セッションがserver-sent events(SSE)でストリーミングされるためです。バッファリングが有効だと、nginxはバッファがいっぱいになるまでレスポンスを保持します。そのため、エージェントが処理している間はコンソールに何も表示されず、最後にすべての内容がまとめて表示されます。proxy_read_timeout 3600sも重要です。デフォルト値は60秒なので、1分を超えて通信がないストリームは、ターンの途中でプロキシによって切断されます。その結果、ランタイムがクラッシュしたように見えます。

ファイアウォールでは22と443を開放します。3000は閉じてください。プロキシはloopback経由で接続するため、ホスト外部から到達できる必要はありません。

独自のボックスを Anthropic SDK の接続先にする

ランタイムは CMA 形式の /v1 インターフェースを実装しているため、Anthropic SDK クライアントは 1 つのフィールドを変更するだけで接続できます。

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'
});

Claude Managed Agents クライアントが送信するベータヘッダー anthropic-beta: managed-agents-2026-04-01anthropic-beta: agent-memory-2026-07-22 も受け付けます。ローカルランタイムでは任意です。ホスト型デプロイメント向けに書かれたコードを、ここでも変更せずに実行するために用意されています。

互換性は高いものの、完全ではありません。インターフェースが存在すると判断する前に、チェックアウト内の docs/api-matrix.md を確認してください。プロジェクト独自の未対応項目が記載されています。たとえば、クライアント側のカスタムツールは、現在のイベント結果プロトコルより上位で名前付き登録する必要があり、まだ対応されていません。

通常の HTTP でも同じように動作します。ランタイムが稼働していることを確認する最も簡単な方法です。

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}'

正常な応答では、イベントのストリームが継続的に届きます。接続が切れた場合は、ターン全体を再実行せず、最後に受信したイベントから再開してください。

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

この再開可能なストリームにより、ノートパソコンを閉じてもセッションが維持されます。イベントはサーバーに永続化されるため、クライアントは唯一のコピーを保持するのではなく、ログを再生します。

ディスク上で資格情報、メモリ、監査証跡が保存される場所

ランタイムが管理するすべてのデータは、ワークスペース内の .managed-agents/ 以下に保存されます。

.managed-agents/
├── config.yaml
├── data.db
├── logs/runtime.log
├── files/
├── skills/
├── snapshots/
└── sandbox/
  • data.db は SQLite のメタデータです。エージェント、セッション、資格情報ボールトのエントリ、メモリーストアのエントリ、API キーが含まれます。
  • files/ にはアップロードしたファイルのバイト列が保存され、skills/ にはアップロードしたスキルパッケージが保存されます。
  • snapshots/ にはセッションワークスペースのスナップショットが保存され、sandbox/ にはローカルモードのセッションの作業ディレクトリが保存されます。
  • logs/runtime.log は、何も処理されないように見える場合に最初に確認する場所です。

資格情報ボールトは、複数の Secret をまとめたものです。各 Secret は environment_variable などの auth_type で追加し、セッション作成時に vault_ids を使ってセッションへ割り当てます。メモリーストアには名前付きエントリを保存します。これらは、独自のアクセス設定と指示を持つ memory_store としてセッションにマウントします。どちらも data.db に保存されます。ここが raw model call との明確な違いです。ランタイムはセッションをまたいで情報を保持し、発生した内容を記録します。

1 つのディレクトリにまとまっているため、ディレクトリ全体をバックアップしてください。

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

最初にサービスを停止してください。ランタイムが SQLite データベースへ書き込み中にコピーすると、復元時に開けないファイルが作成される可能性があります。その事実に気付くのは、バックアップが必要になった時点です。エージェントの YAML を git で管理し、状態を別の場所に保存する場合は、デプロイメントドキュメントに従って、start 上の --data-dir で状態の保存場所を固定できます。

復元は逆の手順です。新しいサーバーで同じタグを checkout し、アーカイブをワークスペースへ展開して、サービスを起動します。${OPENAI_API_KEY} 形式を使用した場合、プロバイダーキーはアーカイブに含まれません。引き続き利用できる場所に保管してください。

systemd で実行する

ランタイム専用のユーザーを作成してください。これにより、ローカルサンドボックスモードでツール呼び出しが実行されても、あなたの権限で操作されることを防げます。

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

これを /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

プロジェクト独自のデプロイ例では、PATH 上の managed-agents バイナリを呼び出します。タグ付きソースからインストールしてもこのバイナリは作成されないため、ExecStart はビルド済みのエントリーポイントに対して node を実行します。

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

正常な結果は、status からの active (running)curl からの 200 です。それ以外の場合は、まず journalctl -u sandbase -n 50、次に .managed-agents/logs/runtime.log を確認します。重要なのは enable --now のほうです。手動で起動したプロセスは、次回の再起動後には終了しているためです。

発生する問題と表示されるメッセージ

npm run build が npm からエラーを出さずに強制終了されます。 1 GB の VPS では、TypeScript のコンパイルがカーネルの OOM キラーによって停止されます。この情報は npm ではなくカーネルログに記録されます。journalctl -k | grep -i "out of memory" で確認すると、強制終了された node プロセスを示す行が表示されます。swap を追加するか、より大きなインスタンスでビルドして dist/ をコピーしてください。

Error: listen EADDRINUSE: address already in use 127.0.0.1:3000 です。 別のプロセスがすでにポートを使用しています。sudo ss -lntp | grep 3000 でそのプロセスを特定できます。そのプロセスを停止するか、--port 3001 を指定してランタイムを起動し、プロキシを更新してください。

ノートパソコンからダッシュボードを読み込めません。 これは想定された動作です。ランタイムは loopback にバインドされています。前述の SSH トンネルを使用するか、リバースプロキシの設定を完了してください。--host 0.0.0.0 で修正しないでください。key が存在するまで認証は無効になっているためです。

Docker サンドボックスが permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock で失敗します。 sandbase ユーザーが docker グループに所属していません。sudo usermod -aG docker sandbase で修正してサービスを再起動してください。ただし、付与した権限の内容を理解しておく必要があります。このグループはホスト上の root と同等であるため、ランタイム専用のユーザーを作成した理由の一部が失われます。

Kubernetes サンドボックスが Error from server (Forbidden) で失敗します。 ServiceAccount に pod 権限または exec サブリソースの権限がありません。kubectl auth can-i create pods/exec -n <namespace> で直接確認してください。yes または no が返されます。

API key を追加すると、すべてのリクエストが 401 を返します。 最初の key が存在すると認証が有効になり、コンソールと API の両方に適用されます。Authorization: Bearer <key> を送信してください。key を紛失した場合は、別の key を作成してください。secret_key は 1 回だけ返され、読み取り可能な形式では保存されないためです。

MCP サーバーのツールがセッションに表示されません。 tools ブロック内の mcp_server_name と、mcp_servers 内の name を照合してください。次に、curl -i <url> を使用して、ランタイムがサーバー自体から URL に到達できることを確認します。URL 型の MCP サーバーはネットワーク依存関係です。VPS では、ノートパソコンとは名前解決とネットワークトラフィックのルーティングが異なります。

FAQ

OpenAI または Anthropic のキーなしで SandBase Harness を実行できますか?

OpenAI 互換エンドポイントがあれば実行できます。ランタイムは OpenAI、Anthropic、OpenAI 互換プロバイダーをサポートしているため、OpenAI API に対応するローカルサーバーを使用できます。.managed-agents/config.yaml でワークスペースのプロバイダーを設定し、api_key とエンドポイントをそのサーバーに向けます。ランタイム自体にはモデルが含まれていないため、呼び出しに応答するものが必要です。

ランタイムをパブリックポートで公開しても安全ですか?

インストール直後の状態では安全ではありません。ランタイムは 127.0.0.1:3000 にバインドし、認証を無効にした状態で起動します。対策は、別のバインドアドレスに変更することではありません。API キーを作成するか、MANAGED_AGENTS_API_KEY を設定して、ベアラートークン認証を有効にします。次に、TLS 用に nginx または Caddy を前段に配置し、ファイアウォールで port 3000 を閉じて、プロキシ経由だけで接続できるようにします。

local、Docker、Kubernetes のサンドボックスにはどのような違いがありますか?

local は、ホスト上でランタイムの子プロセスとしてツールコードを実行します。ランタイムユーザーの権限で動作し、分離されません。docker は各セッション専用のコンテナを作成します。各コンテナには独自のファイルシステム、メモリ制限、CPU シェアがあり、セッション終了時に削除されます。kubernetes はセッションを pod として実行し、kubectl exec で制御します。ランタイムイメージ内に kubectl が必要であり、対象 namespace の pod と exec サブリソースに対する RBAC も必要です。

具体的に何をバックアップする必要がありますか?

ワークスペース内の .managed-agents/ ディレクトリです。ここには config.yaml、エージェント、セッション、認証情報 vault のエントリ、メモリエントリを含む data.db SQLite データベースのほか、アップロード済みファイル、スキルパッケージ、セッションスナップショットが保存されています。SQLite がアーカイブ中に書き込まれないように、コピーする前にサービスを停止してください。${OPENAI_API_KEY} として参照されるプロバイダー API キーはバックアップに含まれないため、別途保管してください。

main ではなく v0.3.2 タグを clone するのはなぜですか?

タグは固定されたツリーであるため、確認した設定キーと CLI コマンドをそのまま使用できます。main は変化するため、ガイドの執筆時点と実行時点の間に設定キーの名前が変更される可能性があります。また、プロジェクトは、npm のスコープなし managed-agents パッケージがこのプロジェクトではないため、npx managed-agents を実行すると無関係なものがインストールされると警告しています。Release v0.3.1 は主に、npm のクイックスタートを、タグで固定したソースを使用する手順に置き換えるために存在します。