fix(pi): bridge 重载后恢复一次性启动 env 的 bash 隔离 home - #3089
Conversation
#3070:cindy-bridge 首次加载时读取 CINDY_PI_BASH_PACKAGE_HOME 后即从 process.env 删除;Pi 进程内扩展重载会重新执行 bridge 文件,第二次加载 读不到已删除的 env,bashPackageHome 变 undefined,此后每次 bash 都在 isolatedBashEnvironment 处 fail-closed,报 Cindy isolated Pi package home is unavailable。 改为 takeOneShotEnv 统一存取:首次加载读删并 stash 进 globalThis (重载与首次加载共享同一 realm),重载实例从 stash 取回 host 注入的 原始值;stash 已有记录而 env 又出现值时视为进程内代码事后写入,忽略 并再次删除。CINDY_PI_PACKAGE_MANAGEMENT token 同法处理,重载后 cindy_pi_extension 工具不再静默消失。stash 拿不到时仍返回 undefined 保持原 fail-closed,不猜外来 runtime 的路径。 Signed-off-by: DavidShen <david@xd.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a233478bfb
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
| Filename | Overview |
|---|---|
| packages/maker-core/src/agents/pi/cindy-bridge-source.ts | 新增不可变的跨重载 bash package home stash、派生路径复核和 fail-closed 处理;此前报告的全局 stash 事后篡改路径已被封闭。 |
| packages/maker-core/src/agents/pi/tests/cindyBridgeSource.test.ts | 新增覆盖首次加载、重载恢复、env 篡改、canary 失配、恶意属性形态及缺失初始化的回归测试。 |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[首次加载 bridge] --> B[读取并删除 CINDY_PI_BASH_PACKAGE_HOME]
B --> C[以 non-writable / non-configurable 属性写入 globalThis]
C --> D[使用隔离 home 启动 bash]
E[bridge 重载] --> F[读取并校验 stash descriptor]
F --> G[由 PI_CODING_AGENT_DIR 派生预期路径]
G --> H{stash 与派生路径一致?}
H -->|是| D
H -->|否| I[返回 undefined 并 fail-closed]
Reviews (6): Last reviewed commit: "fix(pi): bash 隔离 home stash 防篡改并经 PI_COD..." | Re-trigger Greptile
review P1:CINDY_PI_PACKAGE_MANAGEMENT 是 host 签发的 bearer token, 而本进程内会加载用户安装的第三方托管扩展,它们与 bridge 共享 globalThis —— 把 token stash 进 globalThis 等于暴露给任意托管代码, 比留在首次加载闭包里更宽。 token 恢复原语义:读一次即删、仅闭包持有,重载后 cindy_pi_extension 工具退场(bash 直装仍被拦,新会话可再拿到工具,可接受)。 bash-package-home 是路径而非凭证,且本就经 PI_CODING_AGENT_DIR 进 每个 bash 子进程 env,保留 stash 不新增暴露面。 Signed-off-by: DavidShen <david@xd.com>
反馈已处理已处理:
变更:
验证:
备注:
|
greptile P1:plain object stash 可被同进程的第三方托管扩展改写 —— bridge 重载后会信任被污染的值,把攻击者目录当 PI_CODING_AGENT_DIR 传给 bash 与子代理。 修法: - stash 改为 defineProperty(writable:false, configurable:false),语言 层面封死事后改写/替换/删除;读取校验属性形态,可写可配置的属性 (含攻击者抢跑预置的)一律不信任。 - 重载取值加双重验证:stash 必须等于 path.posix.join(PI_CODING_AGENT_DIR, 'bash-package-home') —— 与 host 侧 joinRemotePosixPath 派生式逐字一致。 事后改写 PI_CODING_AGENT_DIR 会让二者失配 → fail-closed(该 env 常驻 可写,顺带变成 canary)。 - env 值在 stash 建立后再次出现一律删除并忽略;验证失败或 stash 缺失 返回 undefined,下游 isolatedBashEnvironment 保持原 fail-closed。 Signed-off-by: DavidShen <david@xd.com>
反馈已处理已处理:
变更:
验证:
备注:
|
MagicLizi
left a comment
There was a problem hiding this comment.
审查通过:bridge 重载后 bash 隔离 home 经防篡改 stash 恢复,token 不进 stash;回归覆盖重载路径。CI 全绿。review-only 本轮不合并。
MagicLizi
left a comment
There was a problem hiding this comment.
bash 隔离 home 用防篡改 stash 抗扩展重载,token 仍只进闭包。测试覆盖重载、canary 失配和 hostile stash。没有 P0/P1。
MagicLizi
left a comment
There was a problem hiding this comment.
审查通过,没有 P0/P1。bridge 重载后 bash 隔离 home 经 stash 与 PI_CODING_AGENT_DIR 双重验证才放行;包管理 token 不进 globalThis。CI 全绿。review-only 本轮不合并。
MagicLizi
left a comment
There was a problem hiding this comment.
审查通过:无 P0/P1。bridge 重载后 bash 隔离 home 的 stash/派生与 Host spawnEnv 一致,token 仍仅闭包持有;review-only 本轮不合并。
MagicLizi
left a comment
There was a problem hiding this comment.
审查通过:bash 隔离 home 用防篡改 stash + canary 抗扩展重绑,token 不进 globalThis。review-only,未合并。
MagicLizi
left a comment
There was a problem hiding this comment.
审查通过,无 P0/P1。bridge 重载后 bash 隔离 home 走密封 stash,缺路径仍 fail-closed。review-only 本轮不合并。
MagicLizi
left a comment
There was a problem hiding this comment.
独立审查无 P0/P1;CI 全绿。review-only 本轮不合并。
MagicLizi
left a comment
There was a problem hiding this comment.
Review passed: fix correctly stashes and re-derives bash package home on bridge reload. Security invariants preserved (SECRET_ENV_NAMES still strips sensitive values). Fail-closed behavior maintained when paths unavailable. Tests cover the key scenarios.
) (makecindy#3165) * fix(pi): 修复 subagent bash 因父 bridge 删除 env 而 fail-closed (makecindy#3132) 父 bridge 在初始化时消费并从 process.env 删除 CINDY_PI_BASH_PACKAGE_HOME; subagent 子进程通过 Object.assign({}, process.env) 构造 childEnv 时已 拿不到该变量,导致子进程里的 bridge 无法解析 bash 隔离 home,抛出 "Cindy isolated Pi package home is unavailable",subagent 内 bash 100% 必现。 修复两处: 1. cindy-bridge resolveBashPackageHome:env 与 stash 均不可用时(正是 subagent 子进程路径),从 PI_CODING_AGENT_DIR 派生 path.posix.join(configHome, 'bash-package-home');非绝对路径仍 fail-closed。公式与 makecindy#3089 stash 校验一致。 2. cindy-subagent runTask:spawn 前,configHome 为绝对路径时写回 childEnv[CINDY_PI_BASH_PACKAGE_HOME] = join(configHome, 'bash-package-home'), 让子 bridge 首次加载时能读到该变量(子 bridge 仍会在加载时删除它)。 不把 subagent 白名单改成含 bash(产品:仍是只读)。 Signed-off-by: Dash <dashhuang@gmail.com> * test(pi): 补 subagent childEnv 执行级测试(响应 greptile P2 makecindy#3165) 结构断言只检查源码文本顺序,无法防止 isAbsolute 分支或变量 绑定的控制流回归。新增 loadSubagentChildEnvHelper 以 runInNewContext 执行真实 childEnv 构造逻辑,断言: - 绝对路径时 CINDY_PI_BASH_PACKAGE_HOME 被正确设置 - 相对路径 fail-closed(不设置) - MCP bridge / remote MCP secret 仍被剥离 Signed-off-by: Dash <dashhuang@gmail.com> * fix(pi): 子进程 bash-package-home 改用 path.posix.join(响应 Codex P1 makecindy#3165) bridge derivedBashPackageHome 与 stash 校验都用 path.posix.join; 子进程 childEnv 写回若用 path.join(Windows 上产生反斜杠), 子 bridge reload 时 stash 与 posix 派生值不匹配 → fail-closed。 改为 path.posix.join 保持一致。测试断言随之更新。 Signed-off-by: Dash <dashhuang@gmail.com> --------- Signed-off-by: Dash <dashhuang@gmail.com>
* fix(pi): bridge 重载后一次性启动 env 经 globalThis stash 恢复 makecindy#3070:cindy-bridge 首次加载时读取 CINDY_PI_BASH_PACKAGE_HOME 后即从 process.env 删除;Pi 进程内扩展重载会重新执行 bridge 文件,第二次加载 读不到已删除的 env,bashPackageHome 变 undefined,此后每次 bash 都在 isolatedBashEnvironment 处 fail-closed,报 Cindy isolated Pi package home is unavailable。 改为 takeOneShotEnv 统一存取:首次加载读删并 stash 进 globalThis (重载与首次加载共享同一 realm),重载实例从 stash 取回 host 注入的 原始值;stash 已有记录而 env 又出现值时视为进程内代码事后写入,忽略 并再次删除。CINDY_PI_PACKAGE_MANAGEMENT token 同法处理,重载后 cindy_pi_extension 工具不再静默消失。stash 拿不到时仍返回 undefined 保持原 fail-closed,不猜外来 runtime 的路径。 Signed-off-by: DavidShen <david@xd.com> * fix(pi): 包管理 token 不进 globalThis stash,保持仅闭包持有 review P1:CINDY_PI_PACKAGE_MANAGEMENT 是 host 签发的 bearer token, 而本进程内会加载用户安装的第三方托管扩展,它们与 bridge 共享 globalThis —— 把 token stash 进 globalThis 等于暴露给任意托管代码, 比留在首次加载闭包里更宽。 token 恢复原语义:读一次即删、仅闭包持有,重载后 cindy_pi_extension 工具退场(bash 直装仍被拦,新会话可再拿到工具,可接受)。 bash-package-home 是路径而非凭证,且本就经 PI_CODING_AGENT_DIR 进 每个 bash 子进程 env,保留 stash 不新增暴露面。 Signed-off-by: DavidShen <david@xd.com> * fix(pi): bash 隔离 home stash 防篡改并经 PI_CODING_AGENT_DIR 双重验证 greptile P1:plain object stash 可被同进程的第三方托管扩展改写 —— bridge 重载后会信任被污染的值,把攻击者目录当 PI_CODING_AGENT_DIR 传给 bash 与子代理。 修法: - stash 改为 defineProperty(writable:false, configurable:false),语言 层面封死事后改写/替换/删除;读取校验属性形态,可写可配置的属性 (含攻击者抢跑预置的)一律不信任。 - 重载取值加双重验证:stash 必须等于 path.posix.join(PI_CODING_AGENT_DIR, 'bash-package-home') —— 与 host 侧 joinRemotePosixPath 派生式逐字一致。 事后改写 PI_CODING_AGENT_DIR 会让二者失配 → fail-closed(该 env 常驻 可写,顺带变成 canary)。 - env 值在 stash 建立后再次出现一律删除并忽略;验证失败或 stash 缺失 返回 undefined,下游 isolatedBashEnvironment 保持原 fail-closed。 Signed-off-by: DavidShen <david@xd.com> --------- Signed-off-by: DavidShen <david@xd.com>
) (makecindy#3165) * fix(pi): 修复 subagent bash 因父 bridge 删除 env 而 fail-closed (makecindy#3132) 父 bridge 在初始化时消费并从 process.env 删除 CINDY_PI_BASH_PACKAGE_HOME; subagent 子进程通过 Object.assign({}, process.env) 构造 childEnv 时已 拿不到该变量,导致子进程里的 bridge 无法解析 bash 隔离 home,抛出 "Cindy isolated Pi package home is unavailable",subagent 内 bash 100% 必现。 修复两处: 1. cindy-bridge resolveBashPackageHome:env 与 stash 均不可用时(正是 subagent 子进程路径),从 PI_CODING_AGENT_DIR 派生 path.posix.join(configHome, 'bash-package-home');非绝对路径仍 fail-closed。公式与 makecindy#3089 stash 校验一致。 2. cindy-subagent runTask:spawn 前,configHome 为绝对路径时写回 childEnv[CINDY_PI_BASH_PACKAGE_HOME] = join(configHome, 'bash-package-home'), 让子 bridge 首次加载时能读到该变量(子 bridge 仍会在加载时删除它)。 不把 subagent 白名单改成含 bash(产品:仍是只读)。 Signed-off-by: Dash <dashhuang@gmail.com> * test(pi): 补 subagent childEnv 执行级测试(响应 greptile P2 makecindy#3165) 结构断言只检查源码文本顺序,无法防止 isAbsolute 分支或变量 绑定的控制流回归。新增 loadSubagentChildEnvHelper 以 runInNewContext 执行真实 childEnv 构造逻辑,断言: - 绝对路径时 CINDY_PI_BASH_PACKAGE_HOME 被正确设置 - 相对路径 fail-closed(不设置) - MCP bridge / remote MCP secret 仍被剥离 Signed-off-by: Dash <dashhuang@gmail.com> * fix(pi): 子进程 bash-package-home 改用 path.posix.join(响应 Codex P1 makecindy#3165) bridge derivedBashPackageHome 与 stash 校验都用 path.posix.join; 子进程 childEnv 写回若用 path.join(Windows 上产生反斜杠), 子 bridge reload 时 stash 与 posix 派生值不匹配 → fail-closed。 改为 path.posix.join 保持一致。测试断言随之更新。 Signed-off-by: Dash <dashhuang@gmail.com> --------- Signed-off-by: Dash <dashhuang@gmail.com>
这次改了什么
摘要
修复 #3070:Pi 会话中途所有 bash 永久失败,报
Cindy isolated Pi package home is unavailable。根因:
cindy-bridge首次加载时读取 host 注入的CINDY_PI_BASH_PACKAGE_HOME后立即从process.env删除(#2771 引入的防御,防止进程内代码改写)。但 Pi 进程内扩展重载会重新执行 bridge 文件:第二次加载读不到已被删除的 env,闭包里的bashPackageHome变成undefined,此后每次 bash 都在isolatedBashEnvironment处 fail-closed。修法:新增
takeOneShotEnv统一存取一次性启动 env——首次加载读删并 stash 进globalThis(扩展重载与首次加载共享同一 realm),重载实例从 stash 取回 host 注入的原始值。stash 已有记录而 env 又出现值时,视为进程内代码事后写入,忽略并再次删除(不信任可改写的 env 面,保留原防御)。CINDY_PI_PACKAGE_MANAGEMENTtoken 同法处理,重载后cindy_pi_extension工具不再静默消失。stash 也拿不到时仍返回 undefined,下游保持原 fail-closed,不猜外来 runtime 的路径。与 #3072 的关系:修同一问题,但 #3072 直接不删 env,把专用变量永久留在
process.env里,进程内代码(含用户可装的第三方 Pi 扩展)可改写且重载的 bridge 会静默信任改写值;本 PR 保留删除防御,值经不可由 env 改写的 stash 传递,并补齐回归测试。变更类型
fix缺陷修复范围
cindy-bridge-source.ts新增takeOneShotEnv(stash 存取一次性启动 env)并让bashPackageHome/piPackageManagementToken走该路径;cindyBridgeSource.test.ts补回归测试SECRET_ENV_NAMES剥离面调整cindy_pi_extension工具重载后仍可用UI 变化
不涉及:仅 Pi 进程内扩展逻辑,无任何 UI 路径改动。
怎么验证的
自动验证
新增回归测试覆盖:同一进程内 double-load 仍取回原始值;首次加载后向 env 写入攻击值不影响重载(取 stash 而非 env);非 Cindy 初始化的进程返回 undefined 保持 fail-closed;多键互不干扰;入口结构断言(裸 delete 只存在于 helper 内)。
手工验证
在隔离沙盒 dev(
--isolated=@worktree,CN 区,commit 05264ab + 本修复)实测:开 Pi 会话跑git --version正常;关闭会话后重新打开(resume)再跑git --version,正常返回——修复前该路径报Cindy isolated Pi package home is unavailable且同会话内重试永久失败。未执行的验证
真实 Pi 进程内扩展重载(非 resume 路径)无稳定本地复现手段,由上述 resume 路径 + 单测的 double-load 用例覆盖同一机理(重载与首次加载共享 realm,env 已被首次加载删除)。
风险
风险分类
影响与回滚
影响范围:Pi bash 隔离与 Pi 扩展管理工具的一次性 env 生命周期。安全面上,值存于
globalThis与闭包同为进程内存,不比原实现(值留在首次加载闭包里)多暴露;bash 子进程 spawn 边界照旧剥离该变量(SECRET_ENV_NAMES不变)。事后写入 env 的值一律不信任,保留并强化了原delete防御。远端会话:bridge 源码字节进入CINDY_PI_EXTENSION_BUNDLE_HASH,字节变化本就要求 daemon restart,不影响 attach 语义。移动端冷更分析:本次改动不触及
apps/mobile任何 fingerprint 输入(app.json/app.config.js/eas.json/package.json/plugins/modules),仅改packages/maker-core的 Pi 扩展源码,不触发冷更。回滚 / 降级方式:revert 本 PR 即恢复旧行为(bridge 加载后删除 env、重载后 bash fail-closed)。
提交前检查
git commit -s,见 DCO)