SSD Nodes Learn 🎉 VPS $5.50/月起
指南 Matt Connor作者: Matt Connor

如何自托管 SandBase Harness v0.3.2

在自己的 VPS 上运行 SandBase Harness v0.3.2,涵盖固定标签安装、agent YAML、MCP 服务器、沙箱模式,以及将 Anthropic SDK 指向自托管服务。

自托管 SandBase agent runtime 的作用

自托管 SandBase agent runtime,意味着在您自己的服务器上运行 SandBase Harness。会话、凭据、记忆和审计记录都存储在您的磁盘上,而不是其他人的服务器上。它是一个 Node 服务。该服务监听 127.0.0.1:3000,提供 /v1 HTTP API 和 Web 控制台,并将状态与 agent 文件一起存储在 SQLite 中。

/v1 API 的设计参考了托管式 Claude Managed Agents(CMA)API。正因如此,这个 runtime 在两个方向上都很实用:您可以使用 Anthropic SDK 编写代码,并将其 baseURL 指向自己的服务器;之后也可以将同一份代码迁移到托管部署环境。

SandBase Harness 不附带模型,而是调用模型。截至 2026 年 8 月,它支持 OpenAI、Anthropic 和兼容 OpenAI 的端点,因此也支持自托管网关以及 DeepSeek V4 等服务提供商。您仍需提供 API key,或运行一个支持 OpenAI API 的本地服务器。

开始前的准备工作

  • 一台运行 Ubuntu 24.04 的 VPS,内存至少为 2 GB。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 -v 应输出 v22 或更高版本,npm -v 应输出 10 或更高版本。如果 node -v 仍输出 v18.19.1,说明发行版软件包仍已安装,并且在 PATH 中优先被找到。请先将其删除再继续,因为构建使用的是 shell 找到的 node

安装 SandBase 的 v0.3.2 标签

请从标签安装,不要从会移动的分支安装。直接克隆 main 会获取一小时前刚提交的内容,下面的配置键可能与其不匹配。截至 16 August 2026,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 ci,不要使用 npm installci 会安装已记录在提交的锁定文件中的确切版本,因此您的代码树与维护者测试过的代码树一致。npm install 可能会解析到更新的版本,这会导致固定标签在不知不觉中失去固定性。

现在创建工作区。工作区是一个独立目录,用于保存代理文件和所有运行时状态。将其放在源代码检出目录之外,可以在拉取更新的标签时避免触碰您的数据。

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/ 目录。start 会启动位于 http://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。整个工作区配置一个提供商,各个代理随后选择具体的模型 ID。

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

${OPENAI_API_KEY} 表单从进程环境中读取值,因此密钥不会写入配置文件,也不会出现在该文件的任何备份中。请将它放在只有 root 可读取的环境文件中,因为 systemd 会在降权前以 root 身份读取 EnvironmentFile=

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

使用编辑器打开该文件并添加一行:OPENAI_API_KEY=sk-...。提供商密钥应放在这里。代理在会话期间使用的密钥应改为存放在运行时的凭据保管库中。这是另一个问题,影响范围也不同。在将生产令牌粘贴到任一位置之前,建议先阅读避免让 AI 代理接触密钥

代理 YAML:mcp_servers、tools 和权限策略

代理以 YAML 文件的形式定义在工作区的 agents/ 目录中。这是您实际需要投入时间配置的运行时部分。

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

加载文件并确认已成功导入:

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

reload 将种子 YAML 导入 SQLite。此时 list 应显示带有 ID 的代理。如果 list 未显示该代理,说明文件未被解析,原因会写入 .managed-agents/logs/runtime.log

mcp_servers 声明 MCP(模型上下文协议)端点。type: url 表示运行时通过 HTTP 与其他位置运行的服务器通信,因此您现有的任何服务都可以在这里使用,包括与运行时部署在同一 VPS 上的 MCP 服务器

声明服务器不会自动将其工具分配给代理。tools 列表通过 mcp_toolset 条目完成此操作,该条目的 mcp_server_name 必须与上面的 name 完全匹配。如果代理表现得像不存在 MCP 工具,请先逐字符比较这两个字符串,再检查其他位置。

agent_toolset_20260401 是内置工具集。带日期的后缀是架构版本,因此锁定该版本的代理会继续使用编写时所依据的工具定义。default_config 设置该工具集中所有工具的策略,configs 下的各个条目则按工具名称覆盖单个工具的设置,例如示例中的 bash

permission_policy 体现了运行时相对于直接调用模型的价值。always_ask 会暂停会话,等待人工批准后才执行调用。always_allow 允许调用继续执行。将 bash 设置为 always_ask 后,代理执行 shell 命令前必须先让您看到完整命令;这与 在 VPS 上安全运行 Claude Code 时采用的控制方式相同。

三种沙箱模式及其适用场景

执行代码的工具调用会在沙箱中运行。系统通过环境的 sandbox_provider 选择后端,该配置位于环境的 config 对象中;也可以在控制台中依次打开 Settings 和 Sandbox 进行配置。环境通过 API 在 POST /v1/environments 创建。

local 将代码作为运行时的子进程,在主机上以运行时自身的用户身份执行。这是默认模式。当您是唯一用户,且代理只读取您拥有的文件时,可以使用此模式。它不提供隔离。删除文件的工具调用会删除您的文件,读取 /etc/sandbase/runtime.env 的工具调用会读取您的 provider key。

docker 为每个会话启动一个容器。

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

会话拥有独立的文件系统、内存上限和 CPU 份额,容器会随会话一并删除。只要代理运行的代码不是您编写的,就应立即切换到此模式。代价是运行时用户需要访问 Docker socket,而加入 docker 组等同于获得主机上的 root 权限。每个会话使用一个容器,这与 每次运行使用一个容器的自托管代理沙箱结构相同,因此关于逃逸进程可能访问哪些资源的判断也完全适用。

kubernetes 将会话工作负载作为 pod 运行,并通过 kubectl execkubectl cp 驱动该 pod。运行时镜像必须包含 kubectl,其 ServiceAccount 还需要在目标命名空间中拥有创建、删除、获取、列出和监视 pod 的 RBAC(基于角色的访问控制)权限,以及访问 exec 子资源的权限。只有在您已经运行 Kubernetes 集群时,才值得为此模式进行配置。

运行时为何绑定到 127.0.0.1?

因为它启动时未启用身份验证。当至少存在一个 API key 时,运行时会启用 bearer-token 身份验证,而全新的 init 不会创建任何 API key。按此默认设置绑定到 0.0.0.0,会将一个未经过身份验证的代理运行时暴露在公网中,而该运行时还持有 shell 工具和您的 provider key。

因此,需要让它可访问时,不要修改绑定地址,而应完成另外两项配置。

首先,启用身份验证。在服务环境文件中设置 MANAGED_AGENTS_API_KEY,或者使用 POST /v1/api-keys 创建一个 key。该命令只返回一次 secret_key 字段,之后不会再次显示。客户端随后必须在每个请求中发送 Authorization: Bearer <key>

其次,在前面配置反向代理,并在那里终止 TLS(传输层安全)。运行时按设计提供纯 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;
    }
}

其中两行不是装饰配置。proxy_buffering off 很重要,因为会话通过服务器发送事件(SSE)进行流式传输。启用缓冲后,nginx 会一直持有响应,直到缓冲区填满。因此,代理工作期间控制台不会显示内容,最后会一次性输出全部内容。proxy_read_timeout 3600s 同样重要,因为默认值为 60 秒。流在超过 1 分钟没有数据时,代理会在一次交互过程中关闭连接,表现出来就像运行时崩溃。

在防火墙上开放 22 和 443。保持 3000 关闭,因为代理通过回环接口访问该端口,主机外部不应访问它。

将 Anthropic SDK 指向您自己的服务器

该运行时实现了类似 CMA 的 /v1 接口,因此 Anthropic SDK 客户端只需修改一个字段即可与其通信。

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 客户端发送的 beta 请求头: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 是排查某项操作无提示失败时首先应查看的位置。

凭据保管库由多组机密组成。每组机密都通过一个 auth_type 添加,例如 environment_variable,并在创建会话时通过 vault_ids 附加到会话。记忆存储包含命名条目。您可以将这些条目作为 memory_store 挂载到会话,并为其设置独立的访问权限和指令。两者都存储在 data.db 中。这正是它与直接调用模型的区别:运行时会跨会话保留记忆,并记录发生过的操作。

由于所有内容都位于同一个目录中,请将整个目录作为一个整体备份。

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 中,并将状态数据存放在其他位置,部署文档支持通过 --data-dirstart 上固定状态数据位置。

恢复时执行相反的操作:在新服务器上检出相同的 tag,将归档解压到工作区,然后启动服务。如果您使用了 ${OPENAI_API_KEY} 形式,归档中不会包含 provider 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.logenable --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 修复此问题,因为在创建密钥之前不会启用身份验证。

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> 检查;该命令会返回 yesno

添加 API key 后,每个请求都返回 401。 创建第一个 key 后会启用身份验证,并且该设置同时适用于控制台和 API。发送 Authorization: Bearer <key>;如果丢失了 key,请重新创建一个,因为 secret_key 只返回一次,并且不会以可读形式存储。

MCP 服务器的工具始终不会出现在会话中。tools 块中的 mcp_server_namemcp_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,以启用 bearer token 身份验证。然后在前面部署 nginx 或 Caddy 处理 TLS,并在防火墙上关闭 3000 端口,确保只能通过代理访问。

local、Docker 和 Kubernetes 沙箱有什么区别?

local 在主机上将工具代码作为运行时的子进程执行,使用运行时用户的权限,且没有隔离。docker 为每个会话创建独立容器,并提供独立的文件系统、内存限制和 CPU 份额;会话结束后,容器会被删除。kubernetes 将会话作为 pod 运行,并通过 kubectl exec 控制它;运行时镜像中必须包含 kubectl,目标命名空间中的 pod 以及 exec 子资源还必须配置 RBAC 权限。

具体需要备份哪些内容?

备份工作区中的 .managed-agents/ 目录。该目录包含 config.yaml、保存代理、会话、凭据库条目和记忆条目的 data.db SQLite 数据库,以及上传的文件、技能包和会话快照。复制目录前请停止服务,避免 SQLite 在归档过程中写入。以 ${OPENAI_API_KEY} 形式引用的提供商 API 密钥不在备份中,因此请单独保存。

为什么要克隆 v0.3.2 标签,而不是 main?

标签对应固定的代码树,因此您查阅到的配置键和 CLI 命令就是实际获取的内容。main 会发生变化,指南编写时与您执行操作时之间,配置键可能已经重命名。项目还警告称,npm 上未限定范围的 managed-agents 软件包不是本项目,因此 npx managed-agents 会安装无关内容。Release v0.3.1 的主要作用,是将 npm 快速入门替换为固定标签源码的安装路径。