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: u64Announcing node ID
entity_id: EntityIdAnnouncing 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: u64Monotonic version (for diffing)
timestamp_ns: u64Timestamp of announcement (nanoseconds since epoch)
ttl_secs: u32TTL for this announcement in seconds
capabilities: CapabilitySetCapability 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: u8Number 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
impl CapabilityAnnouncement
Sourcepub const DEFAULT_TTL_SECS: u32 = 300
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.
Sourcepub fn new(
node_id: u64,
entity_id: EntityId,
version: u64,
capabilities: CapabilitySet,
) -> Self
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.
Sourcepub fn with_reflex_addr(self, reflex: Option<SocketAddr>) -> Self
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.
Sourcepub fn with_signature(self, sig: [u8; 64]) -> Self
pub fn with_signature(self, sig: [u8; 64]) -> Self
Set signature
Sourcepub fn sign(&mut self, keypair: &EntityKeypair)
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.
Sourcepub fn verify(&self) -> Result<(), EntityError>
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.
Sourcepub fn to_bytes(&self) -> Vec<u8> ⓘ
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).
Sourcepub fn from_bytes(data: &[u8]) -> Option<Self>
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.
Sourcepub fn strip_reserved_metadata(&mut self)
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.
Sourcepub fn is_expired(&self) -> bool
pub fn is_expired(&self) -> bool
Check if expired
Trait Implementations§
Source§impl Clone for CapabilityAnnouncement
impl Clone for CapabilityAnnouncement
Source§fn clone(&self) -> CapabilityAnnouncement
fn clone(&self) -> CapabilityAnnouncement
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read more