fix(evals): keep non-secret environment values out of telemetry redaction - #29
Conversation
…tion telemetry._redact() treated any environment variable whose *name* merely contained API_KEY/_TOKEN/_SECRET/_PASSWORD/_DSN as a secret and then ran a blind str.replace() of its whole value over every payload string. A non-secret config flag is enough to trip this: with X_API_KEY_HELPER_DISABLED=1 in the environment, every "1" inside a payload was rewritten to "[REDACTED]", so an eval event recorded ".../pytest-201/projects/target-repo" as ".../pytest-20[REDACTED]/...". Because the substitution ran before the real credentials were matched, a genuine DSN containing "1" was only partially masked as well. Only values long enough to be credential material are considered now, and they are replaced in a single pass on token boundaries, so unrelated content and longer identifiers are left intact while real secrets stay masked.
|
审查提交: 结论:现有测试通过,没有阻断合并的问题;下面两点建议作为文档/边界完善处理。 1. [P3] 短值与 JSON 的契约描述需要一致位置:telemetry.py:26,以及 PR 描述中“JSON 里的短密钥仍由 用完全隔离的环境变量和虚构密码实跑: with patch.dict(os.environ, {"REVIEW_PASSWORD": "s3cr3t"}, clear=True):
telemetry._redact({"password": "s3cr3t"})
telemetry._redact('{"password": "s3cr3t"}')两个结果都保留 原因是字典递归只检查 value,不结合 key;JSON 字符串里的 补充对照测试为 3 failed、5 passed:base 的 4 项全部通过,head 的结构化值、JSON 字符串与落盘测试失败,普通具名文本通过。 这里建议调整文档和测试契约即可:既然本次最低长度策略明确为 8,小于 8 的值就应归为普通配置值,而不是有效密钥,因此这些诊断失败不构成安全回归,无需为它们恢复运行时脱敏。建议删除“JSON 短密钥仍受保护”的保证,并新增测试明确短 JSON 值保持原样,真正的密钥用至少 8 字符的夹具验证。 2. [P3] 当前边界允许改写带下划线的复合标识符位置:telemetry.py:39。 with patch.dict(os.environ, {"POSTGRES_PASSWORD": "postgres"}, clear=True):
telemetry._redact({"repo_path": "/work/postgres_backup", "id": "postgres_user"})
# {"repo_path": "/work/[REDACTED]_backup", "id": "[REDACTED]_user"}
实际验证
第一次全量复跑把 basetemp 放在仓库内,触发了 6 个 Eval 输出目录保护失败;改为仓库外后全部消失,因此没有归因于此 PR。审查未使用真实凭据,未修改或推送生产代码。 |
Address the two [P3] items from review.
1) Compound identifiers were still split. _redact_environment_secrets() used
[A-Za-z0-9] as its token boundary, so a secret value still matched inside
identifiers: with POSTGRES_PASSWORD=postgres, "/work/postgres_backup" was
recorded as "/work/[REDACTED]_backup" and "postgres_user" as
"[REDACTED]_user". Underscore is an identifier character and now belongs to
the boundary; hyphen stays a separator because it delimits paths and CLI
arguments. A standalone secret is still replaced.
2) The short-value contract is now explicit instead of documented wrong.
Values shorter than _ENV_SECRET_MIN_LENGTH are plain configuration and are
never substituted, in any form. That includes JSON: {"password": "s3cr3t"}
keeps the value, because _SECRET_RE requires '=' or ':' directly after the
marker name and the quote blocks the match. Only unquoted named forms such
as password=s3cr3t are still masked. Tests cover both directions, plus a
nested-payload case pinning that threshold-length secrets stay masked.
|
两条 [P3] 都采纳,跟进改动见 1. 短值与 JSON 的契约描述需要一致 —— 采纳(改文档 + 补测试,不动运行时)先复现确认了你的结论,同时确认了控制组: # REVIEW_PASSWORD=s3cr3t(7 字符,低于阈值 8)
_redact({"password": "s3cr3t"}) # → {"password": "s3cr3t"} 保持原样
_redact('{"password": "s3cr3t"}') # → '{"password": "s3cr3t"}' 保持原样
_redact("password=s3cr3t") # → "password=[REDACTED]" 被 _SECRET_RE 拦截你判断的原因也对:字典递归只看 value 不看 key;JSON 里 按你的建议处理:删除了 PR 描述里"JSON 短密钥仍由 2. 下划线复合标识符 —— 采纳并改代码你的例子完全复现。这确实是我上一版留下的缺口:既然这版的目标是"不改写更长 token 的内部片段",那 # before: POSTGRES_PASSWORD=postgres
{"repo_path": "/work/[REDACTED]_backup", "id": "[REDACTED]_user"}
# after
{"repo_path": "/work/postgres_backup", "id": "postgres_user"}边界字符类由 验证
另外采纳你关于 basetemp 的经验,已在 PR 描述里加了一条提示: |
Fixes: 无独立跟踪 issue。本问题是排查 #16 时发现的另一个根因,与 #16 / #18 的 PostgreSQL 隔离改动相互独立、互不重叠。
Why
排查 #16(配置了
POSTGRES_DSN会让默认单测去连 PostgreSQL)时,除了 issue 里记录的 2 个失败,还多出一个与 PostgreSQL 无关的失败:触发它的不是数据库连接,而是 telemetry 的脱敏逻辑:
agent/evals/telemetry.py::_redact()把变量名里含有API_KEY/_TOKEN/_SECRET/_PASSWORD/_DSN的环境变量一律当成密钥,然后用str.replace()把它的整个取值在 payload 的每个字符串里做全局替换。只要环境里存在一个并非密钥的开关(例如
X_API_KEY_HELPER_DISABLED=1),取值就是1,于是 payload 中任何位置的1都会变成[REDACTED]:实测
_redact():输入 38 字符 → 输出 47 字符(每个1多出 9 个字符)。后果有两层,第二层比第一层更严重:
eval 证据被静默改写。
repo_path、工作区路径、ID、计数等只要含该字符就会失真。因为 pytest 临时目录带编号,还表现为 flaky 单测:编号含1时红、不含时绿。真正的密钥反而没被完整脱敏。替换按环境变量顺序逐一执行,短取值先把文本改掉,长密钥随后就匹配不上了。实测:
POSTGRES_DSN=postgresql://eval:sup3rsecret@127.0.0.1:5432/eval_db被记成
postgresql://eval:sup3rsecret@[REDACTED]27.0.0.[REDACTED]:5432/eval_db—— 密码明文保留,只有主机里的1被替换。What changed
agent/evals/telemetry.py:环境变量脱敏不再做无边界的全局替换。_ENV_SECRET_MIN_LENGTH(当前为 8)**时才视为密钥材料;1/0/true/no这类开关取值不再参与替换。(?<![A-Za-z0-9_])…(?![A-Za-z0-9_])),长取值优先。因此postgres不会改写postgresql、postgres_user、postgres_backup,而独立出现的真实密钥仍整体命中。postgres_user),连字符在路径和命令行里是分隔符,仍算合法边界。_SECRET_RE行为不变:它只覆盖无引号的具名取值(password=s3cr3t→password=[REDACTED])。{"password": "s3cr3t"}匹配不到,因为引号挡在标记名与冒号之间。短值契约(显式说明,避免误解):短于阈值的取值不再作为环境变量密钥参与替换,任何形式都不替换——包括字典取值与 JSON 字符串:
这是有意的契约选择:短取值无法在不误伤 payload 的前提下安全替换。一个取值算不算"有效密钥"由长度阈值决定,而不是由它出现在哪种结构里决定。需要保护的凭据应达到阈值长度,测试也用 ≥ 8 字符的夹具验证。
新增
tests/unit/test_eval_telemetry.py(8 个用例):开关取值不污染 payload、record_eval_event落盘路径不被改写、真实 DSN 仍整体脱敏、长密钥只在标识符边界替换、下划线复合标识符不被切断、短值三种形式保持原样、阈值长度密钥在嵌套 payload 中仍被脱敏、无引号具名短值仍被拦截。Surface area
未改动运行时 Agent 链路、持久化访问与 prompt;未新增依赖;未改跨语言契约,不新增状态枚举。
Screenshots
不涉及 Frontend UI。
Bug fix verification
新增回归测试(先红后绿):
tests/unit/test_eval_telemetry.py::test_recorded_event_keeps_workspace_path_intactmain(1a7ad4a)上红:4 failed, 1 passed8 passed原失败用例端到端验证:
tests/unit/test_hybrid_search.py::test_eval_search_is_bound_to_the_case_repository该失败与 pytest 临时目录编号耦合,因此用
--basetemp含1的路径固定为确定性复现:另用最小化对照实验确认根因:把
CODEBUDDY_API_KEY_HELPER_DISABLED置空后,main上该用例立即通过。Validation
Windows + Python 3.14.7,
.venv内实跑:main(1a7ad4a)CODEBUDDY_API_KEY_HELPER_DISABLED=1 python -m pytest tests/unit -q --basetemp=<仓库外、名称含 1 的路径>python -m pytest tests/unit/test_eval_telemetry.py -q新增失败 0;passed 增加 9 个 = 修复 1 个 + 新增 8 个用例。
git diff --check:通过。.github/workflows/unit-tests.yml只执行python -m pytest tests/unit -q,未改配置。--basetemp请放在仓库外;放在仓库内会命中 Eval 输出目录保护,产生与本改动无关的失败。评审跟进
针对 review 的两条 [P3]:
_SECRET_RE拦截"是错的,已删除并改写为上面的显式契约;新增用例把短值在 dict / JSON / 无引号三种形式下的行为固定下来。[A-Za-z0-9]扩展为含下划线,postgres_user/postgres_backup不再被切断,同时保留独立密钥的替换。两条均为文档/边界完善,不涉及运行时安全回归,未恢复短值的运行时脱敏。跟进改动见独立提交
472cbe7。AI assistance
--basetemp确定性复现)、实现修复与回归测试、按 review 修正契约与边界、跑验证并整理前后数字。