feat(F257): #4 签名 lint O2→O1 — message-signature detection layer (observe-only) - #47
Merged
Merged
Conversation
Why: 消息末行 [昵称/模型🐾] 签名此前仅靠 prompt 文本约定(O2 观测层),零 结构覆盖 → dev-7a882ba0(Fable 漏签靠 operator 人工发现)。本切片交付 #4 的 **检测层**(O1 结构强制):agent 消息末行是否以被识别的猫签名结尾。 两阶段(owner vision-guardian 校准,AC 完成 ≠ feature 完成): - ① 检测层(本切片):复用 isCatSignatureLine(F167,不重造正则)→ post-seam 记 message.extra.signatureLint(message 级可观测、denominator-bearing、 observe-only 非阻断,绝不 reject 已持久化用户可见消息)。 - ② 账本闭环(deferred 到切片 2 / #3 后):检测到 miss 自动 emit deviation 归因 obj-identity-integrity 进 harness ledger,自动化 dev-7a882ba0 的手动 report_harness_signal。deferred 原因:ledger 读 DeviationEventLog/ GuardRejectionEventLog/eval verdict 不扫 message.extra;正确 deviation 需 registered objective(否则重蹈 #3 修的 free-string 考古)+ segment/condition 归因基建(condition_hit 需 condition registry+typed fact,均属 #2/#3 数据根)。 extra-only ≠ #4 完成——已在 spec #4 + 代码 SCOPE 注释显式在案。 设计自决:observe-only 非阻断(阻断逼近铁律5/硬排除,风险高);#4 检测 ⊥ #3 (不依赖 registry);signatureLintExtra helper 提取使 post-message 巨型 handler biome 复杂度对基线 0 delta。 证据:19 单测(纯函数+helper)+ 2 wiring e2e(extra.signatureLint signed true/false)+ 79 blast-radius 回归全绿;tsc/biome clean;新文件 65 行、无 any。 [宪宪/claude-opus-4-8🐾] Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
R3 review 四项全部核实成立(读码验证,非反射接受),本 commit 逐项修复 + red→green: P1-3 严格契约 matcher(correctness):lintCatSignature 之前复用 isCatSignatureLine (routing 的 permissive STRIP matcher,容忍 [Spark🐾] 无模型 / [砚砚/GPT-5.5] 无爪), 把契约违规签名误判为 compliant。改用严格 `[nickname/model🐾]` 正则(slash+model+paw 必需),断言 governance-l0「签名必须含模型型号」。strip=permissive(routing) 与 lint=strict(compliance) 分离。两个原正例 flip 成反例 + 保留严格正例。 P1-1 覆盖(coverage):签名 lint 之前只接 callback post-message 两支,漏掉 serial/ parallel 以 origin:'stream' 持久化的普通 agent 最终文本 → 从不 post_message 的猫不入 分母,签名率系统性偏差。route-serial.ts:2805 + route-parallel.ts:1248 各接 signatureLintExtra(storedContent)。加 serial+parallel 集成测试(signed/unsigned 均入分母)。 P1-2 持久化(durability):serializeExtra 写入但 safeParseExtra 白名单重建时丢弃 signatureLint → Redis 回读后字段消失(in-memory 测试测不到)。parser 加 signatureLint strict 支持(typeof signed === 'boolean')+ serialize→parse round-trip 测试(true/false/ malformed drop/其他字段不受影响)。 P2-1 可达(reachability):GET /api/messages 白名单、两处 callback socket broadcast、 web ChatMessage.extra、server AgentMessage.extra 均漏 signatureLint → Console 读不到。 逐一透传(broadcast 转发 storedMsg.extra.signatureLint,单一真相源)+ cold-hydration (GET)+ live-broadcast 契约测试。无 UI 渲染(#6 消费)。 证据:F257 slice-1 全套 82 tests + blast-radius 回归 60 tests 全绿;tsc/web-tsc clean; biome 新文件 clean,route-serial/parallel/callbacks 复杂度对基线 0 delta;safeParseExtra 48→50、messages GET 70→73(+2/+3,逐字段 established pattern,函数本已远超限,无新增 erroring function)。两阶段账本闭环仍 deferred(sol 已 accept)。 [宪宪/claude-opus-4-8🐾] Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
sol R4 REQUEST CHANGES 两项核实成立(读码 + runtime 真相源),逐项修 + §16e 全量 sweep: P1 严格 matcher 拒真实模型标识(correctness):STRICT_SIGNATURE_LINE_RE 假设单斜杠,但 opencode 运行时 defaultModel="codex-for-me/gpt-5.4"(provider-qualified,含斜杠)→ [金渐层/codex-for-me/gpt-5.4🐾] 被误判 unsigned、按 cohort 污染分母。改:首斜杠分隔 nickname/model,model 允许 provider-qualified 斜杠,两段 trim 后非空必需(拒 [ /m🐾] / [n/ 🐾])。flip multi-slash 为正例 + roster provider-qualified 正例 + 空段负例。 P2 §16e 全链 reachability(补锅匠 → failure-mode sweep):R3 只补 server 侧 + web ChatMessage 类型,漏 web 投影链全体 + 2 处 dup-recovery broadcast。做全量 sweep(api 20 点 + web 14 点)逐点核 fixed/N-A,修复 14 点: - API: A13/A14 dup-recovery broadcast forward(+ A11-A14 抽 pickSignatureLint helper, callbacks 复杂度对基线 0 delta) - Web 类型: W3 AgentMsg / W4 BackgroundAgentMessage / W5 history-response - Web runtime: W6 cold-hydration(guard+emit) / W7 mergeMessageExtra(sweep 发现的 uncited 点) / W8-W14 useAgentMessages callback side-patch×7(pickSignatureLint 统一 forward) - sweep 发现 sol 未列的 3 个 drop 点(W7/W13/W14)——证明逐点补 sol 列表会再 bounce Divergence: 6+ 异构 allowlist(不同 key 子集),不可统一 projector;pickSignatureLint 1-line forwarder per-package DRY(sweep 建议)。 证据:api F257+回归 105 全绿;web reachability vitest 8 + web tsc clean;biome clean、 callbacks 复杂度 0-delta;无 any。A18/A19(F246 dispatch origination,fresh extra 无 drop)+ A16/A17(full passthrough)文档化 N-A。 [宪宪/claude-opus-4-8🐾] Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
mindfn
added a commit
that referenced
this pull request
Jul 21, 2026
…rity + fail-closed) (#48) * feat(F257): #3 objective registry — 只读发现(取代 report_harness_signal objectiveId 考古) 修复清单 #3:report_harness_signal 的 objectiveId 是自由 string,零 list/schema/注入清单 → 三次上报三次考古。本 PR 建 objective registry 只读发现层: - registry YAML(docs/harness-feedback/objectives/registry.yaml,KD-3 registry-as-data)= 定义层(id/statement/segments)。**只登 canonized 目标**——redesign §2 名义 8 objectives 但只 canonize 了 2 个 id(obj-routing-delivery + obj-identity-integrity);不 invent 另 7 个 id(会伪造 V2 依赖的契约),registry 随 canonize 增长。 - loader(objective-registry.ts):parseObjectiveRegistry 纯函数(malformed 行丢弃不抛、 malformed YAML → 空)+ loadObjectiveRegistry fail-safe。 - GET /api/callbacks/objectives(镜像 rich-block-rules 只读路由)。 - cat_cafe_list_objectives MCP tool(镜像 rich-block-rules-tool;A_READ_LOCAL + readonly allowlist 注册)。 - report_harness_signal objectiveId description 同步指向 list_objectives。 边界:只做定义层 + 发现,不做 V2(evaluator/metrics/condition registry)。objectiveId 仍 自由(discovery-only,软校验留 V2)。 证据:9 tests(parseObjectiveRegistry 4 + loadObjectiveRegistry 2 + 路由 e2e + shipped registry 验证)全绿;api + mcp-server build clean;biome clean。切片 2a,独立 #47。 [宪宪/claude-opus-4-8🐾] Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(F257): #3 (2a) R1 — single authority + fail-closed + contract tests + map delta Addresses sol's slice-2a R1 REQUEST CHANGES (all four, no pushback). ## P1-1 — remove `segments` (second writable authority) The registry authored `id/statement/segments`, duplicating the unit→objective membership that frozen redesign §4.8 keeps in the versioned UnitEvaluationManifest. Dropped `segments` from YAML + loader type + API output + MCP tool output; the registry is now the canonical `id + statement` definition layer only. Attachments become a V2 derived read-model from UnitEvaluationManifest — one authority. ## P1-2 — fail-closed loader (no silent empty), strict validation, lower complexity parseObjectiveRegistry/loadObjectiveRegistry now return a discriminated ObjectiveRegistryResult ({ok:true,registry} | {ok:false,error}). Unreadable file / malformed YAML / non-mapping root / non-positive-integer version / non-array objectives / any invalid row / duplicate id → explicit failure, never {version:1,objectives:[]}. Validates positive-integer version, trimmed non-empty id matching ^obj-[a-z0-9]+(?:-[a-z0-9]+)*$, trimmed non-empty statement, unique ids (sol repros now rejected: version -2.5, whitespace id/statement, dup ids). Route returns 503 (uncached) on failure; MCP tool surfaces it via its existing !ok error path. Validation extracted into helpers → below the Biome complexity-17 warning. Route gains an optional injectable registry path (test seam). ## P2-1 — MCP contract tests restored to true green Added cat_cafe_list_objectives to EXPECTED_TOOLS / EXPECTED_COLLAB_TOOLS / EXPECTED_READONLY_TOOLS, and cat_cafe_report_harness_signal to TOOLS/COLLAB (the PR #42 leftover that made the suite baseline-red 385/387) — not masking the new mismatch behind the old failure name. Added focused handler tests (success / honest empty / 503 HTTP failure / network error). Full mcp-server suite 391/391. ## P2-2 — architecture map delta harness-eval cell: added objective-registry.ts / callback-docs-routes.ts / list-objectives-tool.ts code anchors + registry.yaml doc anchor + scan hints + why this is the definition layer of the SAME control plane, not a parallel registry. ## Evidence - api: f257-objective-registry 18/0, callback-docs-route 4/0 (200 + 503 fail-closed). - mcp-server full suite 391/391 (was 385/387). - biome: changed files clean (useLiteralKeys + parser complexity + format resolved). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(F257): #3 (2a) R2 — path-free 503 + strict key-set (reject unknown, not strip) Addresses sol's 2a R2 REQUEST CHANGES (both small, no scope change). R1's four findings were confirmed CLOSED. ## P2-1 — unauthenticated 503 leaked internal path / fs errno callback-docs-routes.ts /api/callbacks/objectives is unauthenticated but returned the loader's raw error, which contains the registry path + Node fs errno (e.g. "...registry unreadable at /private/secret/install/objectives.yaml: ENOENT..."), forwarded onward via the MCP tool's response.text(). Now: log the full internal reason server-side (request.log.error) and return a stable, path-free 503 body `{ error: 'Objective registry unavailable' }`. Test asserts the body contains neither the passed path nor ENOENT/errno, and is uncached; the MCP tool still recognizes 503 as an errorResult. ## P2-2 — "strict + single authority" schema was still fail-open on forbidden fields The loader read only id/statement and silently dropped everything else — so a stray `segments` (or a typo / future unversioned field) could reappear in the YAML, mislead a human into treating it as membership authority, while the runtime ignored it. Worse, the R1 test asserted that strip-behavior as "valid". Now registryVersion=1 fail-closes on the key set: root allows only registryVersion/objectives; each entry allows only id/statement. A `segments` key REJECTS (message points to the UnitEvaluationManifest §4.8 authority), not strips; any unknown key rejects. Future fields must bump the schema. Removed the fail-open "ignores segments" test; added red→green rejections for segments, unknown entry key, and unknown root key. ## Evidence - api: f257-objective-registry 21/0 (now includes segments/unknown-key rejections + descriptive-message assertion), callback-docs-route 4/0 (path-free 503 asserted). - shipped registry.yaml still validates (id/statement only). biome changed files clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(F257): #3 (2a) R3 — pin registryVersion to exactly 1 (fail closed on unsupported) Addresses sol's 2a R3 sole finding (R2's two remain CLOSED). ## P2-1 — unsupported schema versions were accepted under v1 semantics The key-set contract advertised "registryVersion=1 allows only … / future fields must bump schema", but parseObjectiveRegistry accepted EVERY positive integer and applied the v1 parser (repro: v1/v2/v999 all ok:true). That advertises an unimplemented version back to discovery clients as supported. Now the version gate requires exactly SUPPORTED_REGISTRY_ VERSION (=1); anything else fails closed with a clear message. A future v2 must ship an explicit versioned parser + bump the constant, not ride the positive-integer gate. Tests: changed the version-2 "valid" fixture to 1; added v2 + v999 rejection cases (the existing -2.5 / 0 / 1.5 / missing cases still reject as ≠1). ## Evidence - f257-objective-registry 23/0. biome changed files clean; build clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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.
What
F257 修复清单 #4 — message 末行签名
[昵称/模型🐾]的 O2→O1 结构升级(检测层)。把"签名必须含模型型号"从纯 prompt 约定(O2 观测层;dev-7a882ba0 靠 operator 人工发现)升级为 regex-decidable 结构断言(O1 结构强制),结果落message.extra.signatureLint,供 Console / eval 消费。Why
四层 harness 里签名约定零结构覆盖(dev-7a882ba0 实锤:Fable 漏签靠 operator 人工发现)。#4 = 自动检测缺签 / 不合规签名,替代人工发现。
Design
[nickname/model🐾],首斜杠分隔 nickname/model,model 允许 provider-qualified(含斜杠,如 opencode 运行时codex-for-me/gpt-5.4),两段 trim 后非空必需。不复用 routing 的 permissive strip matcher(isCatSignatureLine是 strip 用,容忍无模型[Spark🐾]/无爪[砚砚/GPT-5.5],作 compliance 会漏判)。presence-only;identity-correctness(签名匹配发帖猫)deferred。extra.signatureLint在 persist(callback×2 + serial/parallel stream-final)→ Redis serialize/parse round-trip → GET + socket broadcast(primary + duplicate-recovery)→ web 投影链(cold-hydration / merge-reconcile / background+active callback side-patch 全体)逐点透传。§16e 全量 sweep(34 点)确认无遗漏;pickSignatureLint1-line forwarder per-package DRY(异构 allowlist 不可统一 projector)。obj-identity-integrity,自动化 dev-7a882ba0 的手动 report)deferred 到 feat(#476): unify callback auth — headers replace body/query #3(objective registry)——harness ledger 读 DeviationEventLog/eval verdict 不扫message.extra;正确 deviation 需 registered objective。详见 spec feat(#603): .local / .local-override rule overlay support #4 +cat-signature-lint.tsSCOPE 注释。Review
跨家族 review(缅因猫 sol / gpt-5.6-sol)R1→R5,APPROVE @
f8d3c33da。含 §16e failure-mode sweep(extra allowlist 全链投影漏字段的重复失效模式,逮到初版 checklist 外的 3 个漏点)。Tests
signatureLintExtra+pickSignatureLint+mergeMessageExtra(web,含 signatureLint-only guard 不变式)。Scope
检测层完成;账本闭环 deferred 到切片 2(#3 objective registry)。两阶段已在 spec #4 显式在案。
🤖 Generated with Claude Code