Capabilities
Capabilities describe work a node offers and the properties callers can use to find a provider. A node announces a capability set containing tags and metadata; other nodes query the local fold, apply predicates, and route or place work against the result.
The same model can describe a tool, service, device, hardware resource, physical location, operating role, software version, or current load. Applications choose which properties matter for discovery and selection.
The shape of a capability set
A capability set is two things: an opaque set of tags and an ordered map of metadata key-value pairs. The wire format is fully opaque to Net — the SDK ships with views for the conventional axes (hardware, software, devices, dataforts, tier, and so on), but those views are lazy projections, not the source of truth. If your deployment wants a custom axis, you add it; nothing in Net's core has to know about it.
A typical capability advertisement for a fleet node might look something like this when projected through the standard views:
hardware.gpu = "rtx-4090"
hardware.vram_gb = 24
hardware.cpu_cores = 32
software.cuda = "12.3"
software.net = "0.33.0"
devices.lidar = "ouster-os1"
location.region = "us-west-2"
location.zone = "us-west-2a"
tier = "production"
role = "inference"
heat.queries_per_min = 4200Some of these are stable for the lifetime of the node (hardware, installed software). Some change at human pace (location, tier, role). Some change at workload pace (heat counters, current load). The capability subsystem handles all three by giving you change detection, validation, and predicate-friendly access — without forcing you to pick a different mechanism for each rate.
Predicates
The real work happens in capability predicates. A predicate is a small expression that returns true or false against a capability set. Predicates compose with and, or, and not, and the primitives cover the four kinds of comparison you actually need in practice:
- Existence. Does this capability tag exist at all?
exists(hardware.gpu). - Equality. Is the metadata value exactly this?
equals(role, "inference"). - Numeric and semver comparison. Is this version at least this?
software.cuda >= "12.0". Is this counter under this threshold?heat.queries_per_min < 5000. - String matching. Does this metadata value match a glob or regex? Useful for matching hierarchical labels. Regex is gated behind a Cargo feature; deployments that need it opt in.
Predicates are serializable. The same predicate that you build at a Rust call site can travel as a net-where: header on an nRPC request, can be evaluated against a remote node's capability set without round-tripping back to the originator, and can be used as a filter on a channel subscription, a placement decision, or a tool-discovery walk.
Discovery and propagation
Every node's capability set is disseminated as a signed announcement on a dedicated channel. The receiving node verifies the signature against the sender's ed25519 identity, then applies the announcement to a local CapabilityFold — a typed reduction that keeps one entry per (class, node) and uses generation numbers to resolve concurrent updates. Announcements carry a TTL; entries past their TTL are evicted by the fold's expiry task. The result is that every node holds an eventually-consistent view of every other reachable node's capabilities, indexed for fast query.
The fold is the discovery substrate for everything that asks "which nodes can do X." list_tools, find_migration_targets, the placement scheduler's candidate sweep, nRPC's capability-targeted call routing, channel publisher authorization checks — all of them are predicate evaluations over the local fold. There's no separate service-discovery system; there's no central registry to query; the fold is the registry, replicated by gossip and queried in memory.
The bulk-query path is index-driven on the conventional axes (hardware,
software, devices, dataforts). Custom axes use the same index machinery via
the per-fold Index type. Query cost still depends on predicate shape, fold size,
index coverage, and update load; use deployment benchmarks for scheduling loops.
Aggregation scales the model up. When a mesh grows large enough that every node holding every other node's capability set becomes wasteful, aggregator daemons in a parent subnet subscribe to a source subnet's detail channels, summarize what they see, and publish summaries upward at a coarser granularity. The substrate provides the framework; deployments decide where to place aggregators and what summarization granularity to use.
Announcements are not always public
Everything above describes the plaintext plane, where any peer that receives an announcement can read it. There are two encrypted-only alternatives, and a capability is announced in exactly one of the three forms:
- Public — the default. Readable by every peer that receives it.
- Owner-scoped — readable only by members of the announcing node's own organization.
- Granted-audience — readable only by organizations holding a discovery grant for that capability.
The two encrypted forms ride a separate subprotocol id (0x0C04) and never appear inside a plaintext announcement payload. The consequence is the point: a node outside the audience does not discover a capability it may not use and get refused when it calls — it never learns the capability exists. Discovery becomes an authorization boundary rather than a filter.
This matters for how you reason about an empty result. A find_nodes query returning nothing is the correct and expected outcome for an unauthorized caller, and it is indistinguishable from "nobody serves that." See Organizations.
Where capabilities show up
Capabilities are an input to almost every decision in Net beyond the bus itself.
Channel routing and authorization. Channels can require that publishers and
subscribers match a capability filter — "you must have hardware.gpu," or "you
must be on tier.production and have software.cuda >= 12." The result can be
cached in the auth guard. A capability filter is matchmaking, not an access
boundary because it matches self-advertised properties. Use permission tokens
when participation must be restricted
(Channels).
Placement. When a daemon needs to run somewhere, the placement layer scores candidate nodes against the daemon's capability requirements and picks the best match. Placement filters can be hard (the daemon must have a GPU; no GPU, no placement) or soft (prefer GPUs, fall back to CPU); they can be composed across multiple axes, and they can be extended with custom scoring logic registered through a process-global callback registry.
Targeted RPC. An nRPC call can carry a net-where: predicate alongside its method and arguments. The receiver of the request evaluates the predicate against its own capability set; mismatched receivers reject the call without invoking the handler. This lets you write "call this method on any node in region.us-west-2 with software.net >= 0.20" without standing up a separate service-discovery layer.
Tool discovery. Any typed nRPC service tagged as an AI tool advertises an ai-tool:<id> capability tag through the same fold. list_tools(matcher) walks the fold in-memory, applies the matcher, and returns the descriptors. Watch-style streaming is push-driven off the fold's change signal — when a tool joins or leaves the mesh, subscribers see the change without polling.
Subscriptions. Subscribers can advertise capability requirements as part of the subscribe call. Publishers that don't match are routed away from that subscriber — useful for fan-out where not every subscriber wants every event.
Reserved axes
A handful of capability prefixes have specific meaning to the system itself:
causal:capabilities describe a node's position in the causal graph — useful for routing causal events efficiently. Blob transfer discovery rides this prefix: a node holding a chunk advertisescausal:<blake3-hex>and requesters consult the fold to find peers.fork-of:marks a node as a fork or replica of another entity.heat:is the namespace for data-gravity counters. Dataforts and CortEX use these to bias placement and caching.scope:marks the visibility scope of a capability for cross-subnet visibility decisions.ai-tool:marks a node as serving the named LLM-callable tool; populated automatically byserve_tool.subprotocol:lists the wire subprotocols the node handles; populated automatically by the substrate from the registry.
Everything else is yours to define. The substrate doesn't impose a schema; it gives you the tools to enforce one when you want to.
What you'll actually write
In application code you'll see capabilities in three shapes. You'll declare them — at process startup, building a CapabilitySet that describes the local node. You'll update them — when a counter changes, when a role transitions, when a piece of hardware becomes unavailable. And you'll query against them — when you write a placement filter, a channel authorization, a targeted RPC predicate, or a tool-discovery walk.
The right model to hold is that capabilities are the typed description of the mesh's topology. Identity says who; channels say where; capabilities say what kind of node you're talking to. Once you have all three, most of the questions you'd otherwise solve with service discovery, configuration management, or hand-rolled metadata systems become predicates over a capability set.