Agent Sandbox Registry

python-sdk

细粒度能力、路径范围与权限

docs/zh/design/fine-grained-capabilities.md

细粒度能力、路径范围与权限

状态:v2(已评审)。v1 草案经过 fine-grained-capabilities-review.md 记录的两轮评审;评审结论已并入下文,并在第 11 节汇总。 范围:agent-sandbox-server(agent_sandbox.server)的强制执行、template.yaml 声明式配置,以及 agent-sandbox(agent_sandbox)客户端的发现 / 错误 / 辅助方法。 相关文档:client-server-alignment.md、 base-image-and-server-sdk.md、server-api.md。 英文版为准:../../en/design/fine-grained-capabilities.md。

1. 问题

服务端目前以粗粒度能力组(FILE_OPS、PROCESS 等)暴露端点。打开 FILE_OPS 会一次性暴露 11 个端点 (list、stat、mkdir、delete、move、search、upload、download、archive、旧版 upload/download),并且只受一个进程级的 ABX_SERVER_BASE_DIR 约束。产品需要更细的控制:

非目标:这不是操作系统级沙箱。路径范围是 API 层面的控制;如果同一调用方还能运行任意进程(shell、code_run、PTY、 process_start),就能绕过任何文件规则。服务端会对这种组合给出 lint 警告(见 3.9)。

2. 设计原则

  1. 一个引擎,始终开启。 没有"未配置"的代码路径。没有策略就是空策略,仍然套用默认边界(/home/user)和可选组。项目尚未发布, 不为兼容保留任何东西。
  2. 失败即关闭。 存在但无效的策略会使服务端无法启动,绝不退化为"全部放行"。
  3. 单一决策点,以匹配到的路由为键。 策略附着在路由器选中的 RouteInfo 上,而不是重新解析的 URL 字符串,因此路由器与策略之间 不会出现差异(编码路径、方法覆盖、尾部斜杠)。
  4. 只解析一次,使用解析后的值。 路径检查返回的解析结果就是后续操作所用的路径(不二次解析);打开文件时防范符号链接替换。
  5. 收窄容易,放宽必须显式。 预设从拒绝姿态开始;deny 规则在各选择器间累加;allow 根目录必须显式给出。
  6. 发现 <= 执行。 调用方看到的(并可预检查的)恰好是它能使用的。
  7. 一份 schema,两种写法。 JSON 策略(标准库服务端)== template.yaml 的 server: 块(由工具链编译为 JSON)。

3. 模型

3.1 端点标识

每个内置路由已有稳定的 name(files_list、files_download_stream、shell、process_signal 等)。该名称就是策略、发现、 审计和客户端错误中使用的端点 ID。方法 + 路径不是标识。通过 RouteTable.route(...) 注册的自定义路由同样按其 name 处理。

为客户端/服务端对齐新增的内置端点(见对齐文档):files_watch、process_stdin、process_output、pty_detail、pty_resize、 capabilities_check。

3.2 Scope(权限模型)

每个端点要求一组 scope(须全部满足)。默认值来自内置目录(policy.ENDPOINT_CATALOG;单元测试保证每个已注册的内置路由都有条目)。

Scope 默认对应的端点
core:read capabilities、capabilities_check(每个已认证身份隐含拥有)
commands:list / commands:run list_commands / run_command(外加按命令的 scope,3.8)
files:read download、files_list、files_stat、files_search、files_download_stream、files_archive、files_watch
files:write upload、files_mkdir、files_upload_stream
files:write + files:delete files_move
files:delete files_delete
process:run shell、shell_stream、process_start
process:read process_list、process_detail、process_output
process:manage process_signal、process_stdin
terminal:use pty_* 与 PTY WebSocket
system:read / system:write system_info、system_metrics、ports、packages、env_get / env_set
devtools:run / devtools:read code_run / git_status、git_diff
browser:use / browser:evaluate browser_* / browser_evaluate

Scope 语法:namespace:action;* 授予全部;namespace:* 授予整个命名空间。health 无需认证(不变)。

3.3 身份与令牌

今天:一个可选的 ABX_SERVER_TOKEN,与 X-Access-Token 比对。

3.4 端点规则

{
  "version": 1,
  "preset": "default",                // default | transfer-only | read-only | hardened | locked
  "base_dir": "/home/user",           // 可选;覆盖 ABX_SERVER_BASE_DIR
  "groups": { "dev_tools": true },    // 该组端点 id 的别名;也可以写列表 ["dev_tools"]
  "defaults": { "audit": "deny", "limits": { "max_request_bytes": 10485760 } },
  "auth": {
    "tokens": [
      { "id": "uploader", "token_env": "ABX_TOKEN_UPLOADER", "scopes": ["files:write"] },
      { "id": "reader",   "token_sha256": "9f86d0...", "scopes": ["files:read"] }
    ]
  },
  "endpoints": {
    "*":                       { "enabled": false },               // 通配选择器
    "files_download_stream":   {
      "enabled": true,
      "scopes": ["files:read"],                                    // 替换目录默认值
      "paths": { "allow": ["/home/user/out"], "deny": ["**/.env", "**/*.pem"],
                 "read_only": false, "symlinks": "within" },
      "limits": { "max_bytes": 5242880, "rate_per_minute": 60, "max_concurrency": 4 },
      "audit": "all"
    },
    "files_upload_stream": {
      "enabled": true,
      "paths": { "allow": ["/home/user/uploads"] },
      "limits": { "max_bytes": 10485760 },
      "params": { "overwrite": false, "deny_extensions": [".sh", ".so"] }
    }
  }
}

选择器(endpoints 的键):精确 ID、通配(files_*、*)或 group:<name>。 合并顺序(后者优先,按字段):内置默认 < 预设 < defaults < group: 规则 < 通配规则(字面前缀短者在前)< 精确规则。 字段合并语义:标量和 allow 列表替换;deny 列表与 deny_* 参数取并集;同一特异度下 enabled: false 胜过 true。

组是别名,不是闸门。 groups: {file_ops: false}、groups: [agents]、table.enable_group(...) 和 ABX_SERVER_ENABLED_GROUPS=agents 都展开成该组的端点 id,并成为五层之一里的普通端点规则:default(可选组一开始是关的)、 code(enable_group / disable_group)、preset、policy、environment(只能打开)。没有单独的组闸门,也没有组级状态。更具体的选择器优先 (精确 id > glob > group: > @defaults),然后是更靠后的层;同样具体、同一层时,一条 enabled: false 胜出。每次拒绝都会指出规则 (policy:endpoints.shell、preset:read-only、code:enable_group:file_ops、default:opt-in:browser)。CORE 端点 (health、capabilities、capabilities_check)不能被禁用。

3.5 路径范围

对接受文件系统路径的端点(files_*、upload、download、git_*,以及 shell、shell_stream、process_start 的 cwd;code_run 没有 cwd):

算法(PathGuard.resolve(raw, op)):拒绝 NUL/非字符串 -> 转为绝对路径(相对路径的解析见下方兼容说明)-> realpath -> 边界检查 -> deny 检查 -> 只读检查 -> 返回解析后的路径。策略在任何存在性检查之前求值,因此错误响应不会成为文件系统的探测口。打开文件时对最后一个路径分量使用 O_NOFOLLOW,并重新校验已打开的 fd(/proc/self/fd/N,回退为 fstat 身份比较),以缩小检查/使用之间的窗口。

兼容说明:今天相对路径相对服务进程的 CWD 解析。未配置策略时保持不变;配置了 paths.allow 时相对路径相对第一个 allow 根目录解析,这是唯一合理的解读。

展开目录的端点对每个产出的条目都执行策略:files_list 与 files_search 省略被拒绝的条目且不会进入被拒绝的目录;files_archive 排除被拒绝的后代,并拒绝指向边界之外的文件符号链接(这也修复了现有 zipfile 的符号链接读取问题)。

3.6 限制与参数

每个端点的词汇表是封闭的(未知键是校验错误,因此笔误会明显失败)。

位置 键
所有端点 limits.max_request_bytes、limits.rate_per_minute、limits.max_concurrency、audit
upload、files_upload_stream limits.max_bytes;params.overwrite、params.allow_extensions、params.deny_extensions
download、files_download_stream limits.max_bytes
files_list limits.max_entries、limits.max_depth
files_search limits.max_results、limits.max_depth、limits.timeout_seconds
files_archive limits.max_files、limits.max_bytes
files_watch limits.max_duration_seconds
shell、shell_stream、process_start params.allow_commands、params.deny_commands(argv[0] 的基本名和完整路径)、limits.timeout_seconds(上限)、limits.max_output_bytes、limits.max_processes
process_signal params.allowed_signals
pty_create_session params.shells、limits.max_sessions、params.env_deny
env_get、env_set params.allow_vars、params.deny_vars(glob)
code_run params.languages、limits.timeout_seconds、limits.max_output_bytes
browser_navigate params.url_allow、params.url_deny、params.block_private_networks

未设置时限制取今天的常量。已配置的限制只会是用户写下的值,没有隐藏的上限。

3.7 速率限制、并发、审计

按 (身份, 端点) 的令牌桶(rate_per_minute,突发 = 速率)和信号量(max_concurrency)。超限返回 429 + Retry-After。审计事件写入 agent_sandbox.server.audit 日志器,每行一个 JSON 对象:ts, identity, endpoint, method, decision, code, status, paths(解析后,截断), 绝不包含令牌、请求体或文件内容。audit:off | deny(默认)| all。

3.8 按命令的 scope(自定义命令)

registry.command("deploy", scopes=["deploy"]):调用方需要 commands:run 和 deploy。GET /commands 只列出调用方可以运行的命令(发现 <= 执行)。

3.9 Lint

ServerPolicy.lint() 返回人类可读的警告,在启动时记录日志,并由 agent-sandbox-server check-config 打印:配置了路径范围,同时同一批 scope 可以访问 process/terminal/dev_tools/browser 端点;allow 根目录为 /;定义了其他令牌却给某令牌通配 scope;overwrite: false 却没有 paths.allow;等等。

3.10 子进程环境

配置了 auth.tokens 时,由 shell*、process_start、code_run、git_* 和 PTY 会话启动的子进程拿到 os.environ 去掉 ABX_SERVER_TOKEN、 ABX_SERVER_POLICY* 以及所有 token_env 变量,使 process:run 身份无法读取其他身份的秘密。未配置的服务端保持今天的行为。(只会去掉服务端自己的秘密,从不触碰任意用户变量。)

4. 配置入口

解析顺序(低 -> 高):代码默认值 < 约定文件(存在时 /etc/agent-sandbox/server-policy.json)< ABX_SERVER_POLICY_FILE < ABX_SERVER_POLICY(内联 JSON)< SandboxServer(policy=...)(取最高的现存来源,来源之间不合并)。ABX_SERVER_BASE_DIR、 ABX_MAX_UPLOAD_SIZE、ABX_SERVER_TOKEN 作为最低优先级输入读取。唯一的组环境变量是 ABX_SERVER_ENABLED_GROUPS(打开,environment 层); 没有用来关闭组的变量。

Python:

from agent_sandbox.server import SandboxServer, ServerPolicy

policy = ServerPolicy.from_dict({
    "preset": "transfer-only",
    "endpoints": {"files_download_stream": {"paths": {"allow": ["/home/user/out"]}}},
})
SandboxServer(policy=policy).serve(9000)

辅助方法:ServerPolicy.preset(name, **overrides)、ServerPolicy.only(*endpoint_ids, paths=None, **rule)、ServerPolicy.from_file(path)、 policy.with_endpoint(selector, **rule)、policy.explain(endpoint, *, scopes, path=None, op=None) -> 判定(调试辅助)。

声明式(template.yaml):

server:
  preset: transfer-only
  endpoints:
    files_download_stream:
      paths: { allow: [/home/user/out] }
      limits: { max_bytes: 5MB }          # 支持人类可读大小:5MB、1GiB

大小字符串(5MB、1GiB)在 YAML/JSON 中均可使用,由加载器规范化为整数。客户端 SDK 校验该块(SandboxTemplate.server),工具链把它编译为 server-policy.json,放到镜像中的 /etc/agent-sandbox/server-policy.json(CLI 跟进项)。容器内服务端保持仅标准库:原生支持 JSON,仅当碰巧安装了 PyYAML 时才支持 YAML。

预设(都是普通策略字典,见 server-api.md):default(今天的行为)、transfer-only、read-only、hardened、locked。

5. 请求流水线

route = match(method, raw_path)                 # 不接受任何覆盖头/参数
identity = authenticate(headers)                # 401(auth_required=False 时跳过)
rule gate: enabled -> scopes -> rate -> concurrency       # 404 endpoint_disabled(+rule) / 403 / 429
重复的查询参数                                            # 400
body gate: Content-Length 合法 -> <= max_request_bytes    # 400 / 413(读取之前)
handler(request, request.context["policy"]=EndpointContext)
   PathGuard / 限制 / 参数在处理器内部、对其实际使用的*同一批值*执行
audit(decision)

处理器抛出 PolicyDenied(code, message, ...),分发器映射为 403。每个处理器都会收到 EndpointContext;没有无上下文的模式。

6. 发现契约

GET /capabilities(需认证)保持向后兼容(保留 groups),并新增:api_version: 2、server_version、identity {id, scopes}、policy {preset, revision}、 groups 和 group_summary(由端点状态推导)、endpoints{name -> {method, path, group, summary, streaming, enabled, allowed, reason, rule, scopes, limits, paths{allow, read_only, deny_count}, params}}、 warnings(仅对持有 * 的身份)。POST /capabilities/check {endpoint, path?, op?, paths?} 返回服务端对一次假设调用的权威判定 (只做策略计算,从不触碰文件系统),客户端无需重新实现路径规则。revision 是有效策略的哈希,缓存可据此失效。

7. 错误契约

所有错误响应体保留 {"error", "type"},并新增 code(稳定的机器字符串)以及可选的 endpoint、scope、retry_after。

HTTP type code 示例
401 AuthError unauthorized
403 PolicyDenied missing_scope、path_not_allowed、path_denied、read_only、symlink_denied、command_not_allowed、language_not_allowed、signal_not_allowed、shell_not_allowed、url_denied、extension_not_allowed、env_var_not_allowed、env_var_denied、process_denied
404 ValueError endpoint_disabled(+ rule)
409 FileExistsError file_exists(overwrite: false 的上传)
413 ValueError(保留) payload_too_large(+ limit)
429 RateLimited rate_limited、concurrency_limited

客户端异常(新的 E9xxx 系列,语义吻合时继承现有类,使现有 except 子句继续有效):ServerAPIError(E9000)、ServerAuthError(E9001,同时是 AuthenticationError)、PolicyDeniedError(E9002;FilePolicyDeniedError 同时是 PermissionDeniedError)、EndpointNotAvailableError(E9003,同时是 CapabilityNotSupportedError)、PayloadTooLargeError(E9004)、RateLimitedError(E9005,retry_after)。操作系统层面的 404/403 映射到现有的 FileNotFoundError_ / PermissionDeniedError;进程超时映射到 CommandTimeoutError。

8. 客户端 SDK

9. 没有策略时的行为

行为 取值
默认能力组 除 dev_tools、browser、agents 外全部(这三组是可选的)
ABX_SERVER_TOKEN / X-Access-Token 或 Authorization: Bearer 全有或全无;身份 legacy,scope *(没有令牌时是 anonymous)
路径边界 所有接收路径的端点(含 shell 的 cwd)都在 ABX_SERVER_BASE_DIR(默认 /home/user)内;违规是 403 path_not_allowed
GET /capabilities 逐端点状态、rule,以及推导出的 groups 和 group_summary
错误响应体 {error,type},按需加 code、endpoint、rule
相对路径解析 服务端 CWD,之后仍须在边界内
不支持的 HTTP 方法 stdlib 501
能力开关 只有一层:端点规则(enable_builtin、disable_builtin 和 ABX_SERVER_DISABLED_GROUPS 都不存在)

10. 安全测试计划(绕过矩阵)

路径:..、//、/./、尾部斜杠、指向外部 / 内部到被拒绝位置的符号链接、符号链接替换(symlinks: never)、%2e%2e 与双重编码 %252e%252e、NUL 字节、 Unicode 形近字符、超长路径、重复的 path= 查询参数、相对路径与 allow 根目录、deny glob 的大小写变体、archive/search/list 泄漏被拒绝条目、 files_move 进出范围、存在性探测的顺序。传输:方法覆盖头/参数、HEAD/PUT/PATCH、负数或非数字的 Content-Length、超过端点上限的请求体、分块编码、两个 X-Access-Token 头。 鉴权:错误/缺失/其他身份的令牌、经 shell 的 scope 提升、按命令 scope、发现列表、速率与并发耗尽、经 GET /env 和子进程的令牌暴露。

11. 评审后的变更(v1 -> v2)

完整发现见 fine-grained-capabilities-review.md。