模板服务器 HTTP API
生成的模板包含 Dockerfile、commands.py 和 template.yaml。沙箱启动后,调用方通过 HTTP 访问它。commands.py 是容器的主进程:它用 @command 注册命名命令,并调用 start(9000)(或 SandboxServer(...).serve(port=9000))。
Dockerfile 里的 FROM 镜像随模板而变。本 HTTP API 不变。凡是启动 agent_sandbox.server 的模板,都提供下面这些路由。服务器只用 Python 标准库(ThreadingHTTPServer)。它接受 GET、POST 和 DELETE。匹配前会去掉末尾斜杠(/health/ 即 /health)。
已经在跑的服务器可以用 examples/templates/python-hello/call_api.py 这种短客户端来调用。
如何访问沙箱
容器内服务器绑定 0.0.0.0:9000(serve 的默认端口)。从外面访问时,向 SDK 要该端口的公网地址:
url = sandbox.network.get_url(9000)
# https://{port}-{sandbox_id}.{domain}
host = sandbox.network.get_host(9000)
{domain} 来自创建该沙箱的提供方。终端组开启时,对 9001 做同样的调用就是 PTY WebSocket 端口(ABX_PTY_PORT,默认 9001)。
如果你是在本机自己执行 python3 commands.py,基址就是 http://127.0.0.1:9000。
template.yaml 的 ports 必须包含 9000(使用终端时还要包含 9001),平台才会发布该端口。sandbox.network.get_url 只拼出主机名;容器里必须有进程在监听。
安全模式下,平台网关会要求它自己的访问头:
headers = sandbox.network.get_access_headers() # 可能包含 X-Access-Token
这个网关头和容器内进程校验的令牌是两回事。两者都使用头名称 X-Access-Token。下一节是容器内的校验。
认证
SandboxServer 在启动时读取一次 ABX_SERVER_TOKEN。
- 未设置或为空:所有路由都接受请求,调用方身份是
anonymous。这是本地模式。 - 已设置:以
auth_required=True注册的路由(默认如此)要求在X-Access-Token或Authorization: Bearer <token>里带上令牌。所有路由两种头都接受,比较方式是hmac.compare_digest。缺少或错误的令牌返回401:
{"error": "unauthorized", "type": "AuthError"}
同一请求里出现两个不同的 X-Access-Token 值是 400 ambiguous_credentials。
GET /health 是唯一以 auth_required=False 注册的内置路由。设置了令牌时它仍然开放。
GET /capabilities 和 POST /capabilities/check 以 auth_required=True 注册。只有在 ABX_SERVER_TOKEN 未设置时,它们才不带头也能应答。设置了令牌时,请带上凭据。
/v1/ Agents 处理函数的错误使用 OpenAI 信封 {"error": {"message", "type", "param", "code"}}。
不要把令牌写进 commands.py。用环境变量传入。POST /env 拒绝覆盖 ABX_SERVER_TOKEN,以及任何大写形式以 ABX_ 开头的名字。
PTY WebSocket(端口 9001,路径 /pty)检查同样的凭据。它接受 Authorization: Bearer、X-Access-Token 或 ?token=,并要求 pty_connect 端点处于启用状态。
能力组
每条路由是一个端点,有一个 id,每个端点一条规则决定它是否应答。组只是一组端点 id 的名字,没有单独的组闸门。被关闭的端点返回 404,并指出是哪条规则关的:
{"error": "Endpoint 'code_run' is disabled: group 'dev_tools' is opt-in. Enable the group (ABX_SERVER_ENABLED_GROUPS=dev_tools, policy groups: [dev_tools]) or this endpoint (policy endpoints.code_run.enabled: true)",
"type": "ValueError", "code": "endpoint_disabled", "endpoint": "code_run", "rule": "default:opt-in:dev_tools"}
core(health、capabilities、capabilities_check)始终开启。RouteTable.disable_group(CapabilityGroup.CORE) 会抛出 ValueError。其他组的初始状态如下:
| 组 | 默认 | 开启的路由 |
|---|---|---|
core |
开启,不能关闭 | GET /health、GET /capabilities、POST /capabilities/check |
commands |
开启 | GET /commands、POST /commands/{name} |
file_ops |
开启 | POST /upload、GET /download,以及 /files/* |
process |
开启 | POST /shell、POST /shell/stream、/process/* |
terminal |
开启 | /pty/sessions*,以及端口 9001 上的 WebSocket |
system |
开启 | /system/info、/system/metrics、/env、/ports、/packages |
dev_tools |
关闭 | POST /code/run、GET /git/status、GET /git/diff |
browser |
关闭 | /browser/*(Playwright;未安装时返回 503) |
agents |
关闭 | /v1/agents/...、/v1/vaults、/v1/responses、/v1/models、/v1/files 和 /v1/webhook_endpoints。某一层把它打开之前保持关闭 |
切换方式,最后都落成同一份逐端点规则:
| 位置 | 写法 |
|---|---|
commands.py |
default_table().enable_group(CapabilityGroup.FILE_OPS) / disable_group(...) |
template.yaml server: |
groups: [agents, dev_tools] 或 groups: {process: false};单个 id 用 endpoints: |
| 环境变量 | ABX_SERVER_ENABLED_GROUPS=agents,dev_tools(逗号列表,只能打开组) |
没有用来关闭组的环境变量。请在 server: 里写 groups: {process: false},或写 endpoints 规则。层次、具体程度和 rule 的写法见细粒度能力。
template.yaml 的 capabilities(shell、files、code、terminal、ports)是模板元数据。它们不会切换这些路由。生成的模板按下面的方式对齐:files ↔ file_ops,shell ↔ process,code ↔ dev_tools,terminal ↔ terminal,ports ↔ system。
通用响应规则
成功和错误体都是 JSON(Content-Type: application/json; charset=utf-8)。处理函数的错误形如:
{"error": "<message>", "type": "<ExceptionName>"}
未知路径是 404 {"error": "Not found: <path>", "type": "ValueError"}。处理函数里未捕获的异常是 500,带 error 和 type。策略拒绝会额外带上 code(endpoint_disabled、missing_scope、path_not_allowed、command_not_allowed、rate_limited 等)和 endpoint。被关闭的端点还带 rule。重复的查询参数是 400 duplicate_parameter。
POST 和 DELETE 读取 JSON 对象正文。原始正文上限是 10 MiB(413,code 为 payload_too_large)。空正文变成 {}。非法 JSON 是 400。
流式路由(streaming=True)返回 text/event-stream。每个片段是 event: <name>\ndata: <json>\n\n。
凡是接收路径的路由(文件路由,以及 /shell 和进程启动的 cwd)都限制在 ABX_SERVER_BASE_DIR(默认 /home/user)内,或限制在策略的 base_dir、端点的 paths.allow 内。越界的路径是 403 path_not_allowed。解码后的上传大小默认 100 MiB(ABX_MAX_UPLOAD_SIZE)。10 MiB 的 JSON 正文上限先生效,因此 base64 正文实际上到不了 100 MiB。
POST /shell 和进程启动用 shlex.split 拆分命令,并以 shell=False 执行。管道、重定向和 $VAR 不会被解释。需要 shell 时请使用 bash -c '...'。
内置路由
core
GET /health
无需认证。始终 200。
{"status": "ok"}
GET /capabilities
发现文档。没有加载策略且没有设置令牌时,policy.configured 为 false,identity 为 {"id": "anonymous", "scopes": ["*"]}。
{
"api_version": 2,
"server_version": "<package version>",
"groups": {"core": true, "commands": true, "file_ops": true, "process": true, "terminal": true, "system": true, "dev_tools": false, "browser": false, "agents": false},
"group_summary": {"file_ops": {"enabled": 12, "total": 12}, "dev_tools": {"enabled": 0, "total": 3}},
"identity": {"id": "anonymous", "scopes": ["*"]},
"policy": {"preset": "default", "revision": "<16 hex chars>", "configured": false},
"endpoints": {
"health": {
"method": "GET",
"path": "/health",
"group": "core",
"summary": "Liveness probe (anonymous).",
"streaming": false,
"enabled": true,
"allowed": true,
"reason": null,
"rule": null,
"scopes": [],
"limits": {},
"params": {}
}
},
"warnings": []
}
endpoints 是逐端点的状态,以端点 id 为键,是事实来源。它还包含 pty_connect(method 为 WS,路径 /pty),那是 9001 端口上的 WebSocket,不是 9000 端口上的 HTTP 路由。groups(名字到布尔值:组里至少有一个端点开着就是 true)和 group_summary(enabled 与 total 端点数)由它推导。端点关闭或调用方无权使用时,reason 为 endpoint_disabled 或 missing_scope,rule 给出决定它的规则(policy:endpoints.shell、default:opt-in:browser 等),并且省略 limits / params。没有策略文件、也没有 ABX_SERVER_POLICY 时,preset 为 default,revision 是空策略的哈希,configured 为 false。加载了策略时 configured 为 true,preset / revision 来自该文档。
POST /capabilities/check
正文:
| 字段 | 必填 | 含义 |
|---|---|---|
endpoint |
是 | 路由名,例如 upload 或 shell |
path |
否 | 要检查的一个路径 |
paths |
否 | 路径列表 |
op |
否 | read、write、delete 或 exec |
不会报告路径是否存在。200:
{"endpoint": "upload", "allowed": true, "code": null, "reason": null}
拒绝时 allowed 为 false,并填上 code(unknown_endpoint、endpoint_disabled、missing_scope 或路径策略码)、可读的 reason,以及决定它的 rule(有的话)。错误的正文是 400。
commands
GET /commands
来自命令注册表的可见命令(隐藏命令不列出)。命令只用 @command 注册,见命名命令。200:
{
"commands": [
{
"name": "hello",
"description": "Say hello.",
"args": [
{"name": "name", "type": "string", "required": false, "default": "World", "description": ""}
]
}
]
}
type 为 string、integer、float 或 boolean。
POST /commands/{name}
正文是参数的 JSON 对象。200:
{"result": "Hello, World!"}
result 是函数的返回值(必须能 JSON 序列化;不能序列化的值会被编码器转成字符串)。
| 状态 | 正文 |
|---|---|
400 |
缺少必填参数、出现未声明参数,或值无法转换。type 为 ValueError 或 TypeError |
404 |
Unknown command '<name>'; available: <按名排序> — available 列表包含隐藏命令 |
500 |
函数抛出异常。error 是 str(exc),type 是异常类名 |
转换规则:integer 用 int,float 用 float,boolean 接受 true/false/yes/no/1/0(不区分大小写)。其余类型变成 str。仅当省略该字段且注册的默认值不是 None 时才套用默认值。
file_ops
路径来自查询参数或正文,见各条说明。时间戳是 ISO 8601 UTC。
POST /upload
{"path": "/home/user/note.txt", "content_base64": "<standard base64>"}
200:{"path": "<resolved>", "bytes": <int>}。
| 状态 | 条件 |
|---|---|
400 |
缺少 path 或 content_base64,或 base64 非法 |
403 |
path_not_allowed:路径在允许的目录之外 |
413 |
解码后大小超过 ABX_MAX_UPLOAD_SIZE(默认 104857600)。有策略时正文还带 code: "payload_too_large" 和 limit |
500 |
写入失败 |
会创建父目录。有策略时,allow_extensions / deny_extensions 可以拒绝后缀(extension_not_allowed),overwrite: false 在文件已存在时返回 409。
GET /download?path=
200:
{"path": "<resolved>", "content_base64": "<standard base64>"}
路径缺失是 400,越界是 403 path_not_allowed,不是普通文件是 404,超过同一字节上限是 413,读失败是 500。
GET /files/list?path=
| 查询 | 默认 | 含义 |
|---|---|---|
path |
— | 目录(必填) |
recursive |
false |
true / yes / 1 时递归子目录 |
max_entries |
1000 |
上限,也会被策略的 max_entries 再限制 |
200:{"entries": [{"name", "type", "size", "modified"}]}。type 为 directory 或 file(follow_symlinks=False,因此符号链接记为 file)。递归深度停在 3(策略设置了 max_depth 时用该值)。路径不是目录时为 404。
GET /files/stat?path=
路径不存在时也是 200:
{
"name": "note.txt",
"path": "/home/user/note.txt",
"type": "file",
"size": 12,
"permissions": "644",
"modified": "2026-10-12T00:00:00+00:00",
"accessed": "2026-10-12T00:00:00+00:00",
"created": "2026-10-12T00:00:00+00:00",
"exists": true
}
type 为 file、directory、symlink;什么都没有时为 unknown(时间为空字符串,exists: false)。permissions 是 st_mode 的末三位八进制。
POST /files/mkdir
{"path": "/home/user/out"}。200:{"path": "<resolved>", "created": true}。使用 exist_ok。错误路径是 400,OSError 是 500。
DELETE /files?path=
删除文件或整个目录树。200:{"path": "<resolved>", "deleted": true}。不存在是 404。有策略时,删除基目录本身,或删除含有被拒绝路径的目录树,会被拒绝。
POST /files/move
{"source": "/home/user/a", "destination": "/home/user/b"}。200:{"source": "<resolved>", "destination": "<resolved>"}。任一字段缺失是 400(消息以 source: 或 destination: 开头),源不存在是 404。会创建目标的父目录。
POST /files/search
{"path": "/home/user", "pattern": "*.py", "max_depth": 5, "max_results": 100}
pattern 是对文件名的 fnmatch。默认深度 5、最多 100 条、遍历 10 秒。200:
{"results": [{"name": "a.py", "path": "/home/user/a.py"}], "timed_out": false}
path 不是目录时为 404。非整数的限制是 400。
POST /files/upload-stream
正文和成功形状与 POST /upload 相同(path、bytes)。处理函数仍然接收一整份 JSON 正文;它按 64 KiB 分块写入解码后的字节。
GET /files/download-stream?path=
与 GET /download 相同,并多一个 bytes:
{"path": "<resolved>", "content_base64": "...", "bytes": 12}
POST /files/archive
{"paths": ["/home/user/proj"], "format": "tar.gz"}
format 为 tar.gz(默认)或 zip。200:
{"content_base64": "...", "format": "tar.gz", "bytes": 1200}
限制:10000 个文件、归档字节 100 MiB(策略的 max_files / max_bytes 可以再降低)。文件过多是 400。归档过大是 413。输入路径不存在是 404。指向基目录之外的符号链接会被跳过。
GET /files/watch?path=
SSE。查询参数:recursive(false),interval 秒(默认 0.5,夹在 0.1–5),timeout 秒(默认 300,最小 0.1,并受策略 max_duration_seconds 限制)。
| 事件 | 数据 |
|---|---|
ready |
{"path": "<resolved>"} |
change |
{"type": "created"\|"modified"\|"deleted", "path": "...", "is_dir": false} |
end |
{"reason": "timeout"} |
非法的 interval 或 timeout 是 400,path 不是目录是 404。这是轮询,不是 inotify 流。最多跟踪 5000 个条目。
process
shell 和进程启动正文里的 cwd、env、timeout 都可选。cwd 必须是字符串,并且是允许目录内已存在的目录(否则 403 path_not_allowed)。env 必须是值为字符串的对象。timeout 默认 300 秒;非正数或非数字会换回 300。策略可以降低超时(timeout_seconds)并限制输出(max_output_bytes,这会让 /shell 带上 "truncated": true)。
POST /shell
{"command": "ls -la /tmp", "cwd": "/tmp", "env": {"A": "1"}, "timeout": 30}
200:
{"stdout": "...", "stderr": "", "exit_code": 0}
exit_code 是进程返回码,包括非零。command 缺失或 cwd/env 类型不对是 400。shlex 失败是 500。超时是 500 {"error": "Command timed out after <n>s", "type": "TimeoutError"}。
POST /shell/stream
正文相同。SSE 按行输出(去掉行尾换行):
| 事件 | 数据 |
|---|---|
stdout |
{"data": "<line>"} |
stderr |
{"data": "<line>"} |
error |
{"error": "timeout"} 或 {"error": "output_limit"} 或 {"error": "<message>"} |
exit |
{"exit_code": 0} |
这里的 shlex 失败是 400,不是 500。
POST /process/start
{"command": "sleep 30", "cwd": "/tmp", "env": {}, "capture_output": false, "stdin": false}
200:{"pid": 123, "command": "sleep 30"}。服务器最多保留 100 个存活进程(已满时 429 ResourceError;已退出的记录会先被丢掉)。capture_output: true 保存 stdout 和 stderr(默认上限 1 MiB;更早的片段会被丢弃)。stdin: true 保持 stdin 打开,供 /process/{pid}/stdin 写入。错误的命令或 shlex 错误是 400,Popen 失败是 500。
GET /process/list
200:{"processes": [{"pid", "command", "state", "started_at"}]}。state 为 running(这次调用会移除已退出的进程)。started_at 是 Unix 时间戳(time.time())。
GET /process/{pid}
200:
{"pid": 123, "command": "sleep 30", "state": "running", "started_at": 1700000000.0, "exit_code": null}
state 为 running 或 exited。退出前 exit_code 为 null。pid 不是整数是 400,不是本服务器启动的 pid 是 404。
POST /process/{pid}/signal
{"signal": 15}。允许的信号:2(SIGINT)、9(SIGKILL)、15(SIGTERM)、18(SIGCONT)、20(SIGTSTP)。200:{"pid": 123, "signal": 15}。其他信号是 400。pid 1 和服务器自己的 pid 是 403。未知 pid 是 404。
POST /process/{pid}/stdin
仅用于以 stdin: true 启动的进程。
{"data": "hello\n", "eof": false}
data 是 UTF-8 文本。也可以用 data_base64。eof: true 关闭 stdin。data、data_base64、eof 至少要有一个。200:{"pid": 123, "written": 6, "closed": false}。未启用 stdin 或已经关闭是 409。非法 base64 或非字符串 data 是 400。
GET /process/{pid}/output?since=0&wait=0
仅当启动时 capture_output 为 true(否则 409)。since 是序号(默认 0)。wait 秒(默认 0,最大 30)会阻塞到有新数据或进程结束。200:
{
"pid": 123,
"events": [{"seq": 0, "stream": "stdout", "data": "hi"}],
"next": 1,
"state": "running",
"exit_code": null,
"dropped": false
}
dropped: true 表示捕获缓冲区丢掉了更早的事件。
terminal
HTTP 路由管理会话。按键走 ABX_PTY_PORT(默认 9001)上的 WebSocket,仅在本组启用时才会启动。创建时允许的 shell:/bin/bash、/bin/sh、/bin/zsh、/usr/bin/bash、/usr/bin/zsh。最多 10 个会话。空闲 1800 秒的会话会在下次创建或列出时被回收。
POST /pty/sessions
{"shell": "/bin/bash", "cols": 80, "rows": 24, "env": {"LANG": "C.UTF-8"}}
200:
{"session_id": "<12 hex chars>", "ws_url": "ws://localhost:9001/pty?session_id=<id>", "pid": 123}
ws_url 使用 localhost 拼出;当设置了 ABX_SERVER_TOKEN 且服务器没有按身份区分令牌时,还会带上 &token=<经过 URL 编码的令牌>。从沙箱外连接时,在 sandbox.network.get_url(9001) 的路径 /pty 上打开 WebSocket,查询参数相同。达到会话上限是 429。shell 不在名单里或尺寸不是整数是 400。env 里的 LD_PRELOAD、LD_LIBRARY_PATH 和 DYLD_INSERT_LIBRARIES 会被去掉。
GET /pty/sessions
200:{"sessions": [{"id", "pid", "created_at", "cols", "rows", "alive"}]}。
GET /pty/sessions/{id}
200:一个会话对象(id、pid、created_at、cols、rows、alive)。已经不在了是 404。
POST /pty/sessions/{id}/resize
{"cols": 120, "rows": 40}。两者都是必填整数,范围 1 到 1000。200 返回会话对象。
DELETE /pty/sessions/{id}
200:{"status": "closed", "session_id": "<id>"}。不存在是 404。
WebSocket /pty?session_id=
客户端 → 服务器:
{"type": "input", "data": "ls\n"}
{"type": "resize", "cols": 120, "rows": 40}
{"type": "signal", "signal": "SIGINT"}
signal 接受 SIGINT、SIGTERM、SIGKILL、SIGHUP、SIGQUIT。
服务器 → 客户端:
{"type": "output", "data": "..."}
{"type": "event", "event": "started", "session_id": "<id>", "pid": 123}
{"type": "event", "event": "exited", "session_id": "<id>"}
认证失败以关闭码 4001 断开。终端被关闭时关闭码为 4003。
system
GET /system/info
200:
{
"os": "Linux",
"arch": "x86_64",
"cpu_count": 2,
"memory_total_mb": 2048,
"memory_available_mb": 1024,
"disk_total_gb": 20.0,
"disk_free_gb": 10.0,
"python_version": "3.11.0",
"hostname": "sandbox"
}
有 /proc/meminfo 时内存从那里读取。磁盘是 shutil.disk_usage("/")。
GET /system/metrics
200:cpu_load_1m、cpu_load_5m、cpu_load_15m、memory_used_mb、memory_total_mb、memory_percent、disk_used_gb、disk_total_gb、disk_percent、uptime_seconds。uptime 是自本模块被导入以来的秒数。
GET /env
200:{"variables": {"HOME": "/home/user"}}。名字中含有 TOKEN、SECRET、KEY、PASSWORD 或 CREDENTIAL(任意大小写)的变量会被省略。查询参数 filter 是逗号分隔的名字白名单。策略可以再加上 allow_vars / deny_vars 通配。
POST /env
{"vars": {"LANG": "C.UTF-8"}}。200:{"updated": ["LANG"]}。任一键是 PATH、HOME、USER、SHELL,或大写后以 ABX_ 开头,则 403。vars 不是对象是 400。值以 str() 存入。
GET /ports
200:{"listening": [{"port": 9000, "protocol": "tcp", "address": "0.0.0.0", "pid": null}]}。从 /proc/net/tcp 和 /proc/net/tcp6 读取(只看 LISTEN)。两者都空时,处理函数运行 ss -tlnp。只有 ss 回退路径才会填 pid。
GET /packages?manager=pip
manager 为 pip(默认)或 npm。pip 运行 python -m pip list --format json。npm 运行 npm list --json --depth=0。200:{"manager": "pip", "packages": [{"name": "pip", "version": "24.0"}]}。未知的管理器是 400。找不到可执行文件时 packages 为空列表。超时 10 秒(500 TimeoutError)。
dev_tools(启用前关闭)
POST /code/run
{"code": "print('hello')", "language": "python", "timeout": 30}
language 为 python(python3 -c)、node(node -e)或 bash(bash -c)。默认超时 30 秒,最大 300。200:
{
"stdout": "hello\n",
"stderr": "",
"exit_code": 0,
"language": "python",
"execution_time_ms": 12.34,
"_security_note": "Security relies on container isolation, not application-level restrictions"
}
超时是 408。运行时二进制不存在是 500。策略的 languages 列表可以拒绝某种语言。
GET /git/status?path=.
200:{"branch": "main", "clean": true, "files": [{"path": "a.py", "status": "modified"}]}。status 为 modified、added、deleted、renamed、untracked、copied 或 unmerged。git 失败(不是仓库)是 400 GitError。未安装 git 是 500。30 秒超时是 408。path 必须留在允许的目录内(否则 403 path_not_allowed)。
GET /git/diff
查询参数:path(默认 .),staged(true/yes/1 会加上 --staged),file(可选的 pathspec)。200:
{"diff": "<unified diff>", "stats": {"files_changed": 1, "insertions": 2, "deletions": 0}}
diff 文本在 1 MiB 处截断,并附上一行截断说明。git 错误与 status 相同。
browser(启用前关闭)
下面这些都需要 Playwright。如果 import playwright 失败,处理函数返回 503 RuntimeError,并提示 pip install playwright && playwright install chromium。第一次使用时创建一个共享的无头 Chromium 页面。没有按请求区分的浏览器 id。
| 路由 | 正文或查询 | 200 正文 |
|---|---|---|
POST /browser/navigate |
url(必填),wait_until 默认 networkidle,timeout 毫秒默认 30000 |
url、title、status(导航的 HTTP 状态,或 0) |
POST /browser/screenshot |
selector 可选,full_page 默认 true,format 为 png 或 jpeg |
image_base64、width、height |
GET /browser/content |
查询 type:html(默认)或 text |
content、url、title |
POST /browser/click |
selector(必填),timeout 毫秒默认 5000 |
clicked: true、selector |
POST /browser/type |
selector、text(必填),delay 毫秒默认 50,clear 默认 false |
typed: true、selector、text |
POST /browser/evaluate |
script(必填),timeout 毫秒默认 10000 |
result(page.evaluate 返回的 JSON 值) |
POST /browser/pdf |
format 默认 A4,landscape 默认 false |
pdf_base64、pages |
GET /browser/console |
无 | logs:[{type, text, timestamp}],然后清空缓冲区(最多 1000 条) |
screenshot 的 selector 匹配不到时是 404。策略可以限制 navigate 的 url(url_allow、url_deny、block_private_networks)。第一个 URL 之后的重定向不会被过滤。
agents(默认关闭)
用 ABX_SERVER_ENABLED_GROUPS=agents、server: 里的 groups: [agents],或在 serve() 之前调用 default_table().enable_group(CapabilityGroup.AGENTS) 打开。70 条路由全部已实现。有本地后端的运行时有两个:Codex CLI(codex)和 Qwen Code(qwen-code,也接受 qwen 和 qwencode)。claude-code、cursor 和 command 是已声明、没有本地后端的适配器。GET /v1/agents/runtimes 列出每个已声明的运行时,带 implemented 和 installed。选了未实现的运行时返回 404 not_found_error(agents.not_implemented_locally,x-should-retry: false),响应体指向 GET /v1/agents/runtimes。请求体和 agents.default(ABX_AGENTS_DEFAULT)都没有指定适配器时,创建调用使用 codex。每条 Agents 路由都可以单独启用。端点 id 见细粒度能力。默认 scope 是 agents:use。完整路由表见 Agents API 参考。
每个响应都带 X-Agent-Sandbox-Local: 1,每个对象都带 x_agent_sandbox 标记(local、compute、differences)。服务端做不到的字段是带 param 的 400,绝不会接受后忽略。对象属于调用方;别人的 id 与未知 id 的应答一样。
| 路由 | 行为 |
|---|---|
POST /v1/agents/sessions |
用内联 agent 或 agent_id 创建会话,可带 environment({"id": "env_..."})和 vault_ids。字符串 input 会排入第一回合。 |
GET /v1/agents/sessions |
列出会话(可按 agent_id 过滤)。 |
GET、POST、DELETE /v1/agents/sessions/{id} |
一个会话。POST 只更新 metadata。 |
POST /v1/agents/sessions/{id}/events |
排入一个回合。HTTP 202,空响应体。 相同的 Idempotency-Key 会重放这个 202。 |
GET /v1/agents/sessions/{id}/events |
会话事件的 SSE(Accept: text/event-stream)。 |
GET .../items、.../turns、.../turns/{turn_id}、.../turns/{turn_id}/items |
条目和回合。 |
GET .../artifacts、GET 和 DELETE .../artifacts/{id}、GET .../artifacts/{id}/content |
CLI 报告的路径的不可变副本。删除只删副本。 |
GET .../traces |
除非 ABX_AGENTS_TRACES=1,否则为空(只含时间的 span)。 |
GET .../subagents、.../subagents/{id}、/items、/turns、/turns/{turn_id}、/turns/{turn_id}/items |
只有 CLI 给出过的 id。 |
POST、GET /v1/agents,GET、POST、DELETE /v1/agents/{agent_id} |
Agent 定义。只有 mcp 工具。 |
POST、GET /v1/agents/environments,GET /v1/agents/environments/{id} |
目录绑定。 |
POST、GET /v1/agents/environments/{id}/files |
在工作目录下写入(绝不覆盖)和列出文件。 |
/v1/agents/environments/templates[...] |
精简模板:{name, env, 内联文件}。 |
POST /v1/files |
本地 user_data blob,使 file_id 可用(JSON 正文,data 为 base64)。 |
/v1/vaults[...]、/v1/vaults/{id}/credentials[...] |
仅内存的 vault,以及只写的 static_bearer / mcp_oauth 凭据。 |
POST /v1/responses、GET、DELETE /v1/responses/{id}、POST .../cancel、GET .../input_items、POST /v1/responses/compact |
带标记的一次性 CLI 回合。 |
GET /v1/models、GET /v1/models/{id} |
定义和已实现的运行时。 |
/v1/webhook_endpoints[...]、GET /v1/webhook_event_types |
本地带签名的 webhook 端点。 |
GET /v1/agents/runtimes |
每个已声明的运行时,带 implemented 和 installed。 |
有四条路由专门用类型化的 404 not_found_error(x-should-retry: false)拒绝,因为服务端没有对应能力:POST /v1/responses/input_tokens、DELETE /v1/models/{id},以及执行器注册和通道(agents_cannot_*)。它们和其他路由一样被声明、计数和测试。
对应的 CLI 在会话真正用到时才安装。每条拒绝规则、环境变量和 difference 代码见托管 Agent。
自定义命令与自定义路由
命名命令
命名命令只有一种机制:用 @command 注册函数。GET /commands 列出它,POST /commands/{name} 运行它。客户端的 @sandbox.register 装饰器最终也写入同一个注册表。
from agent_sandbox.server import command, start
@command("hello", description="Say hello.")
def hello(name: str = "World") -> str:
return f"Hello, {name}!"
@command(hidden=True)
def _debug_info() -> dict:
return {"ok": True}
start(9000)
GET /commands 列出 hello,不列出 _debug_info。POST /commands/hello 带 {"name": "Ada"} 返回 {"result": "Hello, Ada!"}。POST /commands/_debug_info 带 {} 仍然会执行。名字默认取函数名。参数类型来自注解:int → integer,float → float,bool → boolean,其他 → string。没有默认值的参数是 required: true 且 default: null。除非传入显式的 CommandArg 列表作为 args,否则每个参数的 description 保持 ""。scopes=(...) 要求调用方具备这些 scope;不具备的调用方在 GET /commands 里看不到该命令,也不能运行它。hidden=True 使命令不出现在 GET /commands 中。抛出异常即返回 500。注册表在服务启动时冻结,所以要在导入时注册。
需要隔离的测试可以新建私有的 CommandRegistry(),作为 SandboxServer(registry=...) 传入。生产模板用 @command。
用 @table.route 定义自定义路由
from agent_sandbox.server import CapabilityGroup, ServerResponse, default_table
table = default_table()
@table.route("GET", "/hello/{name}", group=CapabilityGroup.COMMANDS)
def hello_route(request: object) -> ServerResponse:
name = getattr(request, "path_params", {}).get("name", "World")
return ServerResponse.ok({"greeting": f"Hello, {name}!"})
分发器只调用 GET、POST 和 DELETE。处理函数收到的请求具有:
| 属性 | 内容 |
|---|---|
method |
GET、POST 或 DELETE |
path |
去掉末尾斜杠后的路径 |
query |
查询字符串解析成的 {name: [values]} |
headers |
头名称 → 单个值 |
body |
解析后的 JSON 对象;该方法没有正文时为 {} |
path_params |
从 {param} 段提取的 {param: value}(一段路径,不含斜杠) |
context |
registry 和 route_table |
返回 ServerResponse.ok({...})(200,该对象就是正文)或 ServerResponse.error(status, message)({"error", "type"},type 默认 ValueError)。
auth_required=False 使该路由在设置了 ABX_SERVER_TOKEN 时仍然开放。默认要求令牌。streaming=True 时返回 SSEResponse(event_iterator=...);迭代器产出字符串 event: <name>\ndata: <json>\n\n。
该路由不会出现在 GET /commands 里。它出现在 GET /capabilities 的 endpoints 中,键是路由 name(默认 "GET /hello/{name}"),并计入所属组的 group_summary。只有它的端点处于启用状态时才会被提供。端点被关闭时,与内置路由一样返回 404。
curl
设置基址;服务器以令牌启动时再设置令牌。这两个值都不应该写进模板镜像。
export ABX_TEMPLATE_URL="http://127.0.0.1:9000" # 或 get_url(9000) 得到的 https 地址
export ABX_SERVER_TOKEN="" # 本地模式留空
健康检查(设置了令牌时仍然开放):
curl -sS "$ABX_TEMPLATE_URL/health"
列出命令(只要设置了 ABX_SERVER_TOKEN 就带上头):
curl -sS -H "X-Access-Token: $ABX_SERVER_TOKEN" "$ABX_TEMPLATE_URL/commands"
运行注册名为 hello 的命令:
curl -sS -H "X-Access-Token: $ABX_SERVER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Ada"}' \
"$ABX_TEMPLATE_URL/commands/hello"
上传和下载。路径必须留在 ABX_SERVER_BASE_DIR 之下(默认 /home/user)。file_ops 必须启用;除非 server: 策略或 commands.py 把它关掉,否则默认是开的。
B64=$(printf 'hello' | base64)
curl -sS -H "X-Access-Token: $ABX_SERVER_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"path\":\"/home/user/note.txt\",\"content_base64\":\"$B64\"}" \
"$ABX_TEMPLATE_URL/upload"
curl -sS -H "X-Access-Token: $ABX_SERVER_TOKEN" \
"$ABX_TEMPLATE_URL/download?path=/home/user/note.txt"