Skip to content

fix(capability): decode BOM-marked Unicode candidates in the public-safe scan - #7473

Merged
loopx-agent merged 3 commits into
loopx-project:mainfrom
JasonBuildAI:codex/agent-m8k4
Oct 27, 2026
Merged

loopx-agent merged 3 commits into
loopx-project:mainfrom
JasonBuildAI:codex/agent-m8k4

Conversation

@JasonBuildAI

@JasonBuildAI JasonBuildAI commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Goal And Delivered Outcome

Author Declaration

  • Written by: model_agent (DeepSeek, DeepSeek Harness), at the account owner's direction and under their review; the owner set the contribution constraints and reviewed the change.

Implemented against

Criterion (spec clause) Disposition Symbol / path Test or command
BOM-marked UTF-8/UTF-16/UTF-32 candidates are decoded before the rules run implemented scanner._BOM_CODECS, scanner._candidate_text test_bom_marked_unicode_candidates_are_decoded_before_the_rules
Bytes that decode to NUL-bearing text fail closed as unscanned implemented scanner.scan_paths, content_not_text test_undecodable_candidate_bytes_fail_closed_instead_of_reading_clean
The receipt keeps the text read's universal-newline line numbers implemented scanner._candidate_text test_cr_only_unicode_candidate_keeps_the_text_read_line_numbers
Existing UTF-8 path behavior and receipts are unchanged implemented scanner.scan_paths existing scanner suite: 45 passed / 4 skipped
The boundary documentation states the BOM/undecodable rule implemented loopx/capabilities/public_safe_outbound/README.md static review of the changed bullet
  • Self-check before submission: ran the focused capability suite, ruff, git diff --check, the end-to-end scan_cli --scan-root before/after repro, a related contract suite, the whole-worktree scan, the semantic-vocabulary advisory and loopx canary premerge; an independent read-only review reproduced the focused results and probed endianness, clean controls, line numbers and the binary fail-closed edge. Did not run the hosted required checks locally and disclose the one pre-existing Windows-only test failure and the environment-blocked catalog canary below.

Scope And Continuation

  • Completed scope and remaining work: BOM-marked Unicode decoding, fail-closed handling of undecodable candidate bytes, and preserved newline line numbering. BOM-less legacy encodings (for example UTF-16 without a BOM) deliberately fail closed rather than being guessed; converting such a capture to UTF-8 lets the scan report its hits.
  • Behavior change: a candidate whose bytes decode to NUL-bearing text now fails the whole scan (content_not_text) where it previously passed as clean; this is the scanner's documented fail-closed direction and covers binary files outside BINARY_SUFFIXES. The README states the rule.
  • Slice boundary / successor: complete within this scope; no successor needed.

Validation

  • Tested revision: 1708e538b (two commits, rebased onto main at 17b2a0f9b).
  • Run state: finished (not a claim of adequacy or success).
  • Input classes: synthetic.
Check kind Result Public-safe evidence / limitation
regression_parity passed Same synthetic UTF-16LE log with a private path and Bearer token: before the change ok=true, hit_count=0, unscanned_count=0, exit 0; at head ok=false, hit_count=2 (sk_bearer, private_abs_path), exit 1. BOM-less UTF-16 at head: unscanned_reasons=["content_not_text"], exit 1.
unit passed python -m pytest loopx/capabilities/public_safe_outbound/tests -q: 45 passed, 4 skipped, 1 failed. The failure is the pre-existing test_unreadable_requested_file_fails_closed, which also fails on the unmodified base because Windows chmod(0o000) does not remove the owner's read access (raw base run: same assert True is False); it is unrelated to this diff.
integration passed python -m pytest tests/test_contract_scan_unreadable_directories.py loopx/capabilities/public_safe_outbound/tests -q: 51 passed, 8 skipped, 1 failed (same pre-existing Windows chmod case).
real_entrypoint passed python -m loopx.capabilities.public_safe_outbound.scan_cli --scan-root . over a synthetic one-file tree, both before and after; outputs above.
static passed ruff check on the changed module and test file: All checks passed. git diff --check: clean. scripts/generate_semantic_inventory.py --changed-from upstream/main: no supported new vocabulary carriers (the new reason code is a dynamic string, documented in the README).
manual passed Whole-worktree scan_cli --scan-root at head: unscanned_count: 0, so no existing repository file newly fails closed from the NUL rule (the rule hits are the pre-existing synthetic credential/path shapes in tests and examples, which are not a candidate set this repository publishes wholesale).
canary failed (environment) loopx canary premerge --from-git-diff --git-diff-base upstream/main: all 8 risk-profile smokes and the public-boundary canary passed (the canary scanned the three changed files clean). The catalog canary examples/semantic-vocabulary-drift-smoke.py fails before reading this diff because the isolated worktree has no node_modules/typescript (npm ci --ignore-scripts was not run there); no TypeScript or shared vocabulary is changed.
  • Coverage and gaps: the changed code is the single scan_paths read/decode branch, covered by the before/after CLI repro and the three new tests, including the BOM-less fail-closed negative and the CR-only line-number parity case. Hosted required checks were not available before opening; the pre-existing Windows-only chmod test failure and the environment-blocked catalog canary are disclosed above rather than attributed to this change. Non-BOM UTF-16 is intentionally not auto-detected; the scanner blocks instead of guessing an encoding.

Frontend / Visual Evidence

  • UI impact: none
  • Before: N/A
  • After: N/A
  • First viewport and preview: N/A (no UI)
  • 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

  • Direction / acceptance reference, when applicable: N/A — capability boundary hardening; no roadmap maturity claim.

Shared-authority RFC fixture impact

  • Production-scale fixture schema: N/A
  • Semantic dimensions changed, or reviewed no-impact rationale: N/A
  • Provider conformance arms run: N/A
  • Read-only legacy/file/PostgreSQL three-arm rehearsal (required for promotion, runtime-routing, or compatibility-projection changes): N/A

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: this change was drafted with AI assistance at the account owner's request, under their direction and review. The account owner set the constraints (minimal diff, focused issue, upstream base) and reviewed the final diff; the reproduction, test, lint and scan outputs above were run in this environment.

Continuation: follow-up to merged #7247 (same public_safe_outbound private-path/credential detection class), with #6223 and #6428 as the earlier fails-closed slices.

@cocolord cocolord 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-5; OpenAI; self_reported

动机

public-safe-outbound 是发布前的保守扫描边界。原实现把 Windows PowerShell 5.1 产生的 UTF-16 日志按 UTF-8 replacement 模式读取,私有路径和 Bearer token 会变成无法命中的乱码,却得到 ok=true。本 PR 正确补上 BOM-marked UTF-16/UTF-32 解码,并让 NUL-bearing 输入 fail closed;这个方向明显正向,完整 UTF-16 复现已经从 false clean 变为两个命中。

但 linked issue 的验收不只要求识别 BOM,还要求“任何无法有把握解码为文本的候选都必须 unscanned”。当前 helper 仍对 UTF-8、UTF-8 BOM 和 UTF-16/32 使用 errors="replace",replacement character 不含 NUL 时会继续扫描并可能返回 clean。运行 public-safe-outbound 文件扫描的贡献者仍可能收到错误的 clean 回执。BOM-marked UTF-16 从 false clean 变为命中,但 malformed UTF-8 和 truncated BOM UTF-16 在 exact head 仍然 exit 0。完整 UTF-16 正路径已修复,无法可靠解码的输入仍未 fail closed。本 PR 不需要猜测 BOM-less legacy 编码,也不需要扫描已排除的二进制后缀。

改动思路

新 _BOM_CODECS 按 UTF-32、UTF-8、UTF-16 顺序识别 BOM,避免 UTF-32LE 被 UTF-16LE 前缀先截获;_candidate_text 再统一恢复 text-mode universal newline 语义,scan_paths 继续由同一个规则集合产生 masked receipt。这复用了现有 scanner owner,没有新增 capability、协议或第二份安全规则。NUL-bearing BOM-less UTF-16 被记录为 content_not_text,也比猜测 legacy encoding 更安全。

最小修复仍在这个 helper 内:使用 strict decode,捕获 UnicodeDecodeError 并返回 unscanned;无需扩大为字符集探测框架。保留现有 UTF-8 正常文本、大小限制、不可读文件、二进制后缀和 CR/CRLF 行号行为即可。当前 errors="replace" 正好绕过了 issue 规定的 confidence boundary,所以这不是只缺“更多测试证据”,而是已复现的逻辑缺口。

具体改动

全 PR 修改 capability README、scanner.py 和 test_scanner.py,共 +99/-6。文档新增 BOM/NUL fail-closed 说明;实现从 read_text 改为 read_bytes 后按 BOM 解码;测试覆盖 UTF-8 BOM、双端 UTF-16、UTF-32、BOM-less UTF-16 和 CR-only 行号。

关键代码讲解

  • scanner._BOM_CODECS:编码顺序正确,UTF-32 在 UTF-16 前,四种 BOM 和 UTF-8 BOM 都落到标准 codec。
  • scanner._candidate_text:承担 BOM decode、NUL 拒绝和 universal newline normalization。阻断点也在这里:第 280/283 行的 errors="replace" 把解码失败变为普通字符串,绕过 content_not_text。
  • scanner.scan_paths:保持 size/read error receipt,None 时写入 content_not_text;现有规则和 masked location owner 未复制。
  • 三个新测试组:正向编码矩阵与 CR 行号有效,但负向只覆盖“含 NUL”,没有覆盖“无 NUL 的 invalid byte sequence”。

规范依据是 issue #7472,spec_revision 2026-10-27T09:17:35Z:

  • I7472-BOM-DECODE — implemented:有效 BOM-marked UTF-8/16/32 在规则执行前按声明 codec 解码。
  • I7472-FAIL-CLOSED — not_met:malformed UTF-8、UTF-8 BOM 和 truncated UTF-16 可产生 replacement character 而无 NUL,exact head 仍返回 ok=true, unscanned_count=0。
  • I7472-PRESERVE-RECEIPT — implemented:正常 UTF-8、masked receipt、unreadable/oversize 和 universal-newline 行号由现有 suite 与新增 CR case 覆盖。

对主干的风险

已执行 exact-head capability tests:50 passed;两个修改模块 Ruff 与 git diff --check 通过。真实 scan_cli --scan-root 对有效 UTF-16 fixture 返回 exit 1、两个 masked hits,说明主要 bug 的正路径确实修复。最强反例同样走真实 CLI:仅含无效 UTF-8 byte 且无 NUL 的 .log 返回 exit 0、ok=true、unscanned_count=0;truncated BOM UTF-16 和 malformed UTF-8 BOM 在 scan_paths 上也得到 clean。这个结果直接违反 fail-closed 义务,不能被 50 个绿色测试抵消。

语义与 CI 对齐

content_not_text 是现有 receipt 范围内的新原因码,名称保持 domain-neutral,也没有把 prose denylist 当状态机。行为变化已在 README 披露:NUL-bearing 候选从 clean 变为 fail closed。问题在于 disclosure 和实现都把“无法可靠解码”缩窄成“解码后含 NUL”;这会让机器执行的安全义务继续出现 false negative。需要 strict decode 的正负回归:有效 UTF-8/16/32 仍扫描;每个 BOM family 的 malformed bytes 和无 BOM invalid UTF-8 都必须 content_not_text、exit 1;普通二进制后缀仍按既有排除策略处理。

当前远端 CI 还有 browser、stage2c 和 Release Artifacts 红项以及 pending shards,没有完成同 base 归因;本结论不借那些红项加码。REQUEST_CHANGES 完全由 exact-head scanner 真实入口反例支撑。future-facing pass 已考虑的边界是单一 decode helper:strict decode 是同一 change reason 的小修,不需要新 abstraction;更广的 charset detection 应继续不做。

我的整体评价

结论是 fragmented。long_horizon=not_yet_proven:有效 Windows PowerShell 日志已经受到保护,但 scanner 仍会对另一类不可解码候选签发 false-clean receipt,发布边界的持续可靠性没有闭合。user_experience=not_yet_proven:用户看到的 ok=true 仍可能错误,且 receipt 不给出任何 unscanned 恢复动作。

这是一个小而方向正确的补丁,不需要推翻设计;阻断项也很集中。请把 decode 从 replacement 改为 strict,并加入上述 malformed-byte real-entrypoint 回归后再复审。当前 exact head 不能 APPROVE。未发现另一个需要扩大范围的 blocker,也不要求处理 BOM-less legacy encoding 猜测。

English verdict: REQUEST_CHANGES - 1708e53 fixes valid BOM-marked Unicode, but replacement decoding still certifies malformed UTF-8 and truncated BOM Unicode as clean; strict decode plus fail-closed regression coverage is required.

@JasonBuildAI

Copy link
Copy Markdown
Contributor Author

@cocolord 感谢 review,这条成立。_candidate_text 里 UTF-8 / UTF-8 BOM / UTF-16 / UTF-32 都用了 errors="replace",所以 malformed UTF-8 和 truncated BOM UTF-16 会被替换成 U+FFFD 后继续扫描——replacement 不含 NUL,就绕过了 content_not_text,仍然 exit 0、发出 clean receipt。BOM-marked UTF-16 从 false clean 变成命中,但这只是把“猜编码”这一类问题修了,没有落 issue 要求的 confidence boundary。

修改留在同一个 helper 内,不引入字符集探测:

  • 解码改 strict,UnicodeDecodeError 一律判为 unscanned(不扫描、不发 clean);
  • BOM-less NUL-bearing 输入保持现有的 content_not_text 处理,不去猜 legacy 编码;
  • 补 malformed UTF-8 与 truncated BOM UTF-16 走真实入口(public-safe-outbound 文件扫描)的 fail-closed 回归,断言 exit code 与 receipt 都不再是 clean;
  • 保留现有 UTF-8 正常文本、大小上限、不可读文件、二进制后缀和 CR/CRLF 行号行为,作为未受影响的控制用例。

分支落后 main 27 个提交(GitHub 已标 BEHIND),我会一并 rebase 后请求复审。

@JasonBuildAI

Copy link
Copy Markdown
Contributor Author

@cocolord 已按上面的方向改完并 rebase 到最新 main,head e4cec69b5(基于 a704725d6)。

  • _candidate_text 改成 strict 解码:UnicodeDecodeError 直接返回 None,走 content_not_text 的 fail-closed 路径;BOM-less NUL-bearing 输入行为不变,CR/CRLF 行号归一化保留。
  • 新增 test_undecodable_candidate_bytes_never_read_clean:malformed UTF-8 与 truncated BOM UTF-16 各一例,都走 scan_paths 真实入口,断言 ok=False、hits=[]、unscanned=["content_not_text"]。我先在未修改的 scanner 上跑过这个用例,它确实按你描述的方式返回 clean 而失败,改完才通过。
  • loopx/capabilities/public_safe_outbound 全量:46 passed / 4 skipped;唯一失败是 test_unreadable_requested_file_fails_closed,原因是 Windows 上 chmod 0o000 不阻止读取,同一用例在本次改动的 base 上也失败,与本 PR 无关。

临时分支落后 main 的情况也已消掉(原来落后 27 个提交)。

…afe scan

Windows PowerShell 5.1 writes > and Out-File output as UTF-16LE with a BOM. scan_paths read every candidate as UTF-8, so such a captured log became NUL-interleaved mojibake that no rule could match: a file carrying a private home path and a Bearer token returned ok=true with an empty unscanned list.

Decode the codec the BOM declares (UTF-8/UTF-16/UTF-32) before matching, and report NUL-bearing bytes that no BOM explains as content_not_text so the scan fails closed instead of silently passing.

Fixes loopx-project#7472.

Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
Reading bytes instead of text dropped the universal-newline translation the receipt's line numbers depend on, so a CR-only source reported line 1 where the text read reported the actual line. Normalize CRLF and CR after decoding and pin the CR-only case.

Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>
`_candidate_text` decoded every candidate with `errors="replace"`, so a
malformed UTF-8 byte or a truncated BOM-marked UTF-16 candidate turned into
U+FFFD text that no rule matches. The scan then reviewed a mangled copy of
the document and could still return `ok=true` for content it never saw.

Decode strictly instead and report the candidate unscanned on
`UnicodeDecodeError`, the same fail-closed path NUL-bearing bytes already
take. Cover both shapes through `scan_paths`, and keep the BOM-less UTF-16
and CR-only line-number cases as controls.

Signed-off-by: JasonBuildAI <jasonbuildai@gmail.com>

@loopx-agent loopx-agent 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.1-sol · OpenAI · runtime_reported · reasoning_effort=max

未发现当前 head 的阻断问题;此前“替换解码仍会误报安全”的 finding 已按当前实现重新验证。

动机 / Motivation

运行内置 public-safe-outbound 发布前扫描、会扫描 PowerShell 日志的贡献者和维护者。同一日志用 UTF-8 时能发现路径/凭据,用带 BOM 的 UTF-16/32 时旧版可能报安全;新版先完整解码,不能完整读取则拒绝出具安全结论。带 BOM 的 Unicode 候选正常命中;损坏编码和残留 NUL 返回未扫描并 exit1,普通 UTF-8 与 diff 行为保持。不改变另一个 contributor contract scanner,不猜测任意字符集,不修改规则、排除集、权限或发布流程。

该精确版本以 19e1525a8babdd6bae5d505d72747f64ec21bc8d 为分叉点,已重新审阅完整三路径提案并重跑下列验证。依据 #7472 的完整期望,spec_revision 2026-10-27T09:17:35Z,以及修改前能力 README(19e1525a8babdd6bae5d505d72747f64ec21bc8d)。扫描得到“安全”必须表示输入确实读完,而不是用替换字符改造出一份看似干净的文本。当前界限足以关闭该 issue 的编码缺口,不扩展成通用字符集探测。

改动思路 / Approach

解码是现有 scanner 的文件读入边界;复用原规则、行号和脱敏回执,无须新增 provider、配置或并行扫描器。本 PR 只修候选文件的完整 Unicode 读取与 fail-closed,不扩展扫描范围或外部发布权限。UTF-32 BOM 必须先于 UTF-16 比较,因为 LE 前缀重叠;严格解码失败和解码后仍含 NUL 都进入原有未扫描通道。正常解码后沿用旧规则及通用换行处理,不增加账户设置或问询轮次。独立的 loopx check --scan-path contract scanner 本来就要求严格 UTF-8,保留其独立输入政策合理。

具体改动 / Changes

完整三个路径 diff 已核对:生产修改只在同一 scanner 的文件输入层;README 解释 BOM/NUL 和失败结果;四个新增测试覆盖正常 Unicode、无 BOM、损坏编码及 CR 行号。

关键代码讲解

  • _candidate_text:269 处理 UTF8 BOM、UTF16/32 两字节序;UnicodeDecodeError 返回 None,没有 errors=replace 回退。
  • scan_paths:300 在原大小/读取限制后解码,None 记录 content_not_text,再复用 _scan_one/_finish;没有修改候选排除、命中规则或脱敏序列化。

验收映射标签 I7472-BOM-DECODE:真实 CLI 用六个 codec 变体、LF/CRLF/CR 和 clean/hit 配对,UTF16/32 均得到正确命中/行号,UTF8 BOM 与 BE 顺序也检查。I7472-FAIL-CLOSED:实际 malformed UTF8、两序截断 UTF16/32、无 BOM NUL 和未配对 surrogate 均 exit1、未扫描;这覆盖上次 finding,不只断言 Python helper。I7472-PRESERVE-RECEIPT:完整 stdout/stderr/退出码比较,10 组普通 UTF8/diff 不变;所有输出无输入内容和私有绝对路径,sample 始终 ***,输入字节未被扫描改写。

PASS:完整能力测试 51 passed;额外 50 组 paired CLI 的 head 全部满足独立预期,22 组明确显示 base ok=true、head ok=false。另 5 组 同根排除二进制、后建日志、损坏拒绝、恢复和权限拒绝通过。旧 base 为 19e1525a8babdd6bae5d505d72747f64ec21bc8d,复用同一 Python 环境与同一输入;不是以作者测试替代复验。

Ruff/diff check、语义 advisory 和以核实的 target main ca0396a3d80af5a313b776a99bdd375045b3addd 进行标准 premerge,6 direct + 15 selected 通过;首评前物理 CQR cqr_2817ccb6d01726aabf41 已被原生预检验证为当前 scope 有效。公开边界检查中的 scanner.py:30: credential 是规则定义的既有非阻断提示,实际源码已读;没有把提示说成真实凭据或零提示。未查询、轮询或等待 CI。

对主干的风险 / Risks

语义与 CI 对齐

新增默认拒绝针对之前无法完整读取却误报安全的候选,这是 issue 已要求的 fail-closed 语义。无 BOM 的非 UTF8 文本不会被猜测接受,操作者需重新导出完整支持编码;这避免再扩大静默回退。正常 UTF8、文本 diff、原大小限制和内容不可读规则均保持,BOM 剥离和换行规范化后的行号由真实 CLI 对照验证。

本地环境为 macOS/CPython3.14.8;字节级 UTF16/32 两序测试不等于原生 Windows PowerShell 运行。作者旧 Windows chmod 失败和旧 1708 验证仍归原条件;本轮51项全过并不宣称修复该平台既有 chmod 行为。正式安装采用、任意字符集和完整长期性能/模型收益均未验证,也不属于这项小型 IO 修复的通过结论。

我的整体评价 / Verdict

解码是现有 scanner 的文件读入边界;复用原规则、行号和脱敏回执,无须新增 provider、配置或并行扫描器。本 PR 只修候选文件的完整 Unicode 读取与 fail-closed,不扩展扫描范围或外部发布权限。未改渲染组件;已检查 scan_cli 的 JSON/text 输出及现有 capability 入口,本切片不需要页面截图。原生 Windows PowerShell 和正式安装采用未验证。扫描失败后修复原文本即可用同一命令继续,没有新状态、重试循环或配额副作用;复用原回执比新增 charset/provider 框架更合适。仓库经验条目是其它恢复案例,本轮没有当作此 scanner 的验收或权限。当前结论为 APPROVE,旧 CHANGES_REQUESTED 的平台状态、有效审批和合并 readiness 仍独立核对,不能凭本次通过自动清除异议或合并。

English verdict: APPROVE cbe2bcb064d458ab868d9739a955ff24dd47000b — strict BOM decoding closes the false-clean path; 51 tests, 55 paired CLI/scope cases and native premerge passed. Native Windows and installed adoption remain unverified.

@loopx-agent
loopx-agent merged commit 6dba65d into loopx-project:main Oct 27, 2026
26 of 40 checks passed
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]: public-safe-outbound scan reports a UTF-16 PowerShell log as clean, so private paths and credentials pass the pre-publication gate

3 participants