fix(security): record WHY a CMB was refused, not a constant - #38
Merged
Conversation
Owner
Author
|
Reviewed against main 2026-08-12: not superseded — main still records refusals under the constant 'bad-signature' (lib/frame-handler.js:418) while verifyCMB distinguishes four reasons. Checks pass on the branch. Needs rebase across the room rename, then mergeable. Keep open. |
sym-bot
added a commit
that referenced
this pull request
Aug 12, 2026
Three defects, found while trying to decide whether a Node 18 hang belonged to PR #38 or to its base. None of them is exotic; together they mean this repo's green signal has not been evidence for some time. A PR TARGETING A STACKED BASE RAN NO CHECKS AT ALL. The pull_request trigger was filtered to `main`, so PR #39 — targeting the cutover branch feat/cmb-only-pin-core — ran zero jobs, while `gh pr view` reported mergeStateStatus CLEAN and `gh pr checks` said "no checks reported". At the surface a reviewer actually looks at, unverified and passing are the same colour. The filter is removed: every branch that opens a PR is checked. Stacked work is exactly the work most worth checking, because its base is not main. A HANG RECORDED ITSELF AS "cancelled", NOT "failed". The Node 18 matrix job has run past six hours twice while Node 22 finished in ~30s. With no job timeout it burns to the platform ceiling; with cancel-in-progress unconditional, any later run supersedes it first. So a persistent, reproducible break left a trail of cancelled runs that reads as CI noise. timeout-minutes: 15 makes a hang fail fast and red, and cancel-in-progress is now conditional: superseding a PR push is fine, superseding `main` destroys the only record of whether the mainline is green. fail-fast: false, so a Node 18 hang can no longer cancel the Node 22 job and take the one working signal down with it. workflow_dispatch, so `main` can be run on demand. Isolating "is this break mine or the base's?" previously required pushing to main — precisely when you least want to. NOT FIXED HERE, AND DELIBERATELY SO: whatever makes Node 18 hang. engines declares node >=18 and the matrix tests it, so dropping it would be quietly narrowing a supported range to make a board go green. This change makes the failure visible and diagnosable in fifteen minutes instead of invisible for six hours; the diagnosis is its own piece of work, and it needs a Node 18 environment to reproduce. The first run of this workflow is expected to go RED on Node 18. That is the point. Claude-Session: https://claude.ai/code/session_01SpDHL4kTRWvkuw63KADT6R Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
verifyCMB already distinguishes legacy-key-rejected / bad-signature / content-mismatch / no-public-key. Every one of them was recorded as the constant focusLabel 'bad-signature' while the real reason went only to the console. On this host that made 1,538 rejections mutually indistinguishable in the decision log — 32.1% of all 4,791 SVAF decisions, the largest single verdict class. Separating them took a day of inference across three seats and still did not settle the mechanism; recording one field would have answered it in five minutes. The rejections also happen BEFORE SVAF evaluates, so they appear in no drift distribution and no admission tally: every complementarity figure any of us has published is conditioned on the survivors, and the exclusion is not random. Three changes, all in the one branch that writes this verdict: 1. focusLabel carries v.error instead of the constant, so the four verdicts are countable rather than inferable. 2. A pre-v1 key is logged as VERSION SKEW, not "forged/tampered". 764 of the 1,538 were legacy-scheme keys from peers that simply have not upgraded — logging those as forgery is both wrong and the sort of thing that gets noticed in a customer evaluation. 3. The decision records whether the refused CMB was a root or a remix. A rejected CMB is dropped and never stored, so this cannot be recovered afterwards from any store — it is captured here or not at all. The leading hypothesis for the v1 rejections is that remix key derivation consumes envelope data the receiver does not hold identically, which predicts these are overwhelmingly remixes; this boolean turns that from inference into a count. The metric keeps `reason: 'invalid'` so existing consumers are unaffected; the specific verdict rides alongside as `error`. Deliberately NOT touched: the fusion / re-signing path. The content-mismatch diagnosis that motivated it is not established for the population actually being dropped — of 459 rejected CMBs recoverable from a store, content-hash integrity was 100% (454/454 roots; remixes n=5, too few to conclude from) — and relay-key and key-churn were excluded as mechanisms. Instrumenting first is what makes that decision evidence-led rather than another inference. Tests: 4 new in cmb-signing.test.js pinning that a tampered CMB records content-mismatch, a spoofed one records bad-signature, the two are separable, the metric keeps its existing shape, and a remix rejection is flagged as one. Suite 296 tests / 295 pass. The one failure (memory-store "preserves the wire ancestor chain") is PRE-EXISTING on main, verified identical with the change stashed — it is the installed core 0.4.0 against main's ^0.3.49 pin, i.e. the cutover in flight, not this change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
sym-bot
force-pushed
the
fix/verify-error-instrumentation
branch
from
August 12, 2026 09:08
c82a09d to
0bafb27
Compare
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.
CTO-approved instrumentation from the rejected-signature investigation. Note the repo: the brief said
sym-core, but_rejectOnBadSignaturelives here in@sym-bot/sym(lib/frame-handler.js).sym-coreonly suppliesverifyCMB.The problem
verifyCMBalready distinguisheslegacy-key-rejected/bad-signature/content-mismatch/no-public-key. Every one was recorded as the constantfocusLabel: 'bad-signature', with the real reason going only to the console.That makes every refusal on a node mutually indistinguishable — and on an active mesh this is the largest single verdict class. Separating them took a day of inference across three seats and still did not settle the mechanism.
These are dropped before SVAF evaluates, so they appear in no drift distribution and no admission tally — any complementarity figure is conditioned on the survivors, and the exclusion is not random (it removes a node's own operator and doer traffic).
Three changes, all in the one branch that writes this verdict
focusLabelcarriesv.errorinstead of the constant — the four verdicts become countable rather than inferable.The metric keeps
reason: 'invalid'so existing consumers are unaffected; the specific verdict rides alongside aserror.Deliberately NOT touched — the fusion / re-signing path
The content-mismatch diagnosis that motivated it is not established for the population actually being dropped: across the rejected CMBs recoverable from a store, content-hash integrity was intact for roots, with too few remixes sampled to conclude from. Relay-key and key-churn were both excluded as mechanisms.
A supporting root-vs-remix integrity split was withdrawn under CTO challenge: it proved envelope-sensitive, swinging by an order of magnitude depending on which object is hashed. It evidences nothing and is not relied on here.
This is most valuable with
@sym-bot/core≥ 0.4.0, which returns the granularv.error.mainstill pins^0.3.49while 0.4.0 is what is installed. The code degrades gracefully (v.error || 'bad-signature'), but the pin and the cutover branch should probably settle before this is relied on for counts.Verification
296 tests / 295 pass. 4 new tests pin that a tampered CMB records
content-mismatch, a spoofed one recordsbad-signature, the two are separable, the metric keeps its shape, and a remix rejection is flagged.The single failure (
memory-store"preserves the wire ancestor chain") is pre-existing on main — verified identical with the change stashed, deps held constant. It is the installed core 0.4.0 against main's^0.3.49pin, i.e. the cutover in flight.concurrency: cancel-in-progressrecords this as "cancelled" rather than "failed", so it is not visible in run history. Do not merge on the strength of the green Node 22 job alone.Built in a worktree outside the repo; no live tree touched.
🤖 Generated with Claude Code