Repository navigation
fix(presentation): mask unlabeled provider tokens in chat activity - #7718
JasonBuildAI wants to merge 2 commits into
Conversation
The activity masker claimed deliberately broad coverage because a missed secret is stored for replay, but its issuer arm knew only six prefixes. Commands and reasoning summaries carrying npm, PyPI, Google, AWS session, Slack session or JWT shapes kept the raw value in the step that ChatSessionStore persists, the workspace SSE stream replays and the native steward copies into monitor_restart_basis. Compose the arm from PROVIDER_TOKEN_SHAPE_PATTERN_SOURCE, the canonical public-safety owner the sibling scanners already consume (Refs loopx-project#5136, loopx-project#7635, loopx-project#7636), and keep the legacy arm for the generic `sk-` form that owner does not claim. Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
…7718 Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
Assert the visible command stays readable around the masked value, and append the step to a real ChatSessionStore then read it back, so the regression covers the persistence the incident report describes rather than only the in-memory title. Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
songoow
left a comment
There was a problem hiding this comment.
Reviewer: model_agent · GPT-6 Astra Pro · OpenAI · self_reported
动机 / Motivation
在 Chat 查看 Agent 命令与工作活动、或重新打开既有会话的用户会遇到这类泄漏。例如一次发布命令把无标签 npm 凭据带入活动标题,旧版会把原值保存并在恢复会话时再次显示;本次在生成活动摘要时遮盖该值,保留命令用途和周围文字。独立探针通过真实 ChatSessionStore 写入、flush、重新打开和 public_activity_event 读取,确认七类基线会重放的合成凭据在新 head 均不再出现。范围是新生成的活动摘要和后续重放;不改原始执行命令,不重写既有历史文件,也不承诺发现所有未知凭据格式。
改动思路 / Approach
活动摘要 owner 复用 canonical 发行方模式,保留原有 shell/header 遮盖规则;识别共享、遮盖策略留在展示边界,既不改执行输入,也不增加后台任务。本 PR 保留为活动生成器的一条遮盖分支和真实存储重放回归,不另建 sanitizer、历史迁移或执行权限层。不修改会保留已经实际复现的泄漏;只在更晚的消费者拒绝也不能撤回已写入或公开的原值。修复放在现有 owner,正常输入沿原有路径继续处理,不增加重复状态、配置同步或人工确认步骤。对相关四个凭据 PR 的有界比较确认,它们面向不同目的地,能够共享识别规则而保持各自拒绝或遮盖策略。
具体改动 / Changes
完整审阅范围:2 files, +61/-0; production +8, new focused test file +53. 原始问题依据为 https://github.com/loopx-project/loopx/issues/7717,文本版本 body-sha256:06762c9c50ce34a40d8428433237bef1bc65450dc027773fa5c3da4fa88b2a7d;维护者接受的共享识别、目的地策略边界见 #5136。ACT-7717-1:implemented; ACT-7717-2:implemented; ACT-7717-3:implemented,对应识别完整性、兼容控制和真实输出边界,未把更大路线图当成此 PR 的完成条件。
关键代码讲解
-
_SECRET_PATTERNS(loopx/presentation/codex_activity.py:55):增加 canonical provider arm,保留 generic sk-、shell 值和 header 旧分支。 -
_mask_secrets(loopx/presentation/codex_activity.py:84):新增模式用空 capture group 保留 replacer 的 group(1) 接口;模式正文均为 noncapturing,不会错拼前缀。 -
CodexActivitySteps.started(loopx/presentation/codex_activity.py:163):摘要先遮盖再限制长度,输出由既有 buffer/store 保存;真实重开后仍然没有原凭据。
对主干的风险 / Risks
验证资格阻断:当前结论为 REQUEST_CHANGES;没有发现需要修改生产代码的 finding。 当前 build / research-display-language 出现第二个研究详情点击后等待open超时。虽然源码与调用路径检查支持它与本次补丁分离,现行 review policy 仍要求同一检查在 immutable base 与 exact head 上执行并取得相同归一化失败签名;本轮未完成这组浏览器配对,不能把静态排除或一般 merge hold 替代该条件。最小补充:对immutable base2f544f96和当前head运行相同research-display-language场景,保留同一输入、viewport、等待点及实际结果;也可提供既有可核验配对产物。 若配对确认是既有问题,即可据已有完整代码/真实消费者证据重评,无需为了本 review 改动无关业务代码。
未发现阻断性代码问题。共享模式的形状启发式不是秘密检测证明;长 PyPI 形状普通文本可能被遮盖。旧的已持久化原文不会被本补丁批量重写。实际生效调用链为 CodexActivitySteps.started/completed -> _mask_secrets -> _TurnEventBuffer.emit -> ChatSessionStore -> public_activity_event/SSE。最需要反证的是“模式正确,但在落盘/发布之后才被调用”;我使用同一个合成 corpus 跑 immutable base 与当前 head 的真实消费端,且把新版本的无泄漏 oracle 放到基线确认会失败,避免只测试新 helper 自洽。
PASS:.venv/bin/python -m pytest -q tests/test_chat_activity_provider_tokens.py tests/test_chat_activity.py,41 passed in 1.88s。既有 shell 引用、generic sk- 与合法命令周围内容的测试均保留;历史原始事件不会被这个生成端补丁自动重写。base/head 消费探针均有可重放脚本及原始结果,未调用真实模型、凭据或操作者状态。安全输入和拒绝后的继续处理均单独检查;No new refusal or operator recovery step; the existing stream and persisted read continue with a masked title.
语义与 CI 对齐
复用 existing vocabulary,没有新增工作状态分类或扩大 authority。当前 CI 有失败:JUnit 暴露 quota reason、native restart 平台条件、skill 链接和 dashboard launcher 测试;独立选取其中9个节点,在本 head 与2f544基线均为2 passed/7 failed,7个失败节点与规范化 message 完全一致。Windows self-update 4败、research-display-language 浏览器超时也已读取具体日志,但未在本机 Windows/浏览器重跑;它们的 owner 与本次活动遮盖分支分离,仍作为独立合并检查缺口报告,未宣称已修复或全绿。这些红灯没有被改写成通过;已复现的基线失败与本次改变的 invariant 分开归因,未完成的平台/浏览器复验和合并门槛继续明确保留。
当前失败的具体归因与审阅范围
补充 current Windows 失败归因:四个 self-update 节点先由 snapshot fixture 生成 Windows 上 apply_supported=false 的 plan,再只覆盖 pip/pipx driver;execute_update_plan 在检查该 plan 时立即返回 ok=false,尚未进入安装或本次改变的代码路径。三个相关 PR 与各自基线的 self_update.py、runtime_activation.py 和这份测试文件逐字一致。独立使用同一个 pytest 节点清单,仅在该 owner 注入 os.name=nt 的平台观测,在 immutable base 与 exact head 都复现4 failed、逐节点 message 相同;这限定证明已观察到的确定性早退原因,不冒充完整 Windows 操作系统回归,也不声称红灯已修复。
当前 build 的浏览器失败位于 research-display-language:点击第二个研究详情后等待 open 超时。整个 dashboard 树、场景、history backend、analysis store 和 Explore result owner 的 Git 对象在 base/head 相同;该 fixture 的 Chat store 为空、native executable 不可用且所有POST返回405,没有进入本次改变的 CodexActivitySteps 活动生产路径。活动文本的必要持久化/重放证明由本地真实 store 配对覆盖;该研究详情超时的因果范围已有说明,但所要求的同检查配对执行尚缺,不能据此批准。
pytest、checks 与 merge-gate 是现有 workflow 的上游结果汇总,不视作额外独立根因;对应 Python/Windows/浏览器当前失败分别按上述具体路径处理。这次因当前CI归因证据不足而暂不批准;未更改或绕过远端必需状态检查。
我的整体评价 / Verdict
对 exact head 3bcafd97dd439400219ddaec489f4836b39f264c 给出 REQUEST_CHANGES,原因仅为上述当前CI归因资格缺口,未发现代码finding。长期重复输出的凭据暴露风险改善,普通任务和后续读取流程保留,代码体量与已证实的问题相称。已检查 codex_activity 的文本生成与 chat_server 的既有重放消费;没有修改 TSX、布局、字段折叠或交互控件,文字遮盖回归由内容与重放断言验证,不要求重新截图。未运行页面布局截图;此 PR 没有布局或交互变更。该修复本来就作用于既有默认输出边界,没有伪称为 opt-in。建议维持当前最小 owner 修复范围,未来精度调整集中在 canonical 模式与公共 corpus;补齐该配对归因后可复用本次已完成的代码与消费者验证;不要求扩大产品修复范围。
English verdict: REQUEST_CHANGES — current required CI attribution is incomplete. No blocking code finding was identified; provide the same browser check at immutable base and exact head, or equivalent verifiable existing paired artifacts, without relaxing its assertions or changing unrelated code.
配对归因:
|
| 修订 | 运行 | 结果 | 场景耗时 |
|---|---|---|---|
base 2f544f969 |
3 | 3/3 通过 | 28.95s / 28.40s / 27.83s |
head 3bcafd97d |
3 | 3/3 通过 | 28.92s / 29.44s / 29.01s |
局限:本机是 Windows/Node 26,不是 CI 的 ubuntu/Node 22 打包环境;这组配对只证明同场景在 base 与 head 行为一致,CI 侧归因以上面 1–3 为准。
5. 结构范围与验证
- base→head 全量 diff 只有两个文件:
loopx/presentation/codex_activity.py(+8,活动摘要的遮盖分支)与新增tests/test_chat_activity_provider_tokens.py;research-display-language的 fixture Chat store 为空,不进入CodexActivitySteps的活动生产路径。 - 本 head 复跑聚焦测试(
PYTHONUTF8=1,规避 Windows 默认 GBK 读取噪音):tests/test_chat_activity_provider_tokens.py+tests/test_chat_activity.py→ 41 passed。
请求
请按"既有问题"重评 exact head 3bcafd97d。若需要,我可在批准后把分支 rebase 到当前 main 再跑一轮 CI;现在保持 head 不变,以便复用已完成的代码与消费者验证。
English verdict: the requested paired attribution is complete — the same job at the exact head passes on rerun (and the same scenario already passed at that head in dashboard-browser (2)), a docs-only PR hit the identical wait before passing with unchanged scenario code, and local base/head runs are 3/3 each. The research-display-language timeout is a pre-existing environmental flake; no production code changed. Please re-review 3bcafd97d.
Importance And Impact
A Personal Workspace chat owner's pasted npm, PyPI, Google, AWS session, Slack session or JWT credential is currently stored in cleartext in the local chat session and replayed in the workspace activity stream, because the activity masker recognizes only six of the nine issuer families that the canonical public-safety owner already covers; with this change the eight out-of-list families move from retained 8/8 to masked, so no raw token reaches
agent.phasepersistence, SSE replay, ormonitor_restart_basis.Fixes #7717
Goal And Delivered Outcome
upstream/main2f544f969.loopx/presentation/codex_activity.pypromises broad masking because a missed secret is stored for replay, but its issuer arm knew onlysk|ghp|gho|ghs|github_pat|xox[abp]whilePROVIDER_TOKEN_SHAPE_PATTERN_SOURCEinloopx/public_safe_text.pycovers nine families plus JWT (the owner the sibling scanners were consolidated onto in [Bug]: the builtin public-safe-outbound scan certifies GitHub, AWS, Slack, Google, npm, PyPI and Stripe credentials as public-safe #7635/fix(capability): detect unlabeled provider tokens in the outbound scan #7636 under [Architecture]: two modules both claim to own "private-looking text" #5136).Fixes #7717; baseloopx-project/loopxmain.Problem
CodexActivityStepsmasks credential-like command and reasoning text before a step is emitted, and the module states the trade-off explicitly: hiding a harmless value costs nothing, while a missed secret would be stored for replay. That step rides theagent.phaseevent:_TurnEventBuffer.emithands the payload toChatSessionStore.append_event,public_activity_eventreplays command steps unchanged over SSE, andStewardTurnHostcopies the phases intomonitor_restart_basis. An issuer-prefixed value needs no label, so the label arms (Authorization,Bearer,--token=,*_secret=, ...) cannot see it; only the narrow issuer alternation could, and it omitted npm, PyPI, Google, AWS access/session, Slack session and JWT shapes that the canonical owner already recognizes.Change
loopx/presentation/codex_activity.py: add one arm composed fromPROVIDER_TOKEN_SHAPE_PATTERN_SOURCE(imported from the canonical owner) withre.I, plus the comment explaining why the list is not restated locally. The existing legacy arm stays because it also covers the genericsk-form that the canonical owner intentionally does not claim.tests/test_chat_activity_provider_tokens.py(new): parametrized regression over the eight out-of-list families, asserting the token is absent from the serialized step,***appears and the surrounding command text survives, for both a command step and a reasoning summary; a ninth case appends the masked step to a realChatSessionStoreand reopens it to prove the persisted payload is clean. The literals are assembled at runtime so the test source is clean for the public boundary scan.No dependency, protocol, or state change; no behavior change for the shapes that were already masked.
Evidence
Before (baseline
loopx/presentation/codex_activity.pyrestored withgit checkout HEAD~1 -- <path>):After (this head
3bcafd97d):Local Windows caveat, reproducible against the baseline module file and therefore not caused by it:
tests/test_native_steward_monitor.pystops atCodexChatAgentError ... executable_not_foundandtests/test_chat_agent.py::test_result_review_enforces_workspace_read_profile_on_the_original_thread_and_homefails atloopx/chat_agent.py:802withKeyError: 'set';tests/test_chat_agent.pyis not in the requiredwindows-powershelllane, so hosted CI remains the qualification for those paths.Author Declaration
Implemented against
loopx/public_safe_text.pyPROVIDER_TOKEN_SHAPE_PATTERN_SOURCEand the masking contract comment inloopx/presentation/codex_activity.py(lines 47-51) at2f544f969._SECRET_PATTERNScanonical armtests/test_chat_activity_provider_tokens.py9/9 before → after, including a real store reopensk-masking is preservedtests/test_chat_activity.py::test_credentials_in_commands_are_masked_before_replayPROVIDER_TOKEN_SHAPE_PATTERN_SOURCEtests/control_plane/test_public_safe_text_owner_parity.py239 passed with the sibling suitesgit diff --check, the realloopx checkboundary scan and canary premerge. Read the persistence and replay path end to end (chat_runtime.py_TurnEventBuffer.emit→append_event,chat_server.pySSE replay,steward_turn.pyphase copy). Deliberately left out: PEM private-key blocks are not part of the canonical provider-shape owner and remain unmasked; label arms and path redaction were not changed.Scope And Continuation
Validation
3bcafd97dstaticpassedruff checkon both files;git diff --check;py_compilevia premerge direct checksunitpassedregression_paritypassedreal_entrypointpassedloopx check --scan-pathover both changed files:public boundary scan clean: 2 filesintegrationpassedloopx canary premerge --from-git-diff: direct checks 6/6, catalog canaries 2/2, 0 failuresreal_backendnot_applicablemanualnot_runFrontend / Visual Evidence
Type of Change
LoopX Area
Technical Direction
Shared-authority RFC fixture impact
Boundary Checklist
.loopx/,.codex/goals/, and liveACTIVE_GOAL_STATE.md).none.Signed-off-bytrailer (git commit -s).AI Disclosure
AI disclosure: this change was drafted with AI assistance at my request, under my direction and review.