Net as an Event Bus
Net is not Kafka. It is not NATS, not Redis Streams, not Pulsar. The API surface looks superficially similar (publish, subscribe, channels) but the underlying model is different in ways that will produce wrong code if you assume it's just another broker.
Before you write or edit any integration code, read concepts.md in this skill directory. It is the conceptual prerequisite for everything else. The API templates in apis.md will look identical to a dozen broker SDKs and you will write something that compiles and runs and is wrong.
How to use this skill
You have several reference files in this directory. Load them on demand — do not read them all up front.
| File | Read when |
|---|---|
concepts.md |
Always — before writing any integration code. The mental model. ~5 min read. |
bindings/coverage.md |
Before promising any surface exists in a given language. Which of the five bindings supports which operation, with per-binding evidence. Not every operation is at parity: Go has no A2A and no filter DSL, Node and Python reach several surfaces only through the low-level package. |
apis.md |
Before generating any code. The routing page: the four surfaces (named channels / raw firehose / raw poll / nRPC), the cross-SDK rules, and which companion to load. Then load exactly one of bindings/{rust,typescript,python,go,c}.md — not all five. |
patterns.md |
When the user describes a task ("I need a relay", "I need persistence", "I need fan-out across machines"). Maps tasks to recipes. |
mesh.md |
When the user is deploying multi-host. Production transport recipe — PSK / identity bootstrap, peer discovery, NAT traversal toggles, port mapping, 2-node and 3-node working configs. Also subnets (SubnetId / SubnetPolicy / gateway Visibility) and the channel-authorization trap: capability filters are advisory (self-advertised); require_token + token_roots is the only real boundary. |
capabilities.md |
When the user wants to route to "the GPU node" or "a node that has model X loaded". find_nodes / find_best_node / scope filters, plus the canonical schema (wire keys per axis, value-type vocabulary, reserved causal:/heat:/ai-tool:/nrpc:/subprotocol:/nat: prefixes, reserved metadata keys) and the tag-codec encoding caveats. The differentiator vs Kafka/NATS/Redis. |
scheduler.md |
When the user wants to atomically claim a contended exclusive resource under competition (a GPU island / accelerator slot / licensed seat) without double-booking across a partition — publish_island_topology / match_islands / reserve_island / claim_island — and/or drive a task lifecycle on top (WorkflowAdapter, shards, triggers). Contended arbitration + the workflow layer, distinct from advisory capability routing. |
streams.md |
When the user needs ordered point-to-point delivery (large payloads, telemetry-to-one-peer, credit-grant backpressure). Per-peer streams — different surface from the bus despite overlapping vocabulary. |
nrpc.md |
When the user needs request/response (typed call → typed reply, deadlines, retries, hedging, all four streaming shapes, cancellation tokens, RoutingPolicy, net-where: capability-targeted calls, observers + per-service metrics). Separate convention layer on top of the bus — don't reach for it for fire-and-forget broadcast. Also covers AI tool calling (#[tool] / serve_tool / call_tool / list_tools, the ToolEvent streaming envelope, OpenAI/Anthropic format translators), ingress/egress batching (batched_ingress), and the optional net-mesh typegen codegen path. |
a2a.md |
When the thing being integrated is an agent, not a service. Four surfaces: agent-to-agent task handoff (serve_a2a / submit_task / task_status / cancel_task — for parallelism, when a long job runs elsewhere while this agent keeps working, and the executor does not share its memory, so briefs carry Datafort refs rather than inlined context); delegated agent identity (DelegationChain, derive_child_seed, RevocationRegistry) — a different thing from permission-token delegation: a token says "you may," a chain says "you are acting for"; device enrollment (invite → join → approve, InviteToken, JoinRequest, no private key ever transmitted); and paid admission — an A2A service is explicitly free or paid by provider configuration (serve_a2a_configured), and a paid task goes prepare_task → purchase_task → submit_task so that validation, preflight and capacity reservation all happen BEFORE a quote exists, and a crash between payment and launch reconciles instead of double-charging. Paid serving is Rust + Python only; Node, Go and C have none of it in either direction. Rust / Python / Node only — no Go, no C (and in Node and Python it is core-only; see bindings/coverage.md). |
mcp.md |
When the user wants to bridge Model Context Protocol tools over the mesh — wrap an existing stdio MCP server as owner-scoped mesh capabilities (net-mesh wrap), or expose the mesh's capabilities to a local MCP host (Claude Code / Cursor) via net-mesh mcp serve with fail-closed consent + human-approved pinning (net-mesh mcp pin). Also credential forwarding (net-mesh forwarding) — the opt-in, deny-by-default exception to credential locality. Rides on capabilities.md (discovery) + nrpc.md (invoke); credentials stay local by default. |
org.md |
When a service must be reachable by only some organizations — a tenant-private nRPC endpoint, a partner-only capability, a service that must not appear in a plaintext announcement at all. mesh.org(credentials).call(..) / mesh.serve_org(.., OrgAccess::{SameOrg,Granted}, ..), offline org-root issuance via net-mesh org, node adoption via net-mesh node adopt, and the frozen org:<domain>:<kind> errors. A different axis from the nRPC capability gate: the unit of authority is an organization, and the service is invisible rather than refused. |
subnet-auth.md |
When a protected subnet must export a service beyond its boundary — a factory-floor API for a partner org, one API exposed from a tenant enclave, or a gateway authorized to carry protected traffic. The application surface is two ordinary verbs (mesh.serve_subnet_exported(service, exportName, handler), org.call_exported(service, req)); operators provision gateway credential sets, declare boundaries, and mint signed control facts offline with net-mesh subnet. This is subnet authority, not topology: mesh.md places nodes and gateways; it does not grant permission to cross the boundary. |
redex.md |
When the user needs durable per-channel append-only logs ("survive a node restart", "replay from offset N", "tail this channel with retention"). Local files per node; cross-node replication is opt-in per file — with the full ReplicationConfig reference (ranges, defaults, UnderCapacity, bandwidth classes), the coordinator lifecycle, SyncNack retry policy, the seven Prometheus counters, and the failure-mode table. |
cortex.md |
When the user needs folded queryable state ("SQLite-shaped queries on the event stream", "react to changes in derived state", Tasks / Memories / custom adapters, NetDB cross-adapter query façade). Sits on top of RedEX. |
dataforts.md |
When the user asks about greedy caching, data gravity (chains drift toward readers), blob refs (substrate carries content-addressed pointers; bytes in S3 / Ceph / IPFS / FS), read-your-writes (WriteToken + wait_for_token so a producer reads its own write deterministically), or peer-to-peer blob/dir transfer over the mesh (fetch_blob / store_dir / fetch_dir, the net-mesh transfer CLI). Compositional data plane on top of RedEX + CortEX. |
runtime.md |
When writing a shutdown path, handling errors, integrating into an existing async runtime (axum, FastAPI, Express), or debugging "why are my events missing?" Also partitions and healing — recovery throttling under mass failure, the Suspected → Confirmed → Healing → Healed phases, and the one that surprises people: after a conflicted heal the losing side's writes survive as a fork, not a merge, so an app assuming "one entity, one chain" silently reads only the winner. |
observability.md |
When the user asks "how do I know events are being dropped?" or wires Prometheus/OTel. Stat fields per SDK, the silent-drop trap, tuning knobs. |
payloads.md |
When the user is shaping their event schema or asking about size limits, large blobs, batching, or cross-language schema interop (u64/BigInt edges, casing, optional/null, schema evolution). |
filter-dsl.md |
When the user wants a subscriber to receive only some events on a channel (equality predicates, $and/$or/$not, dot-paths) — the bus-side filter. Not to be confused with capability predicates (capabilities.md), which select nodes, not payloads. |
error-codes.md |
When the user needs to classify a specific error variant (TokenError::Revoked, TagMatcherError::RegexNotBuiltIn, ScalingError::InCooldown, StreamError::Backpressure, RpcError::CapabilityDenied) to decide retry vs. drop vs. re-auth vs. fix-a-bug. The fuller core-crate + subsystem taxonomy under runtime.md's SDK-facing errors. |
cli.md |
When the user wants the net-mesh command surface — transfer (blob/dir recv/send/ls/status/cancel) and typegen (generate/snapshot/diff) — plus exit codes and scripting notes. |
testing.md |
When writing unit/integration tests against the SDK. Covers fixtures, race conditions, CI gotchas. |
gotchas.md |
When the user is migrating from Kafka / NATS / Redis Streams / Pulsar, or when their question reveals broker-thinking. Also "should I use Net for this at all?" — the explicit right/wrong use-case lists and the two-question test, for when the honest answer is "not yet." |
event-semantics.md |
When the user is deciding what an event should assert — naming events, or shaping payloads that carry success/acknowledgement meaning (x.ok, write.done, status: 200, delivered: true) instead of stating a fact. The doctrine: an event is a fact observed at one layer, not an end-to-end "OK." Transport success ≠ application success ≠ business success. |
source-access.md |
When you need to read Net's own source and you are not inside the Net repository. Two cases: opening a file this skill cites, and answering something this skill does not cover — how a mechanism actually works, why observed behaviour differs from what you expected, whether a symbol exists in the binding being written. One opensrc command fetches the whole tree (all five bindings, one fetch); the page carries the root map that makes shorthand citations (bus.rs, channel/config.rs, x402/mod.rs) resolvable, what the checkout will not contain (anything post-release, and the napi-generated index.d.ts), why a line anchor is a hint rather than an address, and where the tests are — they are the most direct statement of behaviour in the repository. Read concepts.md for the model first; source settles mechanism, not which surface to reach for. |
examples/ |
When the user is starting from scratch — minimal, runnable hello-world for each SDK. Use as the first thing they run after install, before they write application code. |
TL;DR mental model (the absolute minimum)
If you remember nothing else from concepts.md, remember these five things — they are what makes Net different from every other bus:
There is no broker. A channel is a name, not a process. The publisher holds the subscriber list. Fan-out is N per-peer unicasts. On the mesh transport those unicasts ride already-encrypted sessions end-to-end; on memory there is no wire at all; on Redis / JetStream payloads sit in plaintext at the broker and rely on that system's TLS for transport security. "The bus" is the mesh of nodes themselves; nothing to provision, scale, or fail over.
Backpressure is silence, not a signal. Overloaded nodes drop packets and stop responding. They do not tell the sender. Neighbors detect the silence within a heartbeat and the mesh routes around them. Producers do not slow down — the mesh finds a different consumer.
Subscribers are hot, not cold. A subscriber sees events emitted after it subscribed (plus whatever's still in the ring buffer). There is no replay-from-beginning. If you need durable replay, you need a persistence layer (Redis adapter, JetStream adapter, or RedEX) — that's a separate decision, not a default.
Every node is a peer. No clients, no servers. Producer and consumer are the same primitive (
NetNode/Net). A node can publish, subscribe, relay, and persist all at once.In TS, Python, and Rust, the transport is a runtime choice, not a code change — but memory is not a delivering transport. The same publish/subscribe code compiles and runs on memory, mesh, Redis and JetStream; on memory it selects the Noop adapter, which counts batches and discards them, so publish succeeds and
subscribe()yields nothing forever. Memory is for construction, ingestion, batching, backpressure, counters and lifecycle. Anything a consumer must actually receive needs mesh, Redis or JetStream. Go and C are a further exception (poll-based, transport-specific constructors). Seeconcepts.md§ Transport andtesting.md.
If the user's design language conflicts with any of these (e.g. "the broker", "the cluster", "consumer group", "partition leader"), stop and read gotchas.md — they're carrying assumptions from another system that will break here.
Workflow when integrating
- Select the binding, then load exactly one. Rust, TypeScript, Python, Go or C — there are no others.
- Read it off the project, do not ask if you can see it:
Cargo.toml→ Rust,package.json→ Node/TS,pyproject.tomlorrequirements.txt→ Python,go.mod→ Go, aMakefile/CMakeLists.txtbeside.c/.h→ C. A repo with several means several services; pick the one the task is in. - Then read
apis.md(routing) and exactly one companion —bindings/rust.md,bindings/typescript.md,bindings/python.md,bindings/go.md,bindings/c.md. Loading all five is how two APIs get blended into a third that does not exist. - Never default to Rust because the substrate is Rust. Most consumers never write any.
- Before promising a surface exists in that language, check
bindings/coverage.md. The bindings are not at parity — Go has no A2A and no filter DSL, payments is Rust/Node/Python only, paid A2A is Rust/Python only (Node bindssubmitTask, the free verb, and nothing paid in either direction), and several Node and Python surfaces are reachable only from the low-level package. - With no project context and no language named, ask. This is one of the few questions worth blocking on: the answer changes every line you are about to write, and a wrong guess produces code that is coherent and useless.
- Read it off the project, do not ask if you can see it:
- Read
concepts.mdif this is your first invocation in the session. - If the user is starting from scratch, run the matching script in
examples/first. Confirm the SDK is installed and working before writing application code. - Clarify the task shape — single-process or multi-host? Channels (named topics) or raw firehose? Need persistence? Need typed payloads? Read
patterns.mdfor the recipe that matches. - Pick the transport — memory (single process, does not deliver), mesh (peer-to-peer over UDP), Redis, or JetStream.
concepts.mdcovers the trade-offs. Default tomeshfor production, and tomemoryonly for tests that never assert on what a subscriber received — a delivery test on memory hangs rather than failing. For mesh production deploys, readmesh.md— PSK / identity bootstrap, peer discovery, NAT-traversal opt-ins. - Generate code from your binding companion, not from another language's example. Each companion ends with a "never infer from another binding" section naming that language's specific traps — construction is async in Node and synchronous in Python; Rust has no
channel()at all; discovery returns one node in Rust and Go and a list in Node and Python. Adapt the payload type and channel name; do not invent methods. If a name is not in the companion or the SDK source, it does not exist. - If the task is "route to a specific kind of node" (GPU box, machine with model X loaded, particular tenant), read
capabilities.md. Capability filters replace topic-based routing for placement. 7a. If the task is "atomically claim a contended exclusive resource" (N jobs want the same GPU island / accelerator slot / licensed seat; must not double-book), readscheduler.md. This is contended arbitration (exactly-one-winner CAS), not advisory capability routing — and it carries the task-lifecycle (WorkflowAdapter) layer that runs on top of a held resource. Resource-agnostic: GPU specifics ride plain tags. - If the task is "ordered point-to-point" or "large payload with backpressure", read
streams.md. The bus is fan-out + transient; per-peer streams are the right primitive when you need order + credit-grant flow control. - If the task is "request → typed reply" / "RPC over the mesh" / "I need a deadline + retry", read
nrpc.md. The bus has no return-value mechanism; nRPC adds typed call / serve, deadlines, response streaming, and end-to-end cancellation as a convention layer on top. Don't reach for it for fire-and-forget broadcast. 9b. If the task is "bridge MCP" / "wrap an MCP server" / "expose mesh tools to Claude Code / Cursor" / "pin a capability", readmcp.md.net-mesh wrappublishes a local stdio MCP server's tools as owner-scoped mesh capabilities (secrets stay in the child process, never on the wire);net-mesh mcp servefronts the mesh to a local MCP host with fail-closed consent and human-approved pinning (net-mesh mcp pin). It rides on capabilities + nRPC — use the CLI/adapter, don't hand-roll it from those primitives, and don't trust a wire-declared credential status. The one way a credential leaves the machine is opt-in, deny-by-defaultnet-mesh forwarding(remote/HTTP-only, never for a wrapped stdio server) — configured/audited today but not yet carried end-to-end. 9c. If the task is "only org X may call this" / "a private, partner-only, or tenant-private service" / "this capability must not be visible to outsiders", readorg.md. Org capability auth is a different axis from the nRPC capability gate: authority is per-organization, credentials are issued offline by an org root key (net-mesh org), the node must be adopted (net-mesh node adopt), and the service is announced only inside an encrypted audience — outsiders see no service rather than a refused one. Two verbs (mesh.org(..).call,mesh.serve_org) at parity across all five languages. Don't hand-roll it from tokens or the capability gate. 9d. If the task is "export one service across a protected subnet boundary" / "a subnet gateway" / "callers outside the enclave may reach this one API", readsubnet-auth.md. The subnet authority plane: the provider serves against a named export configured at mesh construction (mesh.serve_subnet_exported), the caller stays an ordinary org client (org.call_exported) and never joins the subnet, and every signed artifact is minted offline bynet-mesh subnet. Distinct frommesh.md's topology plane — topology places, it never authorizes — and fromorg.md's question of who is calling. - Wire the lifecycle correctly — read
runtime.mdfor the shutdown contract and async-runtime integration before plugging into the user's existing app. Always add ashutdownpath. The ring buffer needs a clean drain. - Handle errors per
runtime.md—Backpressureis the only retry-safe error; everything else indicates state change, bug, or config issue. For a variant this skill's prose doesn't explain (TokenError::*,TagMatcherError::*,ScalingError::*,StreamError::*,RpcError::*, the coreIngestionError/ConsumerError/AdapterErrortrio), readerror-codes.md— the full taxonomy with per-variant remediation. For anorg:<domain>:<kind>string, the domain alone answers "did anything leave the process?" — seeorg.md§ Errors. 11a. If the task is "the consumer should only see some events" (by payload content), readfilter-dsl.md. Equality-only$and/$or/$notpredicates evaluated post-retrieval; a missing path and an empty$and/$orboth match nothing. This is content filtering — for selecting which node answers, that's the capability predicate incapabilities.md. - Shape the payload using
payloads.md— small JSON events on the bus, references for large blobs, batched events for telemetry streams. The Schema-interop section covers cross-language traps (u64/BigInt, casing, optional/null, schema evolution). - Wire observability per
observability.md— under the defaultdrop_oldest, drops are silent andevents_droppedstays at zero, because the counters are producer-side. Always alert onevents_dropped. The file lists stat fields per SDK and Prometheus/OTel wiring patterns. - Write tests using
testing.md— memory transport for ingestion, config and lifecycle; a delivering transport (loopback mesh pair, Redis, JetStream) for anything that asserts a subscriber received something. Subscribe before publish, clean shutdown in teardown. - If the user is migrating from Kafka / NATS / Redis Streams / Pulsar, read
gotchas.mdfirst — broker assumptions will produce broken-but-compiling code. 15a. If the user is naming events or shaping event payloads — especially if an event carries a success/acknowledgement meaning (x.ok,write.done,status: 200,delivered: true) instead of stating a fact — readevent-semantics.md. An event asserts one fact observed at one layer, not an end-to-end "OK"; distinct outcomes are distinct events; transport truth belongs to the transport (Receipt/Reliable/ RedEX cursor), not the payload.HTTP 200 is not a business invariant. - If you're unsure about an API, read the SDK source directly:
- Rust:
net/crates/net/sdk/src/andnet/crates/net/sdk/examples/ - TypeScript:
net/crates/net/sdk-ts/src/ - Python:
net/crates/net/sdk-py/src/net_sdk/ - Go:
go/ - C:
net/crates/net/include/net.hThese are authoritative. The README is a good intro; the source is ground truth.
What this skill deliberately does not cover
The event-bus surface is a small slice of Net. The following exist but are out of scope here — they have their own primitives and would bloat this skill:
- Daemons / Mikoshi (live state migration) — stateful event processors that move between nodes carrying their causal chain. Read
README.md§ Daemons + Mikoshi if the user asks. - Subprotocols — custom protocols deployed incrementally over the same mesh. Out of scope unless the user is building one. Two facts are worth knowing without reading the registry: packets carrying an unregistered
subprotocol_idare forwarded unmodified and undecrypted by intermediate nodes (which is what makes rolling deployment possible — producers and consumers can upgrade before the nodes between them), and a node advertises what it handles assubprotocol:0x<id>capability tags, so "who can do daemon migration?" is a capability query, not a separate discovery mechanism. Seecapabilities.md§ Reserved cross-axis prefixes. - Subnet topology internals, identity/permission tokens (full surface) — covered briefly in
concepts.mdandcapabilities.mdbecause subnet scope filters and token-gated channels shape channel visibility, but the full identity / token issuance + delegation surface is a separate concern. Point atREADME.md§ Security surface. (Two exceptions are covered: organization capability auth inorg.md, and the subnet authority plane — exported services, gateway provisioning,net-mesh subnetissuance — insubnet-auth.md.) - Capability sensing (interest coalescing) — the mesh-wide "can any authorized provider currently satisfy Y under constraints C and latency envelope L?" plane. Still ships dark behind
enable_sensing_coalescing = false. As of 0.36 there IS a Rust SDK surface —mesh.sensing()?.provide(..)for a provider, andmesh.sensing()?.watch(..)for an own-organization exact-provider observation — but no TS/Python/Go binding, and no provider-free (AnyAuthorized), cross-organization or sensed-call surface in any language. The places it touches this skill are the scheduler's advisory input seam (scheduler.md) and the config knobs (mesh.md). If a user asks for the API in a non-Rust SDK, the honest answer is that it isn't exposed to their SDK yet. - Mesh transport internals — packet codec, Noise handshake, routing-table internals. Point at
net/crates/net/docs/. The application-level mesh setup is inmesh.md; capability routing on top of the mesh is incapabilities.md; per-peer streams over the mesh are instreams.md.
If the user asks about these, point them at the relevant section of README.md rather than guessing.