Skip to content

test(vectors): pin which vector carries which discrimination - #327

Merged
imran-siddique merged 3 commits into
mainfrom
test/vector-role-ratchet
Sep 11, 2026
Merged

test(vectors): pin which vector carries which discrimination#327
imran-siddique merged 3 commits into
mainfrom
test/vector-role-ratchet

Conversation

@lywinged

@lywinged lywinged commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

What this changes

Takes the DEFECTS guard from #178 on its own, ahead of the 01-09 reissue, 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_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.

tests/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.

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, 04 is the only vector whose issuer key the verifier holds and whose 64-byte signature still fails to verify, so it separates checks_structure_only while 24, at 32 bytes, does not. 14 and 23 carry 64-byte signatures the verifier has no key for, 03 and 17 carry no receipt, and the other twenty-four verify.

What the guard holds is that shape, not the signer. A single-key reissue that collapses 04 to 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 of 04'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; f9841a4 corrects 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-30 carry the discrimination. 01 and 02 are 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_mismatch defect comment and the examples/action-receipts/README.md paragraph that already explains why 01-09 pin an unpublished key.

Measured, not argued

Three mutations, each run against all three guards, and two more added after review:

Mutation independence margin ratchet this guard
04 collapsed to a wrong-length signature fails passes fails, names 04
06 and 25 swap which sits inside the grace window passes passes fails, names 25
04 renamed passes passes fails, names 04
04 with one bit of its 64-byte signature flipped passes passes passes
tests/vector_roles.json deleted, on 62fea76 passes passes skipped and rewrote the file; fails by name since 154e7e6

The 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.json is byte-identical across PYTHONHASHSEED values 0, 1, 7, 42, 12345, 31337 and 99999, which matters because _margin returns a set and _roles iterates it.

Suite is 1403 passed and 1 skipped at f9841a4 on 3.11 and 3.12, with ruff check src tests scripts, mypy src/agentrust_trace and tools/check_dashes.py clean. The one skip is test_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

  • Editorial (typo, link fix, clarification: no normative effect)
  • Non-breaking spec change (new optional field, new platform profile, informative addition)
  • Breaking spec change (requires 14-day comment period and Project Lead sign-off)
  • Schema change
  • Example addition

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

  • DCO sign-off on all commits (git commit -s)
  • CHANGELOG.md updated (for any normative change): not applicable, nothing normative changes
  • Breaking changes marked with <!-- CHANGED: #NNN: description --> in spec text: not a breaking change
  • Backward compatibility statement included (for breaking changes): not a breaking change

🤖 Generated with Claude Code

`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
@lywinged
lywinged requested a review from a team as a code owner September 10, 2026 20:36
@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Contributor Check: UNKNOWN

Check Result
Profile UNKNOWN
Credential LOW
Overall UNKNOWN

Automated check by AgenTrust Contributor Check.

@github-actions github-actions Bot added the needs-review:UNKNOWN Contributor check flagged UNKNOWN risk label Sep 10, 2026
@rajnisht7

rajnisht7 commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

the new test test_no_vector_has_lost_its_role checks the file vector_roles.json to confirm if all vectors are working properly, but if the file is missing, the code creates the file and makes the test green.
the issue is if someone deletes the file, the file will be created automatically during the test, and will make the test green but the deletion of file is never detected, that is guard can turn itself off

…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>
@lywinged

Copy link
Copy Markdown
Collaborator Author

the new test test_no_vector_has_lost_its_role checks the file vector_roles.json to confirm if all vectors are working properly, but if the file is missing, the code creates the file and makes the test green.

the issue is if someone deletes the file, the file will be created automatically during the test, and will make the test green but the deletion of file is never detected, that is guard can turn itself off

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.
Fixed in 154e7e6. A missing file now fails by name, and the message says to restore it from history, or to rebaseline on purpose in the same commit that changes the fixtures, with the reason. The margins ratchet above it had the same shape and takes the same fix, since your point applies to both. With either file deleted its test fails; with both present the module passes 37 and the suite 1403 with 1 skipped on 3.11 and 3.12, the one skip being test_version's environment check, and CI is green on this head.

…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 imran-siddique left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@imran-siddique
imran-siddique merged commit 760cc3f into main Sep 11, 2026
6 checks passed
@imran-siddique
imran-siddique deleted the test/vector-role-ratchet branch September 11, 2026 15:34
lywinged added a commit to lywinged/trace-spec that referenced this pull request Sep 11, 2026
`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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-review:UNKNOWN Contributor check flagged UNKNOWN risk

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants