Skip to main content

CapabilityAnnouncement

Struct CapabilityAnnouncement 

Source
pub struct CapabilityAnnouncement {
    pub node_id: u64,
    pub entity_id: EntityId,
    pub version: u64,
    pub timestamp_ns: u64,
    pub ttl_secs: u32,
    pub capabilities: CapabilitySet,
    pub signature: Option<Signature64>,
    pub hop_count: u8,
    pub reflex_addr: Option<SocketAddr>,
    pub allowed_nodes: Vec<u64>,
    pub allowed_subnets: Vec<SubnetId>,
    pub allowed_groups: Vec<GroupId>,
}
Expand description

Capability announcement message

Fields§

§node_id: u64

Announcing node ID

§entity_id: EntityId

Announcing entity — the 32-byte ed25519 public key. Pairs with signature so receivers can verify end-to-end, and lets the mesh’s channel-auth path resolve node_id → EntityId for token lookups.

§version: u64

Monotonic version (for diffing)

§timestamp_ns: u64

Timestamp of announcement (nanoseconds since epoch)

§ttl_secs: u32

TTL for this announcement in seconds

§capabilities: CapabilitySet

Capability set

§signature: Option<Signature64>

Optional Ed25519 signature (64 bytes, hex encoded for serde). Covers every other field EXCEPT Self::hop_count — the internal signing helper zeros hop_count before serializing and hashing, so forwarders can increment it without invalidating this signature. See Self::sign / Self::verify for the public API; the zeroing is an implementation detail of both.

§hop_count: u8

Number of times this announcement has been forwarded. Origin sets 0; each forwarder increments before re-broadcasting. Sits outside the signed envelope so forwarders don’t need the origin’s secret key. Capped at MAX_CAPABILITY_HOPS — announcements at or beyond the cap are dropped rather than re-broadcast. Old-format announcements missing this field deserialize as 0 via #[serde(default)].

skip_serializing_if omits the field when it’s zero so the SIGNED byte form stays identical to pre-M-1 announcements — a pre-M-1 node’s signature verifies on a post-M-1 node during a rolling upgrade because both produce the same canonical bytes for the origin (hop_count=0). Forwarded announcements (hop_count > 0) serialize the field; receivers still zero it in signed_payload() so verification hits the omitted-when-zero form.

§reflex_addr: Option<SocketAddr>

Observer-visible reflexive SocketAddr as seen by this node’s anchor peers during NAT classification. Populated once the ClassifyFsm (under the nat-traversal feature, in adapter/net/traversal/classify.rs) has ≥ 2 probe results; stays None on nodes that haven’t classified yet, ran with nat-traversal disabled, or landed in the Unknown bucket (different peers disagree on our port so no single address is truthful).

Peer usage. Receivers cache this alongside the nat:* tag and use it as the initial rendezvous target for hole punching — one fewer reflex round-trip per first-contact punch. The field is advisory: the punch step still waits for a real keep-alive exchange on the advertised address before handing off to the Noise handshake, so a lying peer can only fail its own incoming punches, not redirect traffic to a third party (see docs/NAT_TRAVERSAL_PLAN.md §7 for the trust model).

Wire compat. skip_serializing_if keeps the old on-wire shape when the field is None, so pre-stage-2 nodes round-trip through a post-stage-2 deserializer without breaking signatures. A post-stage-2 node deserializing a pre-stage-2 announcement sees the field default to None via #[serde(default)].

§allowed_nodes: Vec<u64>

v0.4 capability-auth allow-list — explicit NodeIds that may invoke any capability listed in capabilities. Empty vec = permissive default (anyone may invoke, subject to the other two lists). See CAPABILITY_AUTH_PLAN.md.

Capped at MAX_ALLOW_LIST_LEN (64) per axis — past that, operators use a super::group::GroupId instead.

skip_serializing_if preserves byte-identity with pre-v0.4 announcements: an unrestricted (empty) list serializes to nothing, so an existing signature verifies on a v0.4 reader and a v0.4 signature verifies on a pre-v0.4 reader (which defaults the field to empty via #[serde(default)]).

§allowed_subnets: Vec<SubnetId>

v0.4 capability-auth allow-list — super::subnet::SubnetIds whose members may invoke. Empty = permissive default. Receivers determine a caller’s subnet via the subnet:<hex> tag on the caller’s own announcement (self-declared, signed, TOFU-bound). Same wire-compat treatment as allowed_nodes.

§allowed_groups: Vec<GroupId>

v0.4 capability-auth allow-list — super::group::GroupIds whose claimants may invoke. Empty = permissive default. Group membership is self-declared via group:<hex> tags on the caller’s own announcement. Same wire-compat treatment as allowed_nodes.

Implementations§

Source§

impl CapabilityAnnouncement

Source

pub const DEFAULT_TTL_SECS: u32 = 300

Default ttl_secs value assigned by Self::new. Five minutes — long enough that a missed re-announcement on one node doesn’t immediately evict it from every peer’s capability fold, short enough that stale state clears on realistic operational timescales. Exposed as a constant so multi-hop dedup retention can be scaled off it.

Source

pub fn new( node_id: u64, entity_id: EntityId, version: u64, capabilities: CapabilitySet, ) -> Self

Create a new unsigned announcement. Receivers that run with require_signed_capabilities = true will drop it until Self::sign is called.

Source

pub fn with_ttl(self, ttl_secs: u32) -> Self

Set TTL

Source

pub fn with_reflex_addr(self, reflex: Option<SocketAddr>) -> Self

Attach the classifier’s observed reflex address. Typically called by the mesh’s capability-broadcast path after NAT classification has completed at least two probes. Pass None to clear a previously-set address — e.g. if reclassification landed in Unknown.

Included in the signed envelope: a post-signing change invalidates verification.

Source

pub fn with_signature(self, sig: [u8; 64]) -> Self

Set signature

Source

pub fn sign(&mut self, keypair: &EntityKeypair)

Sign this announcement in place with keypair. The resulting signature covers every field EXCEPT Self::hop_count — the caller must still ensure keypair.entity_id() == self.entity_id or receivers will reject with InvalidSignature.

Source

pub fn verify(&self) -> Result<(), EntityError>

Verify the signature against the announcement’s own entity_id. Ignores Self::hop_count — forwarders are expected to bump it. Returns Err if no signature is present, if the signature can’t be decoded, or if verification fails.

Source

pub fn to_bytes(&self) -> Vec<u8>

Serialize to bytes — JSON. The compact-postcard codec used by CapabilitySet::to_bytes_compact is not applied here: CapabilityAnnouncement’s wire-compat surface relies on several #[serde(skip_serializing_if = ...)] field omissions (signature, hop_count, reflex_addr, the three allowed_* lists) so signed bytes round-trip byte-for-byte against pre-M-1 / pre-v0.4 peers — and postcard’s positional encoding can’t reconstruct an omitted field. A compact announcement codec would need a separate canonicalized wire struct (TODO; tracked in PERF_AUDIT_2026_05_28_CAPABILITY.md fix #3 follow-ups).

Source

pub fn from_bytes(data: &[u8]) -> Option<Self>

Deserialize from bytes (JSON only — see Self::to_bytes). Returns None on a parse failure OR when any v0.4 capability-auth allow-list exceeds MAX_ALLOW_LIST_LEN — the cap is a wire-level invariant (operators above 64 entries per axis must use a group), so receivers reject oversized announcements at the deserializer boundary rather than scanning unbounded vectors inside may_execute on every call. Symmetric with the CLI’s announce-side check; closes the asymmetry where the substrate accepted any vector length the wire delivered.

Source

pub fn strip_reserved_metadata(&mut self)

Drop every metadata key that the substrate reserves for local trust use (intent, colocate-with, priority, owner). Call this on every announcement decoded from an inbound peer before its metadata is consulted by greedy admission, placement scoring, or anything else that lets a metadata value steer substrate decisions: pre-fix a peer could stamp intent = "high-priority-tenant-X" on its own announcement and steer the receiver’s admission to itself.

tool::* keys are NOT stripped — they’re peer-advertised AI tool descriptors (schemas, descriptions, tags) that MeshNode::list_tools surfaces to agents. Substrate never makes trust decisions from them, so stripping would only defeat cross-mesh tool discovery. See schema::METADATA_RESERVED_PREFIXES.

The schema’s metadata_reserved doc says these keys are writable by user code on the local node — the local node knows its own legitimate intent. But the same wire shape carries inbound peer announcements that the substrate must NOT trust for those decisions. This method is the boundary that draws the distinction; callers on the receive path invoke it after from_bytes.

Source

pub fn is_expired(&self) -> bool

Check if expired

Trait Implementations§

Source§

impl Clone for CapabilityAnnouncement

Source§

fn clone(&self) -> CapabilityAnnouncement

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for CapabilityAnnouncement

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl<'de> Deserialize<'de> for CapabilityAnnouncement

Source§

fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>
where __D: Deserializer<'de>,

Deserialize this value from the given Serde deserializer. Read more
Source§

impl Serialize for CapabilityAnnouncement

Source§

fn serialize<__S>(&self, __serializer: __S) -> Result<__S::Ok, __S::Error>
where __S: Serializer,

Serialize this value into the given Serde serializer. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> DeserializeOwned for T
where T: for<'de> Deserialize<'de>,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more