Skip to content

feat!: add MessageOrigin::Delegate variant for inter-delegate caller attestation#65

Merged
sanity merged 2 commits intomainfrom
feat-message-origin-delegate
Apr 14, 2026
Merged

feat!: add MessageOrigin::Delegate variant for inter-delegate caller attestation#65
sanity merged 2 commits intomainfrom
feat-message-origin-delegate

Conversation

@sanity
Copy link
Copy Markdown
Contributor

@sanity sanity commented Apr 13, 2026

Problem

When delegate A sends a message to delegate B via
OutboundDelegateMsg::SendDelegateMessage, B's process() is invoked with
origin = None. B has no way to learn which delegate (if any) called it,
because MessageOrigin only had a WebApp(ContractInstanceId) variant. This
blocks any trust policy that depends on caller delegate identity, and prevents
permission prompts from showing the actual caller for delegate-to-delegate
RequestUserInput requests (see freenet/freenet-core#3857).

Approach

Add MessageOrigin::Delegate(DelegateKey) so the runtime can attest the
calling delegate's identity. The companion freenet-core PR plumbs this
through the executor at contract.rs:479 so inter-delegate dispatch passes
Some(MessageOrigin::Delegate(caller_key)) instead of None.

Mark MessageOrigin as #[non_exhaustive] in the same change so future
variants can be added without a source-level break. This is a one-time source
break for downstream code that currently matches the enum exhaustively — they
must add a wildcard arm.

Compatibility

This is a source-level breaking change (variant added + non_exhaustive),
hence the major-bump 0.4.0 → 0.5.0. It is not a wire-format break:

  • The bincode discriminant for MessageOrigin::WebApp(..) is unchanged
    (variant index 0). Deployed delegate WASM compiled against 0.4.x will
    continue to deserialize WebApp(..) and None origins identically. A new
    wire-format pin test (webapp_origin_wire_format_is_stable) locks the
    byte layout.
  • Old delegate WASM only fails if it starts receiving an inbound
    MessageOrigin::Delegate(..). That requires another delegate to call it
    via SendDelegateMessage, which no production delegate exercises today
    (river chat delegate, ghostkey, etc. do not participate in inter-delegate
    messaging).
  • Adding #[non_exhaustive] does not affect runtime/wire behavior — it only
    changes how source code may match on the enum.

So in practice: contracts are unaffected entirely (MessageOrigin is
delegate-only); deployed delegates are unaffected unless they begin
participating in inter-delegate messaging; downstream code that builds
against 0.5.x and pattern-matches on MessageOrigin must add a wildcard arm.

Testing

  • webapp_origin_wire_format_is_stable — pins the bincode bytes for
    WebApp(..) so any future refactor that changes the wire format will fail
    loudly. Wire-format breaks at this layer would break every deployed
    delegate, so this test is the canary.
  • delegate_origin_round_trips_and_is_distinct — round-trips the new
    Delegate(..) variant and asserts its discriminant tag is 1, distinct
    from WebApp(..) so the two cannot be conflated.
  • Pre-existing delegate_interface tests continue to pass.

Drive-by: tightened a read()read_exact() test in memory::buf so
cargo clippy --all-targets is clean under newer stable clippy. CI only
clippies the wasm32 target so this didn't surface in CI; I tripped over it
running local --all-targets checks.

Release coordination

This crate must publish to crates.io as 0.5.0 before the freenet-core PR
that consumes the new variant can merge. The freenet-core PR currently
points at this branch via a [patch.crates-io] override.

Fixes

Companion to freenet/freenet-core#3860.

[AI-assisted - Claude]

…attestation

Adds a `Delegate(DelegateKey)` variant to `MessageOrigin` so the runtime can
attest the caller's identity when one delegate sends a message to another via
`OutboundDelegateMsg::SendDelegateMessage`. Previously the receiver was passed
`origin = None` and could not learn which delegate invoked it, blocking any
authorization scheme that depends on caller identity (freenet/freenet-core#3860).

The enum is now `#[non_exhaustive]` so future variants can be added without a
source-level breaking change. This is a one-time source break for downstream
code that currently matches `MessageOrigin` exhaustively — they must add a
wildcard arm.

Wire-format compat: bincode discriminant for the existing `WebApp(..)` variant
is unchanged. A new wire-format pin test (`webapp_origin_wire_format_is_stable`)
locks the byte layout so this guarantee can't drift in future releases. Deployed
delegate WASM compiled against 0.4.x continues to deserialize `WebApp(..)` and
`None` origins identically. Only delegates that start receiving inter-delegate
calls carrying the new `Delegate(..)` variant need rebuilding against 0.5.x —
no production delegate exercises that path today.

Drive-by: tighten a `read()`/`read_exact()` test in memory::buf so
`cargo clippy --all-targets` is clean on stable (the bug surfaces under newer
clippy versions; CI's wasm-only clippy didn't catch it).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…emantics

Addresses skeptical-review feedback on freenet-stdlib#65:

1. Wire format pin: assert the FULL byte layout for `MessageOrigin::Delegate`
   (variant tag + DelegateKey serde repr), not just the variant tag. If
   `DelegateKey`'s serde representation ever changes, deployed delegates
   compiled against a previous stdlib will silently fail to deserialize
   inter-delegate origins — exactly the failure mode this test exists to
   prevent.

2. Document the inherited-origin revocation semantics in the
   `MessageOrigin::Delegate` rustdoc: an inter-delegate message replaces
   rather than composes with any inherited WebApp origin the calling
   delegate may itself hold. The receiver sees only Delegate(caller_key)
   and does not gain contract access on behalf of any web app the caller
   was acting for. Authorization should be made on the calling delegate's
   identity alone.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@sanity sanity merged commit a87b998 into main Apr 14, 2026
8 checks passed
@sanity sanity deleted the feat-message-origin-delegate branch April 14, 2026 00:47
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