Skip to content

Add opaque-digest handoff bodies with corrected discriminators - #40

Merged
germ-mark merged 3 commits into
mainfrom
llm/opaque-handoff-bodies
Jul 22, 2026
Merged

Add opaque-digest handoff bodies with corrected discriminators#40
germ-mark merged 3 commits into
mainfrom
llm/opaque-handoff-bodies

Conversation

@germ-mark

Copy link
Copy Markdown
Contributor

createNewAgentHandoff and PublicAnchorAgent.verify(anchorHandoff:…) gain overloads taking groupContext/mlsUpdateDigest as Data, backed by v2 signature bodies that commit to those bytes as length-framed values and never interpret them. The motivation is ownership: a digest's algorithm is a facet of the MLS backend's cipher suite, so requiring a TypedDigest meant the backend could not adopt a new suite until this package shipped a matching DigestTypes case — a release dependency pointing the wrong way. This is safe because a verifier never parses a digest out of a handoff; it rebuilds the body from its own locally derived reference digest and checks the signature against that, so agreement on the algorithm is enforced where the session is established rather than here.

Purely additive. The typed overloads and the v1 bodies are untouched, so every already-issued handoff keeps verifying and nothing needs to re-pair — which is why this is v2-alongside rather than a replacement: classical relationships are live.

The discriminator fix you were looking for. v1's ActiveAgentBody and RetiredAgentBody carry each other's names. It's a labeling bug rather than a security one — domain separation needs the committed strings to be distinct, not correctly named, and they are, so nothing can be replayed across contexts. But it's a real landmine for anyone writing a spec or a second implementation from the names. It is now frozen and commented (renaming in place would silently fail verification for every live relationship), and v2 carries the corrected names. They're .v2-suffixed because the plain corrected strings are unavailable — v1 has them live on the opposite structs, so reusing one would put two different structures under a single committed string. ActiveAnchorBodyV2 takes the suffix too, though its name was never swapped, to keep discriminator↔encoding strictly 1:1.

Tests: 6 new, full suite 118 green. The round trip runs on a digest tagged 0x7F — a value TypedDigest cannot represent at all — which is the property the change exists for. v1↔v2 non-verification is pinned in both directions, and one test asserts all six discriminators are pairwise distinct while another pins the frozen swap so nobody "corrects" it later.

Adoption is not urgent: the app can keep calling the typed overloads by lifting the backend's bytes with TypedDigest(wireFormat:), which is byte-identical. Moving to these overloads is what removes the last cross-repo release coupling, and it costs a PQ re-pairing whenever you choose to do it.

A digest's algorithm is a facet of the MLS backend's cipher suite, so requiring
a TypedDigest meant the backend could not adopt a new suite until this package
shipped a matching DigestTypes case — a release dependency pointing the wrong
way. The v2 bodies commit to length-framed opaque bytes instead, so the caller's
own self-describing encoding travels inside the value and cross-era signatures
stay unambiguous without this package knowing the eras.

Safe because a verifier never parses a digest out of a handoff: it rebuilds the
body from its own locally derived reference digest and checks the signature
against that. Algorithm agreement is enforced where the session is established.

Purely additive — the typed overloads and v1 bodies are untouched, so every
issued handoff keeps verifying, and v1/v2 are mutually unverifiable by
construction. v1's ActiveAgentBody and RetiredAgentBody carry each other's
discriminators; that is a labeling bug rather than a security one (separation
needs distinctness, not correct names) and is now frozen and commented, since
renaming in place would silently break every live relationship. v2 takes the
corrected names, suffixed .v2 because v1 holds the plain ones.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jul 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b3842d1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@germ-network/autonomous-comm-protocol Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

germ-mark and others added 2 commits July 22, 2026 14:05
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The typed overload made digest well-formedness structural — a TypedDigest is
fixed-width, so empty was unrepresentable. The opaque one moves that to runtime,
and empty is the one degenerate value both ends could plumb symmetrically from a
default-value integration bug: the signatures would agree and the handoff would
verify with its MLS binding absent. Mint and verify both refuse it, so the bug
is loud at whichever end introduced it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@germ-mark
germ-mark merged commit 12c793c into main Jul 22, 2026
8 checks passed
@germ-mark
germ-mark deleted the llm/opaque-handoff-bodies branch July 22, 2026 21:13
@github-actions github-actions Bot mentioned this pull request Jul 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant