Skip to content

fix(presentation): mask unlabeled provider tokens in chat activity - #7718

Open
JasonBuildAI wants to merge 2 commits into
loopx-project:mainfrom
JasonBuildAI:codex/agent-h7k3
Open

JasonBuildAI wants to merge 2 commits into
loopx-project:mainfrom
JasonBuildAI:codex/agent-h7k3

Conversation

@JasonBuildAI

@JasonBuildAI JasonBuildAI commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

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.phase persistence, SSE replay, or monitor_restart_basis.

Fixes #7717

Goal And Delivered Outcome

Problem

CodexActivitySteps masks 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 the agent.phase event: _TurnEventBuffer.emit hands the payload to ChatSessionStore.append_event, public_activity_event replays command steps unchanged over SSE, and StewardTurnHost copies the phases into monitor_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 from PROVIDER_TOKEN_SHAPE_PATTERN_SOURCE (imported from the canonical owner) with re.I, plus the comment explaining why the list is not restated locally. The existing legacy arm stays because it also covers the generic sk- 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 real ChatSessionStore and 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.py restored with git checkout HEAD~1 -- <path>):

$ python -m pytest -q tests/test_chat_activity_provider_tokens.py
...
tests\test_chat_activity_provider_tokens.py:36: AssertionError
FAILED ...::test_provider_tokens_in_commands_are_masked_before_replay[npm_...]
FAILED ...::test_provider_tokens_in_commands_are_masked_before_replay[pypi-...]
FAILED ...::test_provider_tokens_in_commands_are_masked_before_replay[AIza...]
FAILED ...::test_provider_tokens_in_commands_are_masked_before_replay[AKIA...]
FAILED ...::test_provider_tokens_in_commands_are_masked_before_replay[ASIA...]
FAILED ...::test_provider_tokens_in_commands_are_masked_before_replay[xoxs-...]
FAILED ...::test_provider_tokens_in_commands_are_masked_before_replay[xoxr-...]
FAILED ...::test_provider_tokens_in_commands_are_masked_before_replay[eyJ...]
FAILED ...::test_a_masked_step_stays_clean_in_the_reopened_store
9 failed in 1.13s

After (this head 3bcafd97d):

$ python -m pytest -q tests/test_chat_activity_provider_tokens.py tests/test_chat_activity.py
41 passed in 4.24s

$ python -m pytest -q tests/control_plane/test_presentation_public_safety.py \
    tests/control_plane/test_public_safe_text_owner_parity.py \
    tests/architecture/test_package_kernel_import_ratchet.py
239 passed in 35.04s

$ ruff check loopx/presentation/codex_activity.py tests/test_chat_activity_provider_tokens.py
All checks passed!

$ git diff --check
exit=0

$ loopx check --scan-path tests/test_chat_activity_provider_tokens.py \
    --scan-path loopx/presentation/codex_activity.py
public boundary scan clean: 2 files

$ loopx canary premerge --from-git-diff --git-diff-base upstream/main
direct checks 6/6 passed (diff checks, RFC bilingual, py_compile, module ceiling),
catalog canaries 2/2 passed, 0 manual holds

Local Windows caveat, reproducible against the baseline module file and therefore not caused by it: tests/test_native_steward_monitor.py stops at CodexChatAgentError ... executable_not_found and tests/test_chat_agent.py::test_result_review_enforces_workspace_read_profile_on_the_original_thread_and_home fails at loopx/chat_agent.py:802 with KeyError: 'set'; tests/test_chat_agent.py is not in the required windows-powershell lane, so hosted CI remains the qualification for those paths.

Author Declaration

  • Written by: an AI coding agent (Qoder Desktop) operated by JasonBuildAI, working under the operator's direction and review.

Implemented against

Criterion (spec clause) Disposition Symbol / path Test or command
Issuer-prefixed tokens are masked before replay (module contract: "a missed secret would be stored for replay") implemented _SECRET_PATTERNS canonical arm tests/test_chat_activity_provider_tokens.py 9/9 before → after, including a real store reopen
Existing generic sk- masking is preserved implemented legacy arm retained tests/test_chat_activity.py::test_credentials_in_commands_are_masked_before_replay
One canonical definition for provider shapes (#5136) implemented import of PROVIDER_TOKEN_SHAPE_PATTERN_SOURCE tests/control_plane/test_public_safe_text_owner_parity.py 239 passed with the sibling suites
  • Self-check before submission: ran the failing-first matrix, the whole activity file, the presentation/parity/ratchet suites, ruff, git diff --check, the real loopx check boundary scan and canary premerge. Read the persistence and replay path end to end (chat_runtime.py _TurnEventBuffer.emit → append_event, chat_server.py SSE replay, steward_turn.py phase 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

  • Tested revision: 3bcafd97d
  • Run state: finished
  • Input classes: synthetic
Check kind Result Public-safe evidence / limitation
static passed ruff check on both files; git diff --check; py_compile via premerge direct checks
unit passed new module 9 failed → 9 passed; activity file + new module 41 passed; focused presentation/parity/ratchet 239 passed
regression_parity passed the new module fails 9/9 against the baseline module file and passes 9/9 at head
real_entrypoint passed loopx check --scan-path over both changed files: public boundary scan clean: 2 files
integration passed loopx canary premerge --from-git-diff: direct checks 6/6, catalog canaries 2/2, 0 failures
real_backend not_applicable no backend or authority-store change; masking runs before persistence
manual not_run UI rendering unchanged; no screenshot judgment applies
  • Coverage and gaps: the changed lines are exercised by the new matrix and the existing activity tests; the persistence/replay consumers are covered by reading the owning code paths, not by a browser run, because the diff changes only which characters are replaced in the payload. Known residual: PEM private-key blocks stay unmasked (out of the canonical provider-shape scope). The locally failing consumer suites above are environmental/pre-existing on this host.

Frontend / Visual Evidence

  • UI impact: none
  • Before: N/A
  • After: N/A
  • First viewport and preview: N/A, no rendered layout or copy change; only the redaction of already-displayed text
  • States and viewports shown: N/A
  • Source data: none
  • Attention review: N/A
  • Craft review: N/A

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Refactoring (no functional changes)
  • Documentation update
  • Test update

LoopX Area

  • Control plane (goals, todos, quota, scheduler, registry, runtime)
  • Benchmark boundary (adapters, runners, verifiers, scoring, evidence)
  • Capability or extension (providers, adapters, skills)
  • Public docs or presentation surface (README, protocols, dashboard)
  • Build, packaging, installer, or CI
  • Host or runtime integration

Technical Direction

Shared-authority RFC fixture impact

  • N/A, no TypeScript control-plane migration or shared Goal Authority change.

Boundary Checklist

  • Neither the diff nor this PR body/comments/attachments disclose private state, credentials, raw traces or verifier output, internal links, or local machine paths (including .loopx/, .codex/goals/, and live ACTIVE_GOAL_STATE.md).
  • I did not duplicate maintainer-owned benchmark work unless a maintainer split out a public issue for it.
  • I kept the change scoped to the linked issue/task.
  • Existing English/Chinese RFC pairs (including ledger entries) both have a diff, and the local premerge check passes, or no paired RFC changed. (No paired RFC changed.)
  • I completed the visual evidence section for UI changes, or marked UI impact none.
  • Every commit includes a DCO Signed-off-by trailer (git commit -s).

AI Disclosure

AI disclosure: this change was drafted with AI assistance at my request, under my direction and review.

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>
JasonBuildAI added a commit to JasonBuildAI/loopx that referenced this pull request Oct 30, 2026
…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>

@songoow songoow left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 的完成条件。

关键代码讲解

  1. _SECRET_PATTERNS(loopx/presentation/codex_activity.py:55):增加 canonical provider arm,保留 generic sk-、shell 值和 header 旧分支。

  2. _mask_secrets(loopx/presentation/codex_activity.py:84):新增模式用空 capture group 保留 replacer 的 group(1) 接口;模式正文均为 noncapturing,不会错拼前缀。

  3. 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.

@JasonBuildAI

Copy link
Copy Markdown
Contributor Author

配对归因:research-display-language 超时是既有环境性失败,未发现需要修改的代码

结论:代码未改动。评审提出的唯一阻断项是 build(Release Artifacts)里 research-display-language 的 30s 等待超时;补齐 base/head 配对后,证据一致指向既有失败,与本 PR 的生产改动无关。

1. CI:同一 job、同一 exact head 重跑通过

  • 原失败:Release Artifacts run 40996806091 / job 123872101102,2026-10-30T03:46:21Z 在 page.waitForFunction 处 Timeout 30000ms exceeded。
  • 重跑:同一 job 在 exact head 3bcafd97d 上 POST /actions/jobs/123872101102/rerun → job 124387231215(attempt 2),2026-10-31T00:35:36Z 起跑,01:04:20Z 成功。
  • 重跑日志中同一场景在 packaged 模式下通过:
    2026-10-31T00:59:08Z scenario-start=research-display-language
    2026-10-31T00:59:26Z scenario-end=research-display-language duration_ms=18505
    2026-10-31T01:03:10Z personal-workspace-browser-smoke (packaged): ok
    

同一 head、同一 job、同一 merge ref:先失败后通过,属非确定性失败。

2. CI:同一 run 内,同一 head 上该场景在另一 job 已通过

同一次 run 的 dashboard-browser (2)(job 123881520818,04:40Z)在 source checkout 模式下执行同一场景并通过:

2026-10-30T04:40:33Z scenario-start=research-display-language
2026-10-30T04:40:49Z scenario-end=research-display-language duration_ms=16602

该 job 结论为 success,scenarios=...research-display-language... 也在其 catalog 列表中。

3. CI:既有可核验配对产物(docs-only 分支上同一等待点先失败、后通过)

  • 失败:run 41019488663(codex/launch-panorama-20261029,head adcd0a58a,2026-10-30T07:47:23Z)在同一 scenario、同一 page.waitForFunction 等待点超时 30000ms。
  • 该分支对应 #7598(docs-only,已合并):改动仅 6 个文件——两篇博客的中英文 HTML 与两个 README,未触及 examples/personal-workspace-browser/** 或 goal-research-progress.tsx。
  • 同一分支随后一次 run 41028308606(head 1a35c22d,08:47Z)同一 build job 通过(09:16:41Z);compare/adcd0a58a...1a35c22d 的 35 个文件里仍不含该场景文件与研究展示组件。

即:在没有本补丁的 docs-only 分支上,同一检查先失败、后通过,场景与组件代码完全相同。

4. 本地配对执行(同输入、同视口、同等待点)

  • 修订:immutable base 2f544f969 与 exact head 3bcafd97d;场景与被点击组件两边字节一致:
    • examples/personal-workspace-browser/research-display-language.mjs → blob 67d3b0092…
    • apps/presentation/dashboard/src/features/personal-workspace/goal-research-progress.tsx → blob ddcfccdaf…
    • 场景内部视口固定 1440×1000 / 390×1000,等待点未改动。
  • 命令(两个 worktree 相同,仅 repo 根不同):
    LOOPX_PERSONAL_WORKSPACE_SCENARIO=research-display-language \
    LOOPX_PLAYWRIGHT_PACKAGE=<dashboard>/node_modules/playwright \
    node examples/personal-workspace-browser-smoke.mjs
    
  • 环境:Windows 11 (10.0.26200),Node 26.11.1,Playwright 1.62.1 chromium headless shell;source checkout,chat bundle 由两版本相同的 apps/presentation/dashboard 源码各自构建。无凭据、真实模型或操作者状态。
修订 运行 结果 场景耗时
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.

@JasonBuildAI
JasonBuildAI requested a review from songoow October 31, 2026 01:10

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Chat activity masker persists npm/PyPI/Google/AWS/Slack/JWT tokens for replay

2 participants