Net v0.43 — "Iron Man"
Black Sabbath, 1970. v0.42 made the core lean; v0.43 armours what wraps it. Every SDK (Rust, TypeScript, Python, Go and C, the browser) was audited and every finding fixed or decided.
What's in it
- The SDK audit, remediated. A read-only audit of every SDK surface found 99 defects: 13 high, 38 medium, 48 low, none critical. All 13 highs are fixed, and so is every medium and low except a handful kept on purpose, with the reason recorded (
SDK_AUDIT_2026_10_11.md). A review of the remediation found 22 more problems in the fixes themselves, all repaired before merge (CODE_REVIEW_2026_10_11_SDK_AUDIT_BRANCH.md). - Security: a subnet-, org- or channel-only invite can no longer be redeemed for the mesh PSK; a peer can no longer grow a node's org revocation state with self-minted bundles; revoking a device now stops its own
root → devicechain. - The browser: a page's unread streams no longer buffer without bound, and two tabs can no longer run one identity at once.
- The RTC bootstrap listener, which the anchor runs, now bounds request bodies, trickle sockets and attempt tokens.
- Breaking, in a few places: two Go signatures, a handful of zero-value and validation rules, and the browser's one-identity-per-origin lock. See "Breaking changes".
Security
- A standalone invite no longer yields the mesh PSK. An invite issued only for a subnet, an org or a channel could be redeemed at the Noise enrollment listener for the full bundle, mesh PSK included. Enrollment and
MembershipIssuer::issuenow refuse an invite without the mesh relation. - The org floor service is authenticated.
serve_org_floorapplied any bundle a connected peer sent, so a peer could grow the node's permanent org revocation state without bound using org bundles it minted itself. It now applies only bundles of an org the node belongs to, and answersNotMemberto the rest. - Revoking a device revokes it.
OperatorEnrollment::revoke(device)raised the device's generation floor, but floors are checked only on link issuers, so a bareroot → devicegrant still verified. The revocation store now records revoked subjects; the coreRevocationRegistryrefuses any delegation chain or token naming one (revoke_subject,is_subject_revoked, andis_revokedcovers subjects too). The CLI gainsnet identity revoke --subject. A floor alone still verifies, so key rotation is unchanged. - Go: an empty token-roots list is a locked channel. An empty, non-nil
TokenRootswas omitted on the wire and read as "no roots", which is open. It is now sent as[]. - Browser: store handles are capped at 16 per peer, so one peer cannot exhaust a host's store handles.
The RTC bootstrap listener
The listener behind every anchor's /rtc/offer and /rtc/trickle:
- a deadline for reading a request (408 when it passes) and a 64 KiB body cap, against slow-body clients;
- trickle WebSocket messages capped at 16 KiB, and a trickle socket ended once its attempt is superseded or no longer live (the default limit was 64 MiB, with no lifetime bound);
- attempt tokens pruned when they expire, where the map used to keep every one. A token whose offer is being accepted is never pruned mid-accept.
The browser
- Unread streams are bounded. A
LeafStreamnobody reads used to buffer every inbound frame. The store, netcode and region links never read theirs, so memory grew at the tick rate. The buffer before a consumer attaches now holds 1,024 frames or 1 MiB, dropping the oldest;droppedBeforeConsumercounts what it dropped. - One identity, one live node.
connect()with a custodial identity takes a per-identity Web Lock. A second tab with the same identity is refused withIdentityErroruntil the first closes;lockIdentity: falseopts out.openSession()elects under the same lock, so mixed tabs cannot both run the identity. - Org calls from a follower tab have a deadline. The local deadline covers the open and every stream leg, and a proxied call carries the absolute deadline to the leader.
- A leader change no longer strands a store. Session streams retire with the reopenable "stale stream handle" error, so stores reopen on the new leader.
joinStore().reconnect()closes the stream it replaces. lobby.kickreads node ids in decimal, and the join key reachesauthorizeon every read decision.- A STUN probe answers what it saw. Gathering that completes with no reflexive candidate and no "no response" report is inconclusive (
notRun) only beside a public host candidate, whose reflexive twin an engine may suppress. Elsewhere it is silence, which is what blocked UDP looks like, soudp-blockedis still reported (#1236).
Fixes by SDK
Rust SDK
MigrationHandle::cancel()no longer makes a concurrent or laterwait()report success, and no longer erases a failure already recorded. Every clone of the handle sees the terminal failure. A retry that fails to send no longer leaks its orchestrator record (which made every laterstart_migrationreturnAlreadyMigrating).serve_tool_streamingno longer drops the terminalResult/Errorevent: it sends through the new backpressuredResponseSinkTyped::send_wait.DaemonRuntime::unsubscribe_channeland migration teardown no longer unsubscribe the whole node: channel leaves are refcounted across co-located daemons, andstop()tears down its ledger.- An A2A task whose terminal write is lost no longer holds a capacity slot forever: transient I/O failures are retried.
TaskRegistry::forgetrefuses a running task. - A second
publish_toolson one node joins the node's live merge state rather than failing and wiping the first publication's announcement. This also fixes the Node binding and the Python capability gateway. - Unary org calls with
deadline_ms == 0use the documented 300 s default rather than no deadline. Identity::issue_tokenclamps a sub-second TTL to one second instead of panicking.EventStreambacks off on poll errors.call_raw_bytesreturnsSdkError::Rpc(RpcError)rather than flattening it intoSdkError::Config.- A stray call admitted while Closed no longer corrupts a
CircuitBreakerthat has since opened. - Blob fetch pre-allocates at most 8 MiB, not the manifest's claimed size, before any chunk verifies.
- An identity-less
Meshinstalls a token cache, as every binding does, sorequire_tokenchannels admit valid subscribers. - Game-anchor admissions append one line each, and state writes are serialized.
MeshOsDaemonHandle::{signal_shutdown, is_registered}; SDK shutdown closes every daemon's control channel, so a parkednext_controlreturns.
TypeScript SDK
- A filtered subscription no longer stalls.
EventStreamandTypedChannel.subscribe()ignored the core'snextIdwhen a whole window was filtered out, and stalled forever. They now adopt it on every poll and page onhasMore(Python had the same defect, also fixed). DaemonRuntime.registerFactoryandMeshOsDaemonSdk.registerDaemonpass native methods bound to the instance, so class-based daemons survive migration-in and groups. An async factory on the native path is a namedDaemonError.- A throwing
onStreamDatahandler is no longer a fatal process exception, andmeshStoreTransportisolates store and app handlers. Async handler rejections are isolated too. - Deck ids parse losslessly past 2^53.
- CortEX
watch,snapshotAndWatch().updatesandRedexFile.tailreturn a closable iterable:close()wakes a parked loop, and a rejectednext()releases the native iterator. TypedChannel.subscribe({ filter })keeps the channel scoping.SubscribeOpts.cursorand acursorgetter make streams resumable.- A scalar or array event travels as
{ _value: x }and is unwrapped on receipt, as in Python. validateCapabilitiescounts UTF-8 bytes;predicateFromWirevalidates kinds and field types.
Node binding
- The unary
servedeadline spans the handler's promise, so a never-settling async handler no longer holds the call forever. onStreamDataqueues at most 1,024 events (droppedEventscounts the rest), and a subscription survives a discarded handle untilclose().setObserverfeeds a bounded queue.deliverandsnapshotno longer block a runtime worker.- Shutdown wakes a parked
accept(). A*TimeoutMs: 0that meant "forever" overflowed and panicked on Windows; it is now a safe far-future wait. - New: approver-gated
serveEnrollment(sync or async approver; admits only on an explicittrue; a denial, throw or timeout leaves the invite unburned).
Python
NetMesh.shutdown()and__exit__no longer raise "Already borrowed" while another thread is blocked in the mesh, andNet.poll/flushno longer deadlock againstshutdown.- The GIL is released on the blocking sync methods.
AsyncNetMeshkeeps its source runtime alive. A syncRpcStream, client-stream or duplexclose()wakes parked operations. - MeshOS
next_controlwaits in short slices, so it no longer deadlocks withgraceful_shutdown; after shutdown,async forover a handle ends. import net_sdkworks with a wheel built without theorgfeature. The package shipspy.typed, and the stubs match the runtime keyword names.repr(A2aServeHandle)no longer hangs.- New: approver-gated
serve_enrollment, sync or coroutine approver, same rules as Node.
Go and C
- Memory safety: a failed
RegisterDaemonWithCallbacksno longer deletes its handle twice. Group constructors hold the runtime lock, so a racingCloseor finalizer cannot free under them. A server-side stream handle is no longer freed while a goroutine the handler spawned still uses it: the new C exportnet_rpc_request_stream_retire(additive, nRPC ABI 0x0004) retires it, and Go brackets every call with a gate.net_compute_runtime_newconsumes its arguments only on success. - Hangs:
MeshRpc.CloseandOrgClient.Closeno longer deadlock with the context-cancel watcher. Streaming RPCs use the cancellable exports, so a context cancel orClose()wakes a parkedRecv,SendorFinish, and a cancel wakes asendparked on flow-control credit (returningRpcError::Cancelled).DuplexCall.SplitreturnsErrSplitWhileReceivinginstead of parking behind aRecv, and no longer cancels the call it split. MeshOSnext_controland WorkflowWaitForSeqwait in slices. - Threads: deck and meshos calls pin the OS thread across the call and its thread-local error read. CortEX
TriggerEngineandShardGroup, and deckNext, are serialized. - Positive sub-millisecond timeouts round up to 1 ms. A NUL in an audience-secret path is refused.
RpcKindCancelledandRpcError.Statusare exposed.
Smaller changes
- Deck commit (Go/C, Node, Python) commits the simulator's stored triple and requires its provenance stamp.
simulate()stamps the proposal with the time it was created, not the time it was simulated, so a commit no longer failsprovenance_mismatchwhen a millisecond passes between the two (#1238). - Dependency updates, including
serde_json,synand the web site's packages.
Version bump
Everything published moves to 0.43.0:
- every manifest: crate, wire, leaf, CLI, deck, SDK, payments, and the Go, Node and Python bindings;
- the
@net-mesh/*pins and thenet-meshPython bound (now>=0.43.0,<0.44.0); - the skills'
net-version; - the Hermes integration pin;
- the lockfiles.
Breaking changes
Nothing changed on the wire.
Go
NewRegistryClientandNewFoldQueryClienttake*MeshNode, notunsafe.Pointer. The old form took a handle no caller outside the package could obtain.RequireSignedCapabilitiesis*bool, so an unset field means the default (true) andfalsecan be said.
Behaviour
- A call
deadlineMs/deadline_msof0means no deadline in Node and Python, as in Go and C. - In Node, a handler timeout of
0onpublishToolsand the blob adapters means unbounded, as on every other serve verb; it used to fail at once. MeshNode::newrefuses a zero heartbeat interval or session timeout.- Node refuses a malformed
cancelTokenand validates enrollment and delegation TTLs (whole seconds,1..=u32::MAX). Python scoped finds requirekind. serveEnrollment/serve_enrollmentrefuse an approval timeout of0.- Node and Python
BlobRef.hashis null /Nonefor a chunked ref. - The MCP gate's rejection for a revoked device reads
Chain("Revoked"), not"RevokedSubject".
Browser
connect()with a custodial identity refuses a second live node for that identity in the same origin (IdentityError). A page that runs several nodes with one identity on purpose passeslockIdentity: false.- A
LeafStreamread late keeps only the newest 1,024 frames / 1 MiB.
How to upgrade
Bump to 0.43.0 and rebuild.
- Go: pass the
*MeshNodeto the aggregator constructors. SetRequireSignedCapabilitieswith a pointer, or leave it nil for the default. - Node / Python: a call that passed
deadlineMs: 0to mean "use the default" now has no deadline; pass the duration you want. CheckBlobRef.hashfor null before using it. - Browser: pages that open several nodes with one identity in one origin pass
lockIdentity: false. Read streams promptly, or watchdroppedBeforeConsumer. - Mixed TypeScript versions: a 0.43 publisher wraps a scalar or array event as
{ _value: x }, which a 0.42 TypeScript subscriber receives wrapped. Upgrade both sides together.
Released 2026-10-12.
License
See LICENSE.