
oh-my-openagent 反射沙箱修复实录当 bwrap 存在却不可用时如何在降级与失败关闭之间做出选择【免费下载链接】oh-my-openagentOmO: Just type mass ulw keyword with your prompt. Now you are the master of graph engineering.项目地址: https://gitcode.com/gh_mirrors/oh/oh-my-openagent导读本文围绕 oh-my-openagent 的 memory 反射reflection子进程沙箱机制展开深入剖析 issue #6873以及 #7095 的 Defect 1在 Ubuntu 24.04 且内核开启kernel.apparmor_restrict_unprivileged_userns1的环境下/usr/bin/bwrap虽然存在却无法创建用户命名空间导致反射子进程在 spawn 阶段即全部死亡、反射通道永久硬失败。读完本文你将掌握 bwrap 可用性探针smoke probe的设计原理、required/auto/off三种沙箱策略的语义边界以及该问题对应的测试驱动修复与验证方法。问题背景存在性检查为何成为故障源根因exists不等于usable在 oh-my-openagent 的 memory 组件中Linux 平台的反射子进程沙箱选择逻辑位于 sandbox-platform.ts。修复前的实现只做存在性检查只要which(bwrap)能解析出/usr/bin/bwrap就认为沙箱可用。这一假设在 Ubuntu 24.04 上被打破当内核参数kernel.apparmor_restrict_unprivileged_userns1开启后AppArmor 会阻止非特权进程创建用户命名空间。bwrap 二进制本身存在且可执行但每一次实际调用都会在 spawn 时立即死亡输出bwrap: setting up uid map: Permission denied于是出现了一个灾难性的组合存在性检查通过 → 反射子进程被包进 bwrapbwrap 在 sandbox setup 阶段失败 → 每个反射子进程 spawn 即死反射通道reflection lane在每次触发时都硬失败且无法自愈。源码 sandbox-bwrap-probe.ts 的模块注释精确记录了这一背景On Ubuntu 24.04 withkernel.apparmor_restrict_unprivileged_userns1, /usr/bin/bwrap is present and executable while every invocation dies withbwrap: setting up uid map: Permission denied, so an existence check alone selects a sandbox that kills every child at spawn (issue #6873).雪上加霜失效的修复提示另一个并发缺陷来自 worker/remediation.ts 的修复提示remediation hint。修复前bwrap 设置失败会被归入通用的child_exit分支提示用户去查看runtime/reflection-sessions/runId/child-stderr.log但问题在于bwrap 死在 sandbox setup 阶段反射子进程从未真正 exec根本不会写这个日志文件而且运行目录在提示渲染时早已被清理prunedchild-stderr.log是一个指向已删除文件的死指针。修复设计从存在性检查到可用性探针核心数据结构SandboxUsabilitysandbox-bwrap-probe.ts 引入了一个判别联合类型把可用/不可用二元判断升级为可用/不可用原因export type SandboxUsability | { readonly usable: true } | { readonly usable: false; readonly reason: string }不可用时的reason必须非空并携带 bwrap stderr 的尾部内容以便上层策略决策和用户提示都能拿到第一手失败信息。纯分类器classifyBwrapSmokeclassifyBwrapSmoke是一个纯函数分类器输入是一次 smoke 探测的原始结果BwrapSmokeResult输出SandboxUsabilityexport interface BwrapSmokeResult { readonly exitCode: number | null readonly timedOut: boolean readonly errorMessage?: string readonly stderr: string } export function classifyBwrapSmoke(result: BwrapSmokeResult): SandboxUsability { if (result.timedOut) return unusable(smoke test timed out after ${SMOKE_TIMEOUT_MS}ms, result.stderr) if (result.errorMessage ! undefined) return unusable(smoke test could not run: ${result.errorMessage}, result.stderr) if (result.exitCode 0) return { usable: true } return unusable(smoke test exited ${result.exitCode ?? without an exit code}, result.stderr) }分类规则清晰且覆盖四种结果结果判定reason 内容exit 0usable: true—非零退出码usable: false退出码 stderr 尾部spawn 错误usable: false错误信息 stderr 尾部超时ETIMEDOUTusable: false超时时长 stderr 尾部unusable辅助函数会把 stderr 修剪到最近 200 个字符REASON_STDERR_CHARS 200保证 reason 既有诊断价值又不至于失控膨胀。最小化 smoke 调用恰好触达用户命名空间设置探针的执行命令被刻意设计为能触发用户命名空间设置的最小调用const SMOKE_ARGS [--ro-bind, /, /, --proc, /proc, --dev, /dev, true] as const注释说明了设计动机bwrap 必须先执行 unshare 并写入 uid/gid maps然后才能 exec 内部命令——这正是 AppArmor 阻断的步骤。因此这个最小调用足以暴露无法创建用户命名空间的问题同时把探测开销降到最低。超时上限为SMOKE_TIMEOUT_MS 3_0003 秒防止挂起的 bwrap 阻塞反射通道。记忆化探针probeBwrapUsabilityconst verdicts new Mapstring, SandboxUsability() export function probeBwrapUsability(executable: string): SandboxUsability { const memoized verdicts.get(executable) if (memoized ! undefined) return memoized const verdict classifyBwrapSmoke(runBwrapSmoke(executable)) verdicts.set(executable, verdict) return verdict }probeBwrapUsability是按可执行文件绝对路径记忆化的真实探针。记忆化的必要性来自 facts 表面的工作方式facts surface 在每次 launch 时都会重建 transform见 sandbox.ts 与 wiring-runtime.ts如果不做记忆化每个反射触发都会 spawn 一个探测子进程成本不可接受。一个关键细节不存在的路径视为可用sandbox-platform.ts 中的defaultProbe有一个容易被忽略但至关重要的守卫function defaultProbe(executable: string): SandboxUsability { if (!existsSync(executable)) return { usable: true } return probeBwrapUsability(executable) }默认探针只探测当前机器上真实存在的可执行文件。这一设计的价值在于测试封闭性hermeticity测试代码会注入一个伪造的which返回/usr/bin/bwrap在没有 bubblewrap 的测试主机上existsSync为 false于是探针直接返回可用保持仅存在性语义——任何现有测试都不会意外 spawn 真正的 bwrap。而探针的单元测试则在mktemp临时目录中生成专用的 shell 替身来模拟 bwrap 行为而不是真的调用 bwrap。策略语义degrade 还是 fail closed策略类型与三态行为sandbox-contracts.ts 定义了沙箱策略export type SandboxPolicy required | auto | offbuildPathSandboxTransform在 sandbox-platform.ts 中将策略与探测结果组合出完整的决策矩阵策略探测结果行为off不探测恒返回 identity transformwasSandboxed: falseautobwrap 可用正常包裹子进程wasSandboxed: trueautobwrap 不可用降级为 identity transform附running unsandboxed because policy is auto警告并携带探针 reasonrequiredbwrap 不可用抛出SandboxUnavailableError失败关闭fail closed任意可执行文件未解析到off与解析失败时不探测required时抛错auto时降级并警告降级警告的完整文本为reflection sandbox unavailable on platform: reason; running unsandboxed because policy is auto而SandboxUnavailableError定义于 sandbox-contracts.ts的消息格式为required reflection sandbox unavailable on platform: reason此外auto策略下若 lock 路径的父目录不存在、或内部命令无法解析为绝对路径同样会以携带原因的方式降级为 identity而不是抛原始 ENOENT详见resolveLockPaths与guardedSandboxTransform。平台差异Darwin 从不探测在 sandbox-platform.ts 的resolveExecutable中平台选择非常明确if (platform darwin) return which(sandbox-exec) if (platform linux) return which(bwrap) return undefinedLinux 使用 bwrapDarwin 使用sandbox-execSeatbelt profile见buildDarwinProfile其他平台返回 undefined。Darwin 分支从不运行 bwrap 探针——可用性探测是 Linux 专属逻辑与沙箱工具的实现方式namespaces vs. Seatbelt严格对齐。修复提示指向正确的配置而非已删除的日志worker/remediation.ts 中的reflectionRemediation新增了 bwrap 专属分支if (/bwrap:|setting up (uid map|gid map|namespace)/.test(combined)) { return the sandbox helper (bwrap) cannot create a user namespace on this host; set memory.reflection.sandbox to off in your omo config, or allow unprivileged user namespaces on the host }当失败原因匹配bwrap:或setting up uid map|gid map|namespace时提示现在会明确指出问题bwrap 无法在本机创建用户命名空间给出两条可执行路径在 omo 配置中将memory.reflection.sandbox设为off或在宿主机上允许非特权用户命名空间不再引用child-stderr.log——因为 bwrap 死在 sandbox setup 阶段日志文件根本不存在且已被清理。同时通用的child_exit分支保留其原有的 child-log 提示inspect runtime/reflection-sessions/ /child-stderr.log两者职责分离spawn 前的沙箱失败指向配置spawn 后的子进程失败指向日志。源码结构的变化路径解析的抽取修复还带来了一次结构性重构。QA 文档记录的纯 LOC 数据250 行上限约束下文件修复后 LOC说明sandbox-platform.ts208原 231探针接线曾把它推到 259 行因此抽取路径解析sandbox-bwrap-probe.ts54新增探针逻辑sandbox-paths.ts50新增路径/可执行文件解析sandbox.ts78高层包装worker/remediation.ts27修复提示sandbox-paths.ts 抽取出的canonicalPath也解决了一个隐藏问题沙箱在反射子进程运行之前构建而runtime/reflection、runtime/reflection-sessions、agent 目录、XDG_CONFIG_HOME等路径在全新机器或首次反射时尚不存在。裸调用realpathSync会在第一个缺失祖先处抛ENOENT进而表现为 spawn 前的spawn_failed并卡死反射游标。canonicalPath的做法是只解析已存在的路径前缀把不存在的尾部按原样追加从而保证授权始终指向完整目标路径且绝不会放宽到某个祖先目录。测试驱动修复RED 先行GREEN 验证测试命令与范围QA 文档记录了在 worktreefix-6873-bwrap-usernsbun 1.4.0macOS arm64上执行的验证命令聚焦测试bun test packages/omo-senpi/src/components/memory/sandbox-bwrap-probe.test.ts packages/omo-senpi/src/components/memory/sandbox.test.ts packages/omo-senpi/src/components/memory/worker/remediation.test.ts packages/omo-senpi/src/components/memory/sandbox-facts.test.ts packages/omo-senpi/src/components/memory/sandbox-absent-paths.test.ts packages/omo-senpi/src/components/memory/sandbox-lock-invariants.test.ts完整 memory 组件套件bun test packages/omo-senpi/src/components/memory类型检查bunx tsgo --noEmit -p packages/omo-senpi/tsconfig.json扩展构建校验node packages/omo-senpi/plugin/scripts/build-extension.mjs node packages/omo-senpi/plugin/scripts/build-extension.mjs --checkRED先写测试观察失败实现之前、仅有测试时观察到的失败完全命中预期19 pass 6 fail 1 error Ran 25 tests across 3 files. error: Cannot find module ./sandbox-bwrap-probe from .../sandbox-bwrap-probe.test.ts (fail) reflection worker OS sandbox ... #when auto policy is used #then spawn arguments pass through with a warning naming the probe reason (fail) reflection worker OS sandbox ... #when required policy is used #then the build fails closed with a typed error (fail) reflectionRemediation ... #then the hint names the sandbox setting instead of the deleted child log Expected to contain: memory.reflection.sandbox Received: inspect runtime/reflection-sessions/runId/child-stderr.log三类失败各自对应正确的原因探针模块不存在、降级/失败关闭分支缺失、提示仍指向已删除的子日志。GREEN实现后的完整验证实现后QA 文档记录了四级验证结果聚焦沙箱 修复提示套件51 pass, 0 fail, 133 expect() calls覆盖 6 个文件完整 memory 组件套件926 pass, 0 fail, 3052 expect() calls覆盖 134 个文件耗时 289sbunx tsgo --noEmit -p packages/omo-senpi/tsconfig.jsonexit 0无诊断build-extension.mjs --checkexit 0omo-senpi extension build is current。为什么这套方案是充分的QA 文档从五个维度论证了修复的充分性策略分裂被测试钉死issue 的开放问题降级还是失败关闭由两个测试给出相反答案——同一个不可用探针 fixture在auto下产生降级、在required下产生SandboxUnavailableError策略分裂不可能静默回归。过度降级风险被单独钉死一个可用探针必须仍然产生被包裹的子进程且wasSandboxed true的测试防止修复从存在性检查过度乐观滑向一律降级过度悲观。探针成本由测试约束而非希望约束off策略和未解析的可执行文件都会注入一个必抛的探针任何未来在不该探测的路径上探测的代码路径都会让套件失败。封闭性是结构性的而非偶然的默认探针只在解析出的可执行文件真实存在于运行机器时才 spawn见defaultProbe因此所有注入伪造which的既有测试在无 bubblewrap 的主机上保持仅存在性语义探针单元测试用mktemp目录中的 shell 替身而非真实 bwrap。爆炸半径被实际演练而非假设完整 926 测试的 memory 套件覆盖了变更 builder 的两个生产消费者——identity-runtime.ts反射 transform与 wiring-runtime.tsfacts surface。残余风险与已知边界QA 文档诚实记录了残余风险探测通过但真实启动仍失败的宿主机可能出现 smoke 探测通过、但后续真实启动仍失败的情况例如内核策略在进程运行中途变更或单次调用间的竞态。该路径保留修复前的运行时失败行为但现在附带修正后的提示指向memory.reflection.sandbox而非已删除的日志文件。每次启动都重新探测被否决为每个反射触发 spawn 一次探测子进程对一个没有任何报告者遇到过的场景来说成本过高。被省略的内容同样明确当前环境没有可用的、开启apparmor_restrict_unprivileged_userns1的 Ubuntu 24.04 实机因此 AppArmor 拒绝是通过 shell 替身精确复现 issue 报告中的 stderr 来模拟的分类器被喂入 #6873 中原样的bwrap: setting up uid map: Permission denied字符串没有运行 live Senpi harness QA记录的门禁就是上述封闭单元/类型检查/构建集变更及其产物中不包含任何密钥、令牌或环境变量转储。给使用者的配置建议从 worker/remediation.ts 的提示文本可以提炼出两条面向用户的处置路径应用侧在 omo 配置中将memory.reflection.sandbox设为off让反射子进程以无沙箱方式运行宿主侧在宿主机上允许非特权用户命名空间即关闭或放宽kernel.apparmor_restrict_unprivileged_userns1的限制。对于保持auto策略的用户本修复确保系统不会因为一个存在但不可用的 bwrap 而陷入反射通道永久硬失败——它会带着明确的 reason 降级运行对于必须强制沙箱的用户required策略则会在不可用时快速失败并给出可诊断的错误而不是让每个反射子进程无声死亡。【免费下载链接】oh-my-openagentOmO: Just type mass ulw keyword with your prompt. Now you are the master of graph engineering.项目地址: https://gitcode.com/gh_mirrors/oh/oh-my-openagent创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考