fix(core): logger 脱敏表按词边界匹配,不再吃掉 keys/tokens 这类普通字段 (#5573) - #5786
Merged
Conversation
`ObjectLogger.redactSensitive` 的判定是 `key.toLowerCase().includes(pattern)`,
所以任何名字含有 `password`/`token`/`secret`/`key` 子串的字段,整个值都被换成
`***REDACTED***`:`keys`、`keyword`、`keyboard`、`monkey`、`tokens`、`tokenizer`、
`secretary`。读者不但丢了事实,还被告知这里挡住了一个秘密,比字段缺失更误导。
仓库里已有活的命中:dispatcher-plugin.ts 为躲开脱敏器把 `key` 改名成 `keyedBy`,
而 `'keyedby'.includes('key')` 依然为真,那条限流日志一直是 `***REDACTED***`。
按维护者裁决(#5573 评论 5199145168)的 A 案,匹配改为 camelCase / snake_case
分词后的词边界比对。默认脱敏表本身不变,spec 的 schema 默认值也不变 -- 变的只是
这张表怎么用。两个边角是显式取舍:
- 全小写连写(`apikey`)没有边界可分,而 `monkey`/`turkey` 也以 `key` 结尾,
任何区分二者的规则都要编码词法知识 -- 做成一张显式限定词表,`限定词+脱敏词`
的后缀连写算命中,表外连写不算,`secretary`/`keyword` 因此保持干净。
- 复数形变只在与其他词组合时命中:裸 `keys`/`tokens` 是集合与计数,不脱敏;
复合词里的 `apiKeys`/`refresh_tokens` 仍是秘密,照常脱敏。
新增 88 个用例:真秘密拼法矩阵 42 条(含 camel/snake/SCREAMING/kebab/连写/复合
复数)全部仍脱敏,症状面 14 条不再脱敏,加上 #5573 的嵌套原始复现。反向验证换回
`includes` 后 17 红全部落在症状面,真秘密矩阵保持绿。`tokenCount`/`promptTokens`
仍脱敏 -- 词边界分不出词义,改动前后一致,已钉住现状而非留白。
PR #5572 落的「原样转发会被脱敏」pin 按裁决改判,断言翻到正面:`visibleIf` 出现
在输出里、整个 issues 结构逐字相等,而不是因为什么都没产出所以断言过了。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V7WetGmnfoXNn8cLieKKmx
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 26 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
This was referenced Aug 6, 2026
…子串脱敏」说法 (#5573) 模块 docblock 用两条理由解释为什么把 Zod issue 压平而不是原样转发。第一条依据的是 `ObjectLogger` 按**子串**脱敏 —— `'keys'` 含 `'key'`,所以 `keys` 字段会渲染成 `***REDACTED***`。本 PR 把匹配改成词边界之后这条不再成立:裸 `keys` 不再被脱敏。 按 PM 裁决(#5573 评论 5200739786)就地订正:该 bullet 标注为因 #5573 **失效**的 历史理由,并写明今天判定新增字段的规则(`apiKey`/`api_key` 脱敏,`keys`/`tokens` 不脱敏);第二条(一条 issue 可能带上整个被拒 `input`,日志记录必须有界)独自承接 这个决定,仍然成立,措辞改为不再依赖上一条。 `LoggedCauseIssue.unrecognized` 的字段注释复述了同一句已过期的 "substring redactor",一并订正。字段名保持 `unrecognized` 不变 —— 改回是纯 churn。 纯注释,无行为变化。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V7WetGmnfoXNn8cLieKKmx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #5573
按维护者裁决(评论 5199145168)的 A 案实施:
packages/core/src/logger.ts的redactSensitive由子串includes改为 camelCase / snake_case 分词后的词边界匹配。默认脱敏表['password','token','secret','key']本身没有动,packages/spec/src/system/logging.zod.ts的 schema 默认值也没有动 —— 变的只是这张表怎么用。现象与实现
旧判定是
key.toLowerCase().includes(pattern),于是任何名字含有脱敏词子串的字段整块变成***REDACTED***:keys、keyword、keywords、keyboard、monkey、tokens、tokenizer、secretary。读者不但丢了事实,还被告知"这里挡住了一个秘密",比字段缺失更误导。新实现三个模块级函数 + 一个私有方法:
tokenizeFieldName()—— 按 camelCase / snake_case / kebab-case / 字母-数字边界分词并小写化。apiKey、api_key、API_KEY、x-api-key都分成['api','key'];monkey、keyword、tokenizer保持单词一个。singularizeWord()—— 只覆盖脱敏词汇会遇到的复数拼法(keys/tokens/secrets/passwords/passes),并且不会把address/status变成新词。fieldWordsMatchPattern()—— 逐词比对;多词 pattern(host 配redact: ['apiKey'])按连续词串匹配。ObjectLogger.isRedactedFieldName()—— 递归里唯一的判定入口,pattern 在构造函数里分词一次。顺带证实了一个活的 in-tree 命中(原 issue 认为"目前没有任何 in-tree 调用命中"):
packages/runtime/src/dispatcher-plugin.ts:562当年为躲开脱敏器特意把字段key改名成keyedBy,但'keyedby'.includes('key')依然为真 —— 那条限流日志的keyedBy一直是***REDACTED***,这个 PR 之后才真正显示出限流键是什么。该文件本 PR 不需要改,行为自动恢复。两个边角的取舍(裁决没点名,写在这里请复核)
1. 全小写连写
apikey。 先实测:纯词边界下apikey分词只有一个词,会漏。不能用"以key结尾"救,因为monkey/turkey/whiskey也以它结尾 —— 那正是本单要去掉的误报。任何区分二者的规则都必须在某处编码词法知识,所以做成一张显式限定词表CONCATENATED_SECRET_QUALIFIERS(api/access/refresh/client/private/session/ssh/… 共 31 个):限定词 + 脱敏词连写算命中,别的连写不算。secretary、keyword、keyboard保持干净。foobarkey)不脱敏。按仓库命名惯例(Prime Directive Implement ObjectStack protocol specification with Zod schemas and TypeScript interfaces #3:配置键 camelCase、机器名 snake_case)写成fooBarKey/foo_bar_key就通用命中 —— 全小写连写本来就是惯例外拼法,这张表只是安全网。2. 复数。 裁决点名
tokens不脱敏,但apiKeys分词为api+keys,keys ≠ key,纯词边界会让apiKeys: ['sk-…']漏网 —— 与"不漏真秘密"冲突。按派发指示实现为:复数形变只在与其他词组合时命中。裸keys/tokens不脱敏(集合与计数),复合词里的apiKeys/refresh_tokens/clientSecrets/userPasswords照常脱敏。两个诚实的残留,都已钉成测试而不是留给下一个读者去踩:
passwords/secrets也不脱敏(裸复数规则一视同仁)。这是维护者词边界决定的直接推论,不是我引入的;要挡的 host 写redact: [..., 'passwords']显式加回,已有用例。tokenCount、promptTokens、completionTokens仍然脱敏 —— 词边界分不出词义,token作为一个词就是命中,复合复数同理。这三个在本 PR 之前也是脱敏的,没有回归,但也没被修好。再收窄意味着要给prompt和api这两个限定词排座次,那是维护者的面,不是我能自选的,所以只钉住现状并在此点名。PR #5572 的 pin 改判
packages/services/service-automation/src/thrown-cause-diagnostics.test.ts里那条"原样转发 Zod issues 会被脱敏"的 pin,按裁决明文授权改判(只动这一条it的断言与其说明):toContain('REDACTED')→not.toContain('REDACTED'),并且断言翻到正面 ——visibleIf出现在输出里、整个issues结构逐字相等,而不是"因为什么都没产出所以断言过了"。✅ 过期注释已按 PM 裁决订正(#5573 评论 5200739786,第二个提交
ccbdbff)。thrown-cause-diagnostics.ts模块 docblock 用两条理由解释"为什么压平而不是原样转发",第一条依据的正是"按子串脱敏",本 PR 之后失效。现在:keys的普通字段会被整块换成***REDACTED***#5573 失效的历史理由 —— 裸keys不再被脱敏 —— 并写明今天判定新增字段的规则:apiKey/api_key脱敏,keys/tokens不脱敏。input,日志记录必须有界)独自承接这个决定,仍然成立,措辞改为不再依赖上一条。所以 helper 继续压平 issue,字段名unrecognized保持不变。LoggedCauseIssue.unrecognized的字段注释复述了同一句已过期的 "substring redactor",一并订正(字段名改回是纯 churn,不改)。纯注释、无行为变化:
eslint干净,thrown-cause-diagnostics.test.ts定向复跑Test Files 1 passed / Tests 12 passed。测试
packages/core/src/logger.test.ts新增redaction matches whole words, not substrings (#5573),88 个用例:x-api-key)、全小写/全大写连写(apikey/APIKEY/accesstoken/clientsecret/privatekey)、复合复数(apiKeys/api_keys/accessTokens/clientSecrets/userPasswords/apikeys)。keys/keyword/keywords/keyboard/monkey/tokens/tokenizer/secretary/donkey/turkey/whiskey/hockey/keyedBy/tokenizerName。{ issues: [{ code: 'unrecognized_keys', keys: ['visibleIf'], path: ['nodes', 0] }] }逐字幸存(finding(core): ObjectLogger 的脱敏表按子串匹配,一个叫keys的普通字段会被整块换成***REDACTED***#5573 原始复现);反向的{ connector: { auth: { apiKey } } }深层仍被打掉。redact: ['apiKey']命中apiKey/api_key/apikey,而不放大到裸key)。反向验证(方向先判后跑)。 预判:把
isRedactedFieldName换回includes后,症状面转红、真秘密矩阵保持全绿(子串是这些拼法上的超集)。实跑与预判一致 —— 17 红,全部落在症状面(14 条no longer redacts …+ 嵌套复现 + 裸复数 + 多词 pattern),42 条真秘密矩阵与"深层嵌套仍脱敏"保持绿:其中"多词 pattern"转红是新匹配器的净增覆盖:旧的
includes下redact: ['apiKey']命不中api_key字段,现在能。门禁:
packages/core/@objectstack/service-automation都没有typecheckscript(在 #4311 的 DEBT 台账里),所以类型面的证据是上面那条比对冻结计数的 ratchet —— 计数未增长即本次改动没带进新类型错误。changeset:
.changeset/logger-redact-word-boundary.md(patch),正文含匹配语义 FROM → TO 表与影响面(host 侧自定义redact的行为变化:靠子串宽匹配"顺手"挡住某字段的部署需要显式写进redact;反向收益是同一个词现在跨拼法命中)。