Skip to content

feat(security): 工作区里有 .env 就拒绝启动 Codex,逃生阀显式且不静默 (#52) - #53

Merged
LouisDM merged 1 commit into
mainfrom
fix/issue-52
Aug 12, 2026
Merged

feat(security): 工作区里有 .env 就拒绝启动 Codex,逃生阀显式且不静默 (#52)#53
LouisDM merged 1 commit into
mainfrom
fix/issue-52

Conversation

@LouisDM

@LouisDM LouisDM commented Aug 12, 2026

Copy link
Copy Markdown
Owner

Closes #52

维护者决策 实现:不搬 .env,把 happy path 从「会被无视的警告」改成「硬停 + 显式放行」。

改了什么

1. bin/lcwarn_env_in_workspace()guard_env_in_workspace(reg, allow)

.env 在 Codex 工作区这棵树里 → 打印完整警告后非零退出,不启动 Codex。
逃生阀:lc code --allow-workspace-secrets

报错说清三件事(决策第 1 条要求的信息密度,沿用 #49 那条量化文案):

⚠️  这次 Codex 的工作目录里有 .env:/path/to/airgap-coder/.env
   会话是 approval_policy = "never",模型发的 shell 命令不用批准就执行,一句 `cat .env` 就能读走 2 个上游的地址与凭证。
   0600 挡的是别的用户,挡不住以你的身份运行的 Codex。
   要避开:cd 到你自己的项目目录再跑 lc code——工具目录和工作目录是两回事。
❌ 已拒绝启动 Codex。确实要在这个目录里干活(比如用 Codex 改 airgap-coder 自己),加上 --allow-workspace-secrets 显式放行。

flag 会从透传给 Codex 的参数里摘掉。这条不是洁癖:Codex 不认识它,透传下去会以一句 unexpected argument 直接退出,用户看到的就成了「按提示放行之后反而更坏了」。按精确串摘,不做前缀匹配。

2. docker/entrypoint.sh:同机制,逃生阀是环境变量

AIRGAP_ALLOW_WORKSPACE_SECRETS=1。两处和 CLI 侧不同,都是刻意的:

  • 报错教的是挂载写法而不是 cd —— 容器里没有「换个目录跑」这个选项,用户能改的只有 -v
  • 只有精确值 1 才放行。=0 / =false / 空值都当拒绝,因为写下 =0 的人的意思显然是「关」,而这个检查里「静默地当成开」是最坏的失效方式。

3. 两条防退化约束(决策第 3 条 + 后续那条评论)

  • 放行时仍然打印完整警告。 放行不等于安全,只是「你知道你在做什么」。天天带着 flag 的开发者看到的东西和只警告的年代完全一样,不会更差。
  • CLI 侧不提供环境变量形式的全局开关。 export 一次就全局静默,那才是真正的退化。容器侧不得不用环境变量(那边没有命令行可加),代码注释里写明这是被迫、不构成先例。

按要求,bin/lc 里原来那段「不阻断的理由」和 entrypoint.sh:56-57 同样的注释都替换掉了,换成「为什么现在阻断」+ 上面两条约束,免得将来有人读到旧注释又改回警告。A 方案被否的理由(离线包自包含)也留在注释和 threat-model 里,附 issue #52 链接作为将来重新评估的起点。

4. 文档

文件 改动
README.md / README.zh-CN.md #46 / #50 那两处 [!IMPORTANT] 从「会警告」改成「会阻断」+ flag 说明;命令表补上 flag
docs/threat-model.md 缓解措施表格 + 那一节改成「默认拒绝,显式放行」,并写清为什么这比警告强(对开发者 ≈ 无变化,对照文档走的内网用户是决定性的),以及两条防退化约束
docs/offline-deployment.md 第 3 / 4 节 —— 这正是两次踩坑的 happy path
docs/troubleshooting.md 新增一节。这是个新的硬失败,用户撞上会来查文档,不写等于逼他们去删检查
CHANGELOG.md Breaking:原来在仓库目录里跑 lc code 的人现在需要 flag 或者 cd

怎么验证的

CI 断言(决策第 5 条要求各补三条,都在已被 ci.yml 调用的脚本里,不需要动 .github/

scripts/test-lc-commands.sh [6f2] 重写(ci.yml 已有 bash scripts/test-lc-commands.sh):

  1. 工作区有 .env → 非零退出、无 Traceback$CODEX_LOG 为空(压根没启动 Codex),文案含文件名 / 2 个上游 / cd 指引 / 已拒绝启动 Codex / 逃生阀名字
  2. 带 flag → 启动了(--profile beta)、仍然打印警告(文件名 + 2 个上游 + 显式放行)、flag 没透传给 codex、其它参数照旧透传
  3. 工作区没有 .env → 正常启动、不打印警告(防误报)

外加:仓库上一层目录也阻断(判据是「.env 在 CWD 树里」而不是 CWD == ROOT);带 flag 但不该响的目录里不许凭空报警;提示里不含 $CANARY_KEY / master key 的值。

变异测试:把 die(...) 换成 return,断言第 1 条会变红。没有这条,「非零退出」这个断言可能是靠别的原因满足的——找不到注入点就直接 ::error:: 报红,不让它静默失效(照 [6d] 已有的形态写)。

scripts/test-entrypoint.shci.yml 已调用):同样三条,另加「只有 =1 放行,0 / false / 空值都必须拒绝」和「拒绝时 $WORK/args 为空」。

本地全套(runner 无 GPU / 无 API Key)

py_compile bin/lc scripts/*.py            ✅
bash -n scripts/*.sh docker/entrypoint.sh ✅
lc --help / lc help                       ✅
test-project-metadata.py                  ✅
test-entrypoint.sh                        ✅
test-codex-review-script.sh               ✅
test-version.sh                           ✅
test-doctor-probe.sh                      ✅
test-export.sh                            ✅
test-lc-secrets.sh                        ✅
test-lc-commands.sh                       ✅ (含新增 20 条 + 变异测试)
lc sync(假 registry + 假 .env)→ 四条生成器断言 ✅,跑完已删除
git status --short 干净,无 .env / registry.json / 生成物

真实模型路径(smoke.py / test-codex.sh)本地跑不了,未验证。

不做(决策明确排除)

  • 不搬 .env
  • 不加 AIRGAP_CODER_ENV 这类位置配置项

关 issue 的方式

按决策最后一条,合并后在 #52 的关闭评论里保留 A 方案的完整取舍分析,作为将来出现第三条暴露路径时重新评估的起点。

🤖 Generated with Claude Code

`lc code` 在 CWD 里启动 Codex,配置是 approval_policy = "never";容器里更是
danger-full-access。工作区里躺着 .env 的话,一句 `cat .env` 就能读走全部上游的
地址与凭证——#42 收紧的进程环境这条读法完全绕过去了。

#46 / #50 都只做到「警告 + 文档」。#52 评估了把 .env 搬出仓库目录(A 方案),
结论是不搬:它会破坏「解包 → 目录里什么都有 → 删目录即干净卸载」这个对隔离网
交付的承诺。改成默认拒绝 + 显式放行。

- bin/lc: warn_env_in_workspace() → guard_env_in_workspace(reg, allow),
  非零退出;`lc code --allow-workspace-secrets` 放行。flag 从透传给 Codex 的
  参数里摘掉(Codex 不认识它,会以 unexpected argument 退出)。
- docker/entrypoint.sh: 同机制,逃生阀是 AIRGAP_ALLOW_WORKSPACE_SECRETS=1;
  报错教的是挂载写法而不是 cd(容器里没有别的目录可去)。
- 两条防退化约束写进代码注释,替换掉原来那段「不阻断的理由」:
  放行时仍然打印完整警告;CLI 侧不提供环境变量形式的全局开关。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant