test(runtime): add native Codex isolation acceptance probes - #413
Conversation
cda5deb to
049e6be
Compare
Pull request was converted to draft
Pull request was converted to draft
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
Standalone PR ReviewPR-Agent could not safely update the persistent review. This standalone result will not replace the canonical review. |
PR Code Suggestions ✨No code suggestions found in the successfully analyzed chunks. |
Standalone PR ReviewPR-Agent could not safely update the persistent review. This standalone result will not replace the canonical review. |
Standalone PR ReviewPR-Agent could not safely update the persistent review. This standalone result will not replace the canonical review. PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
Standalone PR ReviewPR-Agent could not safely update the persistent review. This standalone result will not replace the canonical review. PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
The pinned legacy Landlock backend needs recursive read-dir on / before it runs a tool, and Landlock only ever adds access, so granting that made every sibling Conversation listable and broke the owner workspace write. Disable the native filesystem sandbox on Linux, keep the legacy backend selected, and prove the platform boundary with a sibling-Conversation negative control in the image probe.
PR Code Suggestions ✨No code suggestions found for the PR. |
最终验收证据(clean head
|
Review follow-up on #413: - boundary rules only accept canonical directories and reject any entry that reaches the shared Conversation boundary in either direction, so an injected PATH entry or a release installed in a system root cannot widen it - the Conversation root is no longer writable; only its home and workspace are - /proc is listed but never read, so a model tool cannot lift the model credential out of a native process environment, and /dev keeps only the rights a device node needs - a transport already bound to another Conversation fails closed instead of aliasing two Conversations onto one native process - native evidence requires an observed zero exit code, so output without the exec envelope is incomplete evidence rather than a pass
自动化评审意见逐条处理(head
|
| 意见 | 处置 |
|---|---|
| PATH 或程序目录条目可能包含/落在共享边界内,从而绕过隔离 | 规则只接受绝对且已规范化的目录(realpath(path) === path,因此路径中任意一段符号链接都被拒绝),并且双向拒绝与共享边界重叠的外部条目:落在边界内的条目会暴露兄弟 Conversation,反向包含边界的条目会暴露全部 Conversation |
dirname(dirname(executable)) 可能解析成 /,把整个根目录变成可读 |
同一条检查覆盖:把 pinned 发行版装在系统根目录时推导出的程序目录反向包含边界,直接被拒绝,不再静默授予该根目录 |
| 词法绝对路径仍可能是符号链接 | 同上,realpath 相等性检查取代了原来的 lstat 判定 |
policy.root 拿到完整可写权限,连带 home |
Conversation 根不再可写,只对它自己的 home 与 workspace 各授一条规则;缺失或非规范的自有目录改为直接拒绝启动,而不是静默丢规则 |
/proc 递归 read-file 会暴露同 UID 兄弟进程 |
/proc 改为只可列举、不可读取。实测:Landlock 本来就拒绝跨域读取(同一容器内 path-beneath:read-file,read-dir:/proc 下读 /proc/1/environ 返回 EACCES,读 /proc/self/environ 成功),所以兄弟进程通道原本已关闭;本次改动关闭的是剩下那条——模型工具读自己原生进程的 environ 取走模型传输凭证。镜像探针新增该负向用例,并已反证:把 /proc 改回可读后该阶段立即失败 |
/dev 授了包含 make-*/remove-* 的完整可写集合 |
收窄为 read-file,read-dir,write-file,原生进程从不在 /dev 下创建或删除节点 |
系统程序与库目录(/usr、/etc、/bin 等)递归可读 |
保持只读且不含写权限,这是运行 pinned 发行版与 Node 的最小集合,其中不存在 Conversation 数据;与边界重叠时按上面的规则拒绝。/etc、/sys 只读元数据,/proc 只可列举 |
Fixed — Driver 与证据
| 意见 | 处置 |
|---|---|
| 同一 transport 被另一个 Conversation 复用会把两个 Conversation 别名到一个原生进程 | 生产路径改为 fail closed:transport 已绑定到别的 conversationKey 时直接 RUNTIME_CODEX_UNAVAILABLE。只有脚本化测试替身显式声明「一个 transport 服务全部 Conversation」,该开关只存在于 test-support 调用的受保护入口 |
completeToolOutput 接受 undefined 退出码,任意文本都能算成功 |
现在要求每条输出都带实际观测到的 0 退出码,缺少 exec 信封一律判为证据不完整。单测 fixture 相应改为携带真实信封 |
Measured — 实测不成立
| 意见 | 实测结果 |
|---|---|
controlProbe 从未提交给 model.url,controlHold.observed 永远不会 resolve |
该 probe 是经原生 turn/start 提交的(raw native client 的输入里带 ISOLATION_PROBE:<probeId>),loopback 模型按 id 匹配后 resolve。验收报告里 activeThreadEvidence.rawForeignMarkerControl = {status: pass, category: success, markerObserved: true},即该对照确实跑过;waitForModelSignal 也保证观测不到时判为 model-observation-unavailable 而不是挂死或直接通过 |
missing native root 用例仍期望 nativeDriver(path) 拒绝,与本 PR 冲突 |
不冲突:丢失所属目录就是要 fail closed。ensureOwnedDirectory(dir, create) 的 create 只在 Conversation 根不存在时为 true,已存在的 Conversation 缺失 home/workspace 不会被重建。pinned 原生恢复套件 15/15 通过 |
By design — 保持现状并说明理由
| 意见 | 理由 |
|---|---|
setpriv 不应与 codex 从同一个可配置 launchPath 解析 |
该 PATH 是部署装配,不能由用户请求或 Grant 覆写,且镜像以只读根文件系统与文件权限保证运行用户不能替换它;这条约束写在 HLD §10.1。把它换成硬编码路径会把发行版布局固化进运行时代码,收益低于成本 |
ensureOwnedDirectory(dataRoot, true) 会重建丢失的持久根 |
数据根与共享边界目录是部署装配的容器,重建它们不携带任何 Conversation 状态;只有 Conversation 自己的目录丢失时 fail closed(见上一节) |
Conversation RPC 永不驱逐,原生进程直到 close() 才退出 |
原生进程死亡后当前设计是对该 Session fail closed(后续调用返回稳定的 RUNTIME_CODEX_UNAVAILABLE),而不是静默重开一个新进程去续接历史;改成自动重开会掩盖崩溃并改变恢复语义,属于需要单独评审的设计变更。close() 清空两张表并关闭全部 RPC |
| 启动前应校验完整的 wrapped command | 已经是这样:Conversation 自有目录缺失时 allowOwned 直接抛出,边界数组构造失败即拒绝启动,不会带着不完整的规则集 spawn |
Linux 验收 launcher 的 wrapper/环境/observations.jsonl 写入问题 |
该 harness 当前只在 darwin 运行,其结论明确不外推为 Linux;Linux 侧的兄弟 Conversation 负向证据由正式镜像探针在真实镜像与容器约束下产生 |
Linux 上模型工具仍可写本 Conversation 的原生 home |
这是 pinned 后端互斥导致的已记录代价,理由与实测见 #404 Linux 边界评论,Spec §10.9 与 ADR 0008 均按平台记录。它属于同一用户范围内的削弱,不影响跨 Conversation 结论;Darwin 的 home 对模型工具仍只读 |
临时把 / 加入 allowlist / 由环境变量放宽 allowlist |
这些都是当轮定位问题的临时诊断,早已 revert,当前 head 不存在任何 / 规则或环境变量放宽路径 |
验证
pnpm install --frozen-lockfile、check、check-types、test(全仓绿,@agent-infra/agent-runtime 488 passed / 16 skipped,platform-store 269 passed)、build、smoke、五个部署镜像逐个构建、markdownlint、markdown-link-check、workflow policy、actionlint、git diff --check 全部通过。darwin 双用户原生隔离验收 overall=pass(20/20,绑定 e2fb163…),pinned 原生恢复 15 passed,正式镜像探针 14 项 check status=passed。
…tion Native thread and turn IDs only mean something inside the app-server process that issued them, so journal resolution now matches the Conversation that owns the transport as well as the ID. Also give the two deterministic sandbox admission rejections a deadline that does not depend on machine load.
PR Code Suggestions ✨No code suggestions found for the PR. |
…e once Review follow-up on #413: - the transport-sharing policy moves to a protected hook the test double overrides, so the production class no longer carries a test-only flag; one scripted transport can host exactly one JSON-RPC multiplexer, which is why the double shares it and production never can - close() joins the first shutdown, so repeated or concurrent calls close the model transport once and never return early - the darwin-only isolation harness refuses to run elsewhere: its launch observer writes outside the Conversation boundary, so on Linux it would report an enforced boundary as a harness failure
Closes #404
变更摘要
agentId/conversationId/sessionGeneration派生的存储键,为每个 Conversation 代次运行独立原生app-server进程与持久目录(<driver-state>.native/conversations/<key>/{home,workspace})。deny、本home只读、workspace可写);Linux 由部署可信setpriv的 Landlock allowlist 限定整个原生进程。无法施加边界的平台拒绝启动原生进程,不存在无边界回退。initialize与受限配置;一个进程的准入不为另一个担保,一个原生传输只允许一个请求多路复用器。item/completed记录 agent message 正文(pinned Responses 传输从不流式发送 delta,平台 replay 原本完全丢失 assistant 正文);Bridge 拒绝为 loopback 模型端点走宿主代理。与原 Scope 的两处偏离(均为实测冲突,已在文档修正)
/usr/bin/sandbox-exec执行工具,外层 profile 只要含任何具有约束力的规则,内层sandbox_apply即Operation not permitted,会使工具普遍不可用并伪造出「隔离通过」。实测矩阵与改用原生权限 profile 的依据见 #404 设计修订评论。default_permissionsprofile 与--use-legacy-landlock同用(连只写extends=":workspace"也拒绝),而它的替代后端需要 bubblewrap 与 user namespace,pinned 发行版不附带该二进制、运行容器在移除全部 capabilities 与no-new-privileges下也无法unshare(CLONE_NEWUSER)。实测该 legacy Landlock 后端在执行工具前需要对/的递归read-dir:缺少它时工具以Failed to execvp /bin/sh: Permission deniedpanic,补上它之后兄弟 Conversation 目录重新可列举、本人workspace写入反而失效。Landlock 只能增加访问、深层规则无法收窄父规则,因此两个后端不能同时成立。本 PR 保留能真正隔离的那一个:Linux 关闭原生自有文件 sandbox,平台 Landlock 边界成为唯一文件边界,后端选择仍留在 legacy Landlock 以避免残留代码路径落到需要 namespace 权限的后端。代价是同一 Conversation 内的模型工具与该 Conversation 自身权限相同,可写入本 Conversation 的原生home;跨 Conversation 的读取、列举、搜索与写入仍全部被拒绝,并由镜像探针的负向用例证明。Darwin 不受影响,home对模型工具仍只读。验收标准
0.153.0(upstreamTag=rust-v0.153.0、schema sha256 已校验)与 darwin 有效配置approvalPolicy=on-request、sandboxType=workspaceWrite、networkAccess=false,launchConfiguration.actors.{a,b}.isolated=true与actorsDisjoint=true;configuration.personal-memory=pass(原生 feature 关闭且模型未暴露 memory 工具)。Linux 的机制、已验证保障与已记录代价见上一节与 Spec §10.9。同目录与 threadId 均未单独作为结论。owner-file-read、thread-context正向对照 pass;cross-file-read、cross-file-search为foreign-read-permission-denied,cross-file-modify为foreign-write-permission-denied(他人文件独立回读未变),cross-history-file为foreign-marker-absent(本人present+ 对方denied)。四通道均未观察到外来标记。Linux 侧由正式镜像探针native-sibling-conversation-denied覆盖:预先存在的兄弟 Conversation 目录读取、列举与写入全部被拒绝,同一命令内本人workspace写入并回读成功。concurrent与restart-resume两个阶段全部场景 pass,restart-resume.original-native-sessions=two-processes-resumed-original-native-sessions;证据绑定 clean-head commita5feebb6d6e7a35b8ac06c86a0614ac7cc39f38b,persistence=pass / required-403-merge-reachable-clean-head(mergeCommit=389b2b30890399270c645a32cd21ddf3a81dd41e)。READ、写APPLIED、历史OWNER=present),因此不是「工具普遍不可用」;host-positive-control=pass,activeThreadEvidence=pass且 raw marker 正向对照success。镜像探针的兄弟目录负向用例另做过反证:把 allowlist 放宽到/后该阶段立即失败,确认不是空断言。现有 Session/Grant/fence 回归随pnpm test全绿。自动验证
pnpm install --frozen-lockfile、pnpm check、pnpm check-types、pnpm test、pnpm build、pnpm smoke、pnpm docker:build、markdownlint-cli225 文件、markdown-link-check、verify-workflow-policy、run-actionlint、git diff --check。@agent-infra/agent-runtime488 passed / 16 skipped。overall=pass,退出码 0,全部场景 pass;pinned 原生进程恢复套件 15 passed。status=passed,14 项 check 含新增native-sibling-conversation-denied。本地内核只提供 Landlock ABI 4,因此本地运行把 ABI V5 准入探针权限位换成fs:truncate;CI 走未改动的fs:ioctl-dev路径。pnpm docker:build并行调用因 npm registry 超时失败,改为逐服务构建后全部成功;合并main之后本机@agent-infra/platform-store的 PostgreSQL 容器因本地磁盘压力无法就绪。两项都与本变更无关,以 CI 结果为准。git diff对.github/workflows/ci.yml相对实现基线为空。人工验证
ready-for-human曾被应用,因此 Human Validation Gate 需要针对当前 heada5feebb6d6e7a35b8ac06c86a0614ac7cc39f38b的团队成员确认评论。原生验收需要 pinned 二进制且默认跳过,无法由 CI 复跑,请按需回读本 PR 的脱敏报告。评审意见处理
48 条自动化评审线程已全部回复并 resolve(0 条未解决),逐条处置见
评审处置汇总。其中实际改码的部分:
边界规则只接受规范化目录并双向拒绝与共享边界重叠的条目;Conversation 根不再可写;
/proc只可列举(关闭模型工具从原生进程环境取走模型凭证的路径,并有反证);
/dev收窄到设备节点所需权限;transport 已绑定别的 Conversation 时 fail closed 且该策略不再是生产状态里的标志位;通知路由绑定到持有
transport 的 Conversation;证据要求实际观测到的 0 退出码;
close()缓存首次关闭使并发/重复调用只关闭一次模型传输;darwin-only 的隔离 harness 在其他平台直接 skip(它的观察 wrapper 写在边界外,在 Linux 会把
「边界确实生效」报成 harness 失败)。其余为实测判定不成立(含一条 harness 断言被验收数据反驳)或保持现状
并给出理由;
setpriv从可信 PATH 解析这一条明确 declined 并说明了真实不对称在于 helper 缺少 provenance固定,建议单独开票。
风险
RUNTIME_*失败而不是启动失败)。Linux 上模型工具可写入本 Conversation 的原生home(见上节代价),Darwin 不受影响。旧共享布局的持久目录不会被迁移或复用。macOS 结论不外推为 Linux/Pod 验收。git revert本 PR 的 squash 提交即可回到单进程装配;持久目录只新增conversations/<key>/子树,回退后旧代码仍使用原home/workspace路径,不需要数据迁移。回退将同时恢复 test(runtime): verify Codex cross-user workspace and memory isolation #404/test(platform): validate main-system Pilot readiness with Fake Connection #194 的隔离门禁。