已开启
[Feature]: sandbox_http 适配 ADX yaml 契约接口设计方案 #9
WanYihui创建于 9月28日
openJiuwen-bot
9月28日 评论:
9月28日 评论:
欢迎来到 openJiuwen 社区
Hey @pikachuwan , 感谢你对社区的贡献.
机器人使用手册
有关指令的使用,可以点击 此处 查看详情。开发人员可以在每个PR或Issue下方评论特定指令来触发机器人任务。


9月28日 修改了issue 的描述
此处折叠了20条事件消息 查看更多
5 天前 修改了issue 的描述
Paired: GitHub #15 ↔ GitCode #9
sandbox_http 适配 ADX yaml 契约接口设计方案(agentos 分支)
背景描述
executor 的既有沙箱 HTTP 服务由
sandbox_http.py提供(前缀/v1/sandbox/sandboxes,7 个路由:create、delete、execute、files_read、files_write、files_list、files_search),底层为 yr RPC(SandboxManager→Sandbox→SandboxInstance,沙箱内 native Python I/O),仅回环、无凭证。与两份契约对比的结论:create/delete 对应管理面端点但出入参/状态码不一致;execute 与 files 4 项能力以 invoke action 形态提供;files/search 由契约file.searchaction 承载。设计思路
适配策略(P1/P2)与耦合约束
executor 基线(
executor/全套源码与tests/executor/测试基线)已在 agentos 分支存在,适配改造落在 agentos 分支;契约取自上游 master 的platform/api/openapi/(file.search已收录最新版)。外部依赖仅一个:yr 运行时 SDK(import yr,部署前提,代码不解决)。sandbox_http.py:yaml 三端点全套 + 字段接通 + 旧路由不落地(SANDBOX_ROUTES/legacy dispatch/405 判定/PUT raw-body 读取不进入最终形态)http_server.py:5 个方法分发点统一走sandbox_api_kind()前缀判定;_write_json不改(复用既有 extra_headers 能力)tests/executor/test_sandbox_yaml_api.py新建(三端点契约行为 + 字段接通 + 旧前缀 404 回归)test_http_server.py移除 legacy 路由用例,保留文件/exec/mkdir/chunked 等非沙箱用例(个别通用用例改走新端点)data-plane.yaml已收录file.search(action 枚举 +FileSearchArgs/FileSearchResult/FileSearchRequest完整 schema),本方案按该契约适配路由收敛
7 路由收敛为 3 个 yaml 端点:
POST /api/environment/v1/environments(create)、DELETE /api/environment/v1/environments/{environmentId}(delete)、POST /direct/{environmentId}/invoke(execute 与 files 系列含 files/search 合并为单端点 action 分发,分发模型与 execdnormalize_sandbox_action归一化表一致,dispatch.rs:399)。旧前缀路由不保留,/v1/sandbox/sandboxes*一律 404。删除范围为 URL 路由与 dispatch;_build_sandbox_options保留并按契约接通。适配层位置
所有契约差异的转换(字段改名、hex 编解码、Envelope 封装、错误模型、状态码)全部在
sandbox_http.py适配层完成;sandbox_manager.py零改动;sandbox.py除命名实例 namespace 钉扎(见设计思路·直接接通)与search_files扩展(depth剪枝参数 + 返回扫描条目计数)外,底层签名与返回结构不变。认证与请求标识
requestId 承担幂等语义(同一逻辑操作重试须保持同一 ID):管理面用
X-Request-Id头(缺省服务端生成 UUID;成功与错误响应均在响应头 echo,错误响应的生效 ID 同时落入 ErrorEnvelope 的error.requestId);数据面用X-ADX-Request-ID头或 bodyrequestId(优先级:头 > body > 服务端生成)。trace_id走X-Trace-ID头(私有约定,契约外——header 不受 body 的additionalProperties: false约束):适配层读取后传底层 RPC 追踪,并复用于 agent 进出日志(追踪统一走该头);不参与幂等。服务维持 loopback-only(非回环 403),不实现凭证校验(原生 API Server 的认证不适用于本机场景;若未来对外暴露需补认证)。响应与错误模型
data为 base64 编码的各操作结果 JSON:{"code": 200, "message": "ok", "data": "<base64(JSON 业务结果)>"}X-Request-Id),符号码映射:{"code": <HTTP 状态码>, "message": "...", "data": null, "error": {"code": "<符号码>", "retry": "...", "outcome": "...", "requestId": "..."}}{"error": "..."}(405 保留 Allow 头);429(file.search结果超限)错误体为{entries, error_code, error}失败形态(见 API-3)。"4XX"通配响应内。413/512MB 为本服务的体积上限(契约外):本服务仅监听回环、调用方直连,该上限直接生效。create 请求体接通(13 个契约字段)
extra_config携带——适配层提取三键映射到底层SandboxCreateOptions顶层字段(三键在底层各有专门处理:upstream→反向隧道 :995、working_dir→runtime_env :934、user→host_user :912),剩余键经底层现成的custom_extensions["extra_config"]通路(:936)透传后端;执行器类型由契约顶层runtime字段承载(runtime→底层执行器选择 :906);body 顶层出现sandbox_type/upstream/working_dir/user/trace_id,或extra_config内出现sandbox_type时返回 400 并提示正确位置(sandbox_type→runtime,upstream/working_dir/user→extra_config,trace_id→X-Trace-ID头)。name→SandboxCreateOptions.name→opt.name(:921)。命名实例的后续 invoke 一律按{ns}-{name}寻址(libruntime 由 functionMeta 的 ns 组合目标 id),而 create 侧仅在 invoke options 携带 namespace 时才给 designated id 加前缀——故适配层须在命名创建时钉住opt.namespace(:926,取运行时配置的 namespace,取不到回退平台默认值yr_defalut_namespace),保证注册名与寻址名一致。idleTimeoutSeconds→SandboxCreateOptions.idle_timeout(校验非负、允许 0;idle_timeout为兼容别名,主名优先)。rootfs:字符串直传SandboxCreateOptions.rootfs(底层 :938;字符串内容归调用方,同时携带 mounts 返回 400);对象由适配层组装为 rootfs JSON 后以字符串传入(子键原样保留透传后端,mounts 并入);仅传 image 时维持现有底层组装路径(:940-949)。mounts:契约 mounts 数组原样并入 rootfs JSON 的mounts键(与底层 host_dir 组装结构位一致,:944-948)随 rootfs 透传后端;本机不做挂载动作;仅对象形态 rootfs 或 image 可携带。snapshotId:适配层分流——存在时改走Sandbox(checkpoint_id=snapshotId)(既有 restore 链路,sandbox.py:832)→get_instance_id()→sandbox_manager.register()(既有登记入口,底层与 manager 零改动);此时其余 create 参数忽略。tunnel:proxyPort→SandboxCreateOptions.proxy_port;enabled: true须搭配extra_config.upstream(本机隧道由 upstream 驱动)否则 400;enabled: false显式关断;缺省由 upstream 自然驱动。涉及到的对外API
接口修改汇总表("原接口"列为现行接口到契约接口的映射)
POST /v1/sandbox/sandboxesPOST /api/environment/v1/environmentsextra_config携带、执行器类型由顶层runtime字段承载、trace 走 X-Trace-ID、接通 name/rootfs/mounts/snapshotId/tunnel/idleTimeoutSeconds;成功响应 Envelope 化;错误全量 ErrorEnvelope 化;SSE 不实现(遗留)DELETE /v1/sandbox/sandboxes/{id}DELETE /api/environment/v1/environments/{environmentId}POST /v1/sandbox/sandboxes/{id}/executePOST /direct/{environmentId}/invoke(action=process.exec,process.run同义)returncode→exit_code;action 级失败改 HTTP 200 + body errorGET .../files/readPOST /direct/{environmentId}/invoke(action=file.read整读)mode→binary;二进制编码 base64→hex;响应改为{data, error}PUT .../files/writePOST /direct/{environmentId}/invoke(action=file.write整写)mode→binary(追加模式a/ab由append参数承接,见 action-3);编码 base64→hex;空内容由原 400 放宽为可写空文件;响应改为{name, path, type, size, error}GET .../files/listPOST /direct/{environmentId}/invoke(action=file.list)recursive/max_depth(默认 20,仅递归时生效)→depth(默认 1);include_files/include_dirs无对应参数(取消);条目结构对齐 execd(type类别化、modified_time转 unix 秒、permissions恒 null——py 底层无权限数据)GET .../files/searchPOST /direct/{environmentId}/invoke(action=file.search)invoke(file.search)(参数见 API-3 action-5);契约已收录完整 schema,按最新契约适配API-1 创建环境:
POST /api/environment/v1/environments请求头:
""A-Z a-z 0-9 _ - . :(底层校验)请求体(13 个契约字段;其余 23 个平台侧字段不消费,见遗留):
""proto:port)由底层在创建期校验,无效元素随创建失败 5000mounts键透传后端;本机不做挂载动作;须搭配对象形态 rootfs 或 image(字符串 rootfs 不接受 mounts)Sandbox(checkpoint_id=...)+manager.register;此时其余参数忽略upstream/working_dir/user三键映射底层对应字段,剩余键透传后端body 顶层出现
sandbox_type/upstream/working_dir/user/trace_id,或extra_config内出现sandbox_type时返回 400 并提示正确位置(sandbox_type→顶层runtime,upstream/working_dir/user→extra_config,trace_id→X-Trace-ID头)。三键中
user接受任意 DockerConfig.User形式(如"1000"、"1000:1000"、"root"),仅runtime="docker"时生效;upstream为本机反向隧道上游地址(host:port)。请求示例:
POST /api/environment/v1/environments X-Request-Id: req-create-013 X-Trace-ID: trace-abc {"image": "ubuntu:22.04", "cpu": 2000, "memory": 4096, "ports": ["8080"], "idleTimeoutSeconds": 600, "runtime": "supervisor", "extra_config": {"working_dir": "/ws"}}返回值示例(成功,HTTP 200,响应头 echo X-Request-Id;
data为 base64 编码的 JSON):{"code": 200, "message": "ok", "data": "eyJlbnZpcm9ubWVudElkIjoic2J4LTEyMyIsInN0YXR1cyI6InJ1bm5pbmciLCJyZXF1ZXN0SWQiOiJyZXEtY3JlYXRlLTAxMyJ9"} // data 明文:{"environmentId":"sbx-123","status":"running","requestId":"req-create-013"}其中 environmentId 取底层 create 返回的 instance_id,status 恒为
"running",requestId 取本次请求 ID。返回值示例(失败,HTTP 400,响应头同样 echo X-Request-Id):
{"code": 400, "message": "cpu must be an integer", "data": null, "error": {"code": "invalid", "retry": "never", "outcome": "not_started", "requestId": "req-create-013"}}状态码:200 成功(Envelope);400/405/413/500 失败(ErrorEnvelope;405 断开 keep-alive)。
API-2 删除环境:
DELETE /api/environment/v1/environments/{environmentId}路径参数:
environmentId(string,必填,即底层 instance_id)。请求头X-Request-Id(可选,成功与错误响应均 echo)。无请求体。请求示例:
返回值示例(成功,HTTP 200):
{"code": 200, "message": "ok", "data": "eyJzdWNjZXNzIjp0cnVlfQ=="} // data 明文:{"success":true}返回值示例(沙箱不存在,HTTP 404):
{"code": 404, "message": "sandbox sbx-123 not found", "data": null, "error": {"code": "not_found", "retry": "never", "outcome": "terminal", "requestId": "req-del-007"}}状态码:200 成功(Envelope);404/500 及其余 4XX/5XX 失败(ErrorEnvelope)。
API-3 运行时动作分发:
POST /direct/{environmentId}/invoke统一承载原 execute / files_read / files_write / files_list / files_search。响应为 raw action JSON、无 envelope;action 级失败返回 HTTP 200、错误在 body。各 action 的 args 参数以数据面契约的 per-action Args schema 为准(
ExecArgs、FileReadArgs等),下表与契约一致(file.search的参数与错误形态见 action-5)。公共参数:
""A-Z a-z 0-9 _ - . :(底层校验)process.exec、process.run、file.read、file.write、file.list、file.search;契约其余 22 个 action 返回 400{}action-1
process.exec/process.run(命令执行;两个 action 名同义——契约ProcessExecRequest共用同一请求/结果 schema,服务对二者行为完全一致):command同报时优先。字符串(须非空)经沙箱内/bin/sh -c执行;数组不经 shell 直接执行(契约外扩展),空数组与元素非法由运行期判定、错误随 action 级失败走 200cmd同报时被其覆盖working_dir同报时优先;须已存在,本覆盖不创建目录(契约语义)cwd的并列字段env同报时优先;指定后整体替换沙箱默认环境(不合并)envs的并列字段{"action": "process.exec", "args": {"cmd": "ls -la", "cwd": "/ws", "timeout": 300}, "requestId": "invoke-001"} → HTTP 200 {"stdout": "total 4\n...", "stderr": "", "exit_code": 0} // RPC 失败(仍 200):{"stdout": "", "stderr": "execute failed: ...", "exit_code": -1}契约
ExecResult含可选exit_signal(POSIX 信号号);py 底层无信号数据,恒缺省(可选字段,契约合法,与permissions恒 null 同属两实现已知差异)。action-2
file.read:falsedata为 hex 编码;false 为 UTF-8 文本{"action": "file.read", "args": {"path": "/etc/hostname", "binary": false}} → HTTP 200 {"data": "host-123\n", "error": null} // action 级失败(仍 200):{"data": null, "error": "No such file or directory"}action-3
file.write:falsefalsea/ab{"action": "file.write", "args": {"path": "/ws/a.txt", "data": "hello", "binary": false}} → HTTP 200 {"name": "a.txt", "path": "/ws/a.txt", "type": "file", "size": 5, "error": null}action-4
file.list(目录列表):1(clamp 1–64){"action": "file.list", "args": {"path": "/ws", "depth": 2}} → HTTP 200 {"entries": [ {"name": "a.py", "path": "/ws/a.py", "type": "file", "size": 1024, "permissions": null, "modified_time": 1695441203.5}], "error": null}条目说明:
permissions恒为 null(py 底层条目无权限数据;execd 实现返回八进制权限串,属两实现的已知差异);modified_time为 unix 秒(由底层本地时区格式化字符串转换);至多返回 10000 条、超出静默截断,不可读子目录跳过(底层既有行为)。depth 语义与file.search一致;适配层按底层计数口径换算(底层自根 0 层计数、current_depth > max_depth才停止,故传max_depth = depth - 1)。action-5
file.search(递归文件搜索;契约已收录完整 schema,args 键必填):**、?与字符类,*也匹配路径分隔符[]64(clamp 1–64)1000(clamp 1–10000)100000(clamp 1–1000000)10000(clamp 1–30000)语义:不跟随子目录符号链接;结果按
path排序;失败不返回部分结果。响应:成功{"entries": [EntryInfo...], "scanned_entries": <遍历遇到的条目数,文件与目录、不含根,受 depth 约束>, "error": null};失败{"entries": [], "error_code": "<符号码>", "error": "<描述>"},符号码枚举INVALID_ARGUMENT/FILE_NOT_FOUND/NOT_A_DIRECTORY/PERMISSION_DENIED/FILESYSTEM_ERROR/RESOURCE_EXHAUSTED/DEADLINE_EXCEEDED——数量超限(max_results超限,RESOURCE_EXHAUSTED)返回 HTTP 429;本服务实际产生的失败码为FILESYSTEM_ERROR(RPC 失败,随 HTTP 200),契约枚举的其余符号码不产生(参数非法走传输级 400、根不存在/非目录返回空结果)。实现:py 底层
search_files(sandbox/filesystem.py:fnmatch 递归、仅文件)按depth剪枝并统计扫描条目数(scanned:遍历遇到的文件与目录总数,不含根);pattern 双形态(文件名或含/的完整路径,匹配路径统一 posix 分隔)与 exclude 过滤、path排序、max_results超限判定(429)在适配层完成;条目结构同file.list。py 实现与 execd 的已知差异:max_entries/timeout_ms经校验接受,但 py 底层不执行扫描上限与搜索超时,不触发对应的 429/408(同permissions恒 null 的已知差异)。{"action": "file.search", "args": {"path": "/ws", "pattern": "*.log", "exclude_patterns": ["tmp*"]}} → HTTP 200 {"entries": [ {"name": "a.log", "path": "/ws/a.log", "type": "file", "size": 1024, "permissions": null, "modified_time": 1695441203.5}], "scanned_entries": 3, "error": null} // action 级失败(仍 200):{"entries": [], "error_code": "FILESYSTEM_ERROR", "error": "search files failed: ..."}invoke 状态码:200(含 action 级失败);400(未知/未支持 action、args 非法、JSON 格式错误);404(实例不存在、旧前缀 URL);405(带 Allow 头,断开 keep-alive);413(超 512MB);429(
file.search结果超max_results,RESOURCE_EXHAUSTED);500——429 的错误体为file.search失败形态{entries, error_code, error},其余错误体均为{"error": "..."}。本期不实现的 yaml 接口清单(yaml 有、本服务无;仅列出,不处理)
管理面 environment.yaml(11 个操作):capabilities、resources 列表、/api/instances 视图、pause、resume、reload、network、快照创建(env 维度)、快照列表(租户维度)、快照获取/删除。另 admin keys 与 global-scheduler 端点属平台管理域,不在本服务范围。
数据面 data-plane.yaml 端点(7 个):healthz、upload、upload/status、upload/commit、download、pty、/api/environment/v1/commands/watch。
invoke 其余 action(22 个):进程管理 9(process.start/get/poll/wait/kill/list/capabilities/stdin/send_stdin)、文件系统 8(file.exists/remove/rename/mkdir/make_dir/stat/read_chunk/write_chunk)、Shell 会话 4(shell.create/run/poll/close)、其他 1(entrypoint.poll)。
与其他模块的相关性描述
sandbox_api_kind()前缀判定接入新端点;_write_json未改动(复用既有 extra_headers 能力,Envelope/错误分支在 sandbox_http 适配层)。Sandbox(checkpoint_id=...)restore 链路、manager.register登记入口;改动为命名创建时钉住 invoke namespace(见设计思路·直接接通)与search_files扩展 depth 剪枝及扫描计数(filesystem.py_search_files_impl对应实现)。测试设计与测试计划
单元测试:
test_file_handler、test_probe、test_process_manager、test_runtime原样;test_http_server保留文件/exec/mkdir/chunked 等非沙箱用例。test_sandbox_yaml_api.py新建:覆盖三端点路由与错误模型(405 Allow/404 判定、405 未读 body 断开 keep-alive 的连接复用回归)、create 字段校验与 X-Request-Id 生成/echo(含错误响应)、delete Envelope 与 404 ErrorEnvelope、invoke 6 action 行为(参数并列字段与同报优先级、cmd 数组形态直执行、hex 往返、append 追加写、条目转换与file.listdepth 边界、action 级失败 200、file.search契约参数与错误形态——pattern 默认值/文件名与完整路径双形态匹配、exclude 只过滤、symlink 目录不跟随、max_results超限 429、scanned_entries遍历计数与深度剪枝、失败体error_code)、旧前缀 404 回归与字段接通(runtime 映射、idleTimeoutSeconds 校验与 idle_timeout 别名、extra_config 三键映射、rootfs/mounts 组装与字符串 rootfs 携带 mounts 拒绝、snapshotId 分流、tunnel 校验、字段位置 400 判定(body 顶层 5 字段与 extra_config.sandbox_type)、X-Trace-ID 读取)。集成测试计划:
其他信息
遗留问题(本期不做,特别标明):
Accept: text/event-stream,本服务恒返回 JSON(调用方文档声明);实现需将_write_json的一次性写出改造为长连接分帧写出,工作量独立成期。file.search实现差异:max_entries/timeout_ms不触发对应 429/408(py 底层不执行扫描上限与超时),见 API-3 action-5;与permissions恒 null 同属两实现已知差异。涉及文件列表(agentos 分支,按阶段):
executor/src/yr/agentexecutor/sandbox_http.py(yaml 三端点 + 字段接通 + 旧路由不落地)、executor/src/yr/agentexecutor/http_server.py(分发点统一sandbox_api_kind())、executor/src/yr/agentexecutor/sandbox/sandbox.py(命名创建钉住 invoke namespace;search_files扩展 depth 与扫描计数)、executor/src/yr/agentexecutor/sandbox/filesystem.py(_search_files_impl深度剪枝、扫描计数与 symlink 目录不跟随)、tests/executor/test_sandbox_yaml_api.py(新建)、tests/executor/test_http_server.py(剥离 legacy 用例)file.search已由上游 master 收录进data-plane.yaml,服务侧按该契约对齐依据文件:agentos 分支
executor/(基线源码与构建配置)、tests/executor/;上游 masterplatform/api/openapi/{environment.yaml, data-plane.yaml}(契约;environment.yaml 为管理面契约,file.search已收录)与platform/api/http/runtime-data.md(数据面运行时契约文档);platform/runtime/execd/src/runtime/{dispatch.rs, fs.rs}(invoke action 契约参照)。