test(vectors): pin which vector carries which discrimination - #327
Conversation
`test_margins_have_not_thinned` stores a count and `_margin` returns a set, so both are blind to identity. A vector rewritten into a copy of its partner leaves the rule's margin at two, and `test_vectors_for_each_rule_are_independent` still passes whenever some other declared defect happens to separate the pair. What is lost in that case is the specific property the vector was written to exercise, and nothing recorded which vector carried it. `vector_roles.json` records, per fixture, the rules it is load-bearing for and the declared defects it separates. `test_no_vector_has_lost_its_role` fails by name when a fixture drops one, on the same ratchet terms as the margins file: gaining a role is an ordinary PR, losing one is updated in the same commit with a reason. This lands ahead of the #178 reissue rather than with it, because a guard that arrives after the thing it guards protects nothing. Of the nine fixtures that reissue moves, exactly one carries a discrimination: `04` is the corpus's only structurally sound signature that does not verify, so it separates `checks_structure_only` while `24`, whose signature is the wrong length, does not. That shape exists only when the signer is a key the verifier does not hold, so a single-key reissue that corrupts `04` to keep it failing turns it into a second `24`. The reissue therefore takes two published deterministic keys, and the README and the `signature_or_key_mismatch` defect comment now say so. Measured on both mutations rather than argued: - Collapsing `04` to a wrong-length signature: the new guard names it, and `test_vectors_for_each_rule_are_independent[signature_or_key_mismatch]` also fails, though its message says only that the pair is redundant. - Swapping which of `06` and `25` sits inside the sixty-second grace window, touching only unsigned context so both stay stale: the independence test and the margin ratchet both pass, and the new guard is the only thing that fails, naming `25` and the defect it stopped separating. - Renaming `04`, which is the likeliest thing a reissue actually does to a file: the independence test and the margin ratchet both pass, because the renamed vector still carries the same coverage under its new name, and again the new guard is the only thing that fails. Suite is 1403 passed and 1 skipped, with ruff, mypy and check_dashes.py clean. Signed-off-by: LouieLuNZ <48041247+lywinged@users.noreply.github.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gt2dsnVufgd4VyPFThx3JV
|
❔ Contributor Check: UNKNOWN
Automated check by AgenTrust Contributor Check. |
|
the new test |
…ng one Both ratchets in this module, the margins count and the new roles record, wrote their baseline file when it was missing and reported skipped. So a change that degraded a fixture and deleted the file in the same commit recorded the degraded state as the baseline: one run skipped, the next passed, and nothing was red. Measured on 62fea76 with 04 collapsed to a 32-byte signature and vector_roles.json removed: first run skipped and rewrote the file with 04's discrimination gone, second run passed. A missing file now fails by name. The message says to restore it from history, and that rebaselining on purpose means writing the file in the same commit that changes the fixtures, with the reason. With either file deleted the test fails; with both present the module passes 37 and the suite 1403 with 1 skipped on 3.11 and 3.12. Found by rajnisht7 on the pull request. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: LouieLuNZ <48041247+lywinged@users.noreply.github.com>
Thank you @rajnisht7 🙏, this is exactly the case the guard was supposed to make loud and it made it quiet instead. With tests/vector_roles.json deleted, the test wrote a fresh file from the current tree and reported skipped, and the next run passed. Measured on 62fea76 with 04 collapsed to a 32-byte signature and the file removed in the same change: first run skipped and rewrote the file with 04's discrimination gone, second run passed, nothing red. |
…igner The README paragraph, the defect comment and the docstring all said that a structurally sound signature that does not verify exists only when the signer is a key the verifier does not hold, and so a single-key reissue of 04 would turn it into a second 24. Measured, that is false: flipping one bit of 04's signature at its full 64 bytes keeps the issuer's kid trusted and the signature invalid, every guard in this module passes, the fixture suite passes 35, and 04 still records checks_structure_only. The guard sees the shape of a signature, not who made it. All three now say that. 04 is the only vector whose issuer key the verifier holds and whose 64-byte signature still fails to verify; 14 and 23 carry 64-byte signatures the verifier has no key for, 24 is 32 bytes. The second published key that #178 asks for stays right, on the fixture's meaning rather than on a shape the guard can enforce, and the text says it is a decision the reissue makes on purpose. No code path changes; the suite is 1403 passed and 1 skipped on 3.11 and 3.12. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: LouieLuNZ <48041247+lywinged@users.noreply.github.com>
imran-siddique
left a comment
There was a problem hiding this comment.
Approving. This is the guard from my 10 September ruling on #178, landed on its own ahead of the reissue, and it closes a hole the ruling did not name.
What the ruling asked for, and what each piece does. I said the DEFECTS assertion lands first because a guard that arrives after the thing it guards protects nothing, and that the reissue keeps its original timing. Both hold here: tests/vector_roles.json and test_no_vector_has_lost_its_role are the guard, the reissue is untouched.
The part beyond the ruling is the part worth having. test_margins_have_not_thinned stores a count and _margin returns a set, so both are blind to identity. A vector rewritten into a copy of its partner leaves the rule's margin at two and test_vectors_for_each_rule_are_independent still passes whenever some other declared defect separates the pair. The property that vector was written to exercise is gone and nothing records which vector carried it. That is a real gap and I had not seen it.
Ran it rather than read it. vector_roles.json records 28 fixtures. Suite is 37 passed. Then I mutated 04's signature from 64 bytes to 32, making it a structural copy of 24, which is the exact scenario described above. Two tests fail by name:
04-signature-key-mismatch.json: no longer separates ['checks_structure_only'] for
'signature_or_key_mismatch'. It still fails for the rule, so what it lost is the
margin rather than the coverage: it has become a copy of its partner.
The message distinguishes losing the margin from losing the coverage, which is the distinction that makes the failure actionable rather than just red.
Your measurement that 04 is the only vector of the thirty whose issuer key the verifier holds and whose 64-byte signature still fails to verify matches what I measured independently on 9 September, from the fixture bytes: 04 decodes to 64 and 24 to 32, and vector_margins.json records the rule at two.
The ratchet terms are right too: gaining a role is an ordinary PR, losing one is declared in the same commit with a reason. That is what keeps this from becoming a file people edit to make CI green.
`main` moved from a247244 to 760cc3f while this branch was open, taking df0120b (agentrust-io#315) and 760cc3f (agentrust-io#327). The only conflict is CHANGELOG.md, where agentrust-io#315 added an entry under `### Fixed` at the same anchor this branch does. Both entries are kept, this branch's first, which is the order the file already uses. Nothing else overlaps: of the eight files this branch touches, CHANGELOG.md is the only one main also touched. The check worth stating is not the conflict. This branch's sweep accounts for every parameter of every public function by name and fails on one it has not swept or declared, and agentrust-io#315 changed src/agentrust_trace/adapters/sandbox.py. It reports nothing new: on the merged tree the suite gives 1494 passed, 1 skipped and 2 xfailed, against 1491 passed and 2 xfailed on this branch alone and 1402 passed on main alone, so the three tests main adds are the whole difference and none is lost. The two xfailed are still xfailed rather than xpassed, so agentrust-io#320 is still open and the strict marker still holds. ruff, mypy on src/agentrust_trace, tools/check_dashes.py and the suite all pass on the merged tree. Signed-off-by: Louie Lu <48041247+lywinged@users.noreply.github.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
What this changes
Takes the
DEFECTSguard from #178 on its own, ahead of the01-09reissue, on @imran-siddique's ruling of 10 September: a guard that lands after the thing it guards protects nothing. The reissue keeps its original timing.test_margins_have_not_thinnedstores a count and_marginreturns a set, so both are blind to identity. A vector rewritten into a copy of its partner leaves the rule's margin at two, andtest_vectors_for_each_rule_are_independentstill passes whenever some other declared defect happens to separate the pair. What is lost in that case is the specific property the vector was written to exercise, and nothing recorded which vector carried it.tests/vector_roles.jsonrecords, per fixture, the rules it is load-bearing for and the declared defects it separates.test_no_vector_has_lost_its_rolefails by name when a fixture drops one, on the same ratchet terms as the margins file: gaining a role is an ordinary PR, losing one is updated in the same commit with a reason.Why it is this shape
Of the nine fixtures the reissue moves, exactly one carries a discrimination, which the corpus itself says rather than the argument: measured over all thirty,
04is the only vector whose issuer key the verifier holds and whose 64-byte signature still fails to verify, so it separateschecks_structure_onlywhile24, at 32 bytes, does not.14and23carry 64-byte signatures the verifier has no key for,03and17carry no receipt, and the other twenty-four verify.What the guard holds is that shape, not the signer. A single-key reissue that collapses
04to the wrong length, or renames it, fails by name. One that corrupts the signature at fixed length passes every guard, measured by flipping one bit of04's 64 bytes: the shape survives even though the fixture no longer models a wrong signer. So the second published key that #178 asks for stays right, on the fixture's meaning, and it is a decision the reissue makes on purpose rather than one this test enforces. An earlier version of this paragraph, of the README paragraph, of the defect comment and of the docstring said the shape exists only under a second key;f9841a4corrects the three files, and this paragraph is the fourth surface.The remaining six of the nine record as load-bearing with no defect of their own, which is what they are: their partners in
17-30carry the discrimination.01and02are absent from the file altogether, because they are the valid vectors and deleting a rule does not change the outcome of a fixture that passes.The two-key requirement is written down in two places, the
signature_or_key_mismatchdefect comment and theexamples/action-receipts/README.mdparagraph that already explains why01-09pin an unpublished key.Measured, not argued
Three mutations, each run against all three guards, and two more added after review:
04collapsed to a wrong-length signature0406and25swap which sits inside the grace window2504renamed0404with one bit of its 64-byte signature flippedtests/vector_roles.jsondeleted, on62fea76154e7e6The second touches only unsigned context, so both vectors stay stale and the fixture-correctness suite still passes 35 tests: the swap is a real role exchange rather than a broken fixture. The third is the likeliest thing a reissue actually does to a file. In both of those the existing instruments see nothing, because the count is unchanged and some declared defect still separates the pair. The fourth is the limit of the guard, stated above. The fifth is the reviewer's finding, below.
Each of the three ways to lose a role reports its own reason rather than sharing one closing sentence that fits only one of them.
vector_roles.jsonis byte-identical acrossPYTHONHASHSEEDvalues 0, 1, 7, 42, 12345, 31337 and 99999, which matters because_marginreturns a set and_rolesiterates it.Suite is 1403 passed and 1 skipped at
f9841a4on 3.11 and 3.12, withruff check src tests scripts,mypy src/agentrust_traceandtools/check_dashes.pyclean. The one skip istest_version's environment check.Since opened
154e7e6: both ratchets in the module fail by name when their baseline file is missing, instead of writing one and skipping. Found by @rajnisht7: with the file deleted in the same change as a degrading reissue, the first run skipped and recorded the degraded state as the baseline, and the second run passed.f9841a4: the two-key sentence corrected in the README, the defect comment and the docstring, as above. No code path changes.Type of change
None of the five. Tests and one README paragraph. No schema, wire format, signature semantics or verifier behaviour changes, and no fixture is modified.
Spec section
None.
Checklist
git commit -s)CHANGELOG.mdupdated (for any normative change): not applicable, nothing normative changes<!-- CHANGED: #NNN: description -->in spec text: not a breaking change🤖 Generated with Claude Code