Mesh
A Mesh is a provider-owned node operations plane. It may be a single node, a pair of nodes, a chain such as node 1 ↔ node 2 ↔ node 3, or a routed tree or graph. The name is intentionally broader than transport or tunnel: byte streams and datagrams are Mesh operations, but the same Mesh can survey devices, upload and execute files, run commands, load provider-defined components, publish triggers, and report beacons.
Choose your path
The machine-readable companion to the API guide is the daemon OpenAPI document.
Why not transport or tunnel?
TransportEndpoint remains the narrow chain capability for byte
movement. A tunnel is an operator workflow that bridges a stream to a local
IP address and port so a compatible client can connect without knowing how
the route works. Mesh is the umbrella for the whole node operations plane.
Mesh model
- Mesh Node — an addressable participant such as the controller, a relay, a target-side agent, or a managed implant.
- Mesh Link — a communication edge between two nodes.
- Mesh Route — an ordered path across nodes and links used for a task or stream.
- Mesh Destination — a host or service reachable through a node or route but not modeled as a Mesh Node.
- Mesh Task — a node, route, or destination operation such as survey, upload, execute, upload_execute, command, load, or stream.
- Mesh Listener — a provider-reported listening post or data-plane rendezvous resource, distinct from both the provider control-plane adapter and a daemon-owned Mesh Bridge.
- Mesh Bridge — a daemon-owned authenticated loopback TCP or UDP endpoint that forwards one local client or peer to one provider-owned Mesh session flow.
- Trigger — a provider-declared condition that can cause Mesh work or state changes.
- Beacon — a time-stamped liveness or status signal from one node.
Where TLS and credentials fit
Workspace PKI owns certificate custody and lifecycle; a Mesh provider owns
the protocol that consumes a certificate. Hovel does not infer TLS from a
listener kind, a port, or a protocol string. Instead, a provider
advertises strict credential slots, an operator binds an assignment to the
intended consumer, and a credential-aware Mesh request selects that
assignment without carrying secret bytes.
- active PKI assignment
- non-secret assignment and slot selection
- validate provider descriptor and consumer ID
- resolve short-lived projection
credential.runtime- listener, task, or stream in the same process
- digest-only execution record and cleanup
| Surface | Typical TLS role | Assignment identity | Implemented Mesh selection behavior |
|---|---|---|---|
| Provider control plane | TLS client or server used by the provider itself. | mesh-provider with the canonical, unversioned provider name, such as lab-relay-mesh. | The provider binding is allowed for credential-aware listener start, task, stream, and bridge-open calls. |
| Mesh Listener / listening post | TLS or mTLS rendezvous endpoint. | mesh-listener joins slash-free provider and listener components as lab-relay-mesh/listener-west; slash-bearing components use the opaque digest form below. | Allowed when the operation names that exact listenerId. The broader listening-post assignment type is not derived by these Mesh calls. |
| Mesh Node | Node-side client or server identity. | mesh-node uses the same canonical encoding; a slash-free ID remains lab-relay-mesh/relay-west. | Allowed only when the operation explicitly names that nodeId; route membership alone does not derive node consumers. |
| Implant or stager | Artifact-embedded identity, bootstrap identity, or a later runtime identity. | The assignment model includes implant and stager consumer types with operator-selected IDs. | Those consumer types are extension points for Mesh orchestration today. If the implant is exposed as a Mesh Node, the implemented runtime path can bind its node identity as mesh-node. Build-time stamping is a separate credential-provider workflow. |
| Routed stream | A provider may establish TLS itself, or it may carry already encrypted application bytes unchanged. | No stream consumer type is derived. Selections bind to the provider, explicit listener, or explicit node. | OpenMeshStream delivers selected material to the provider process before mesh.open_stream. |
| Local bridge | Local adapter for a provider-owned session. | The bridge has no PKI assignment identity. | OpenMeshBridge uses the same credential-aware stream-open path; credentials go to the provider, not to the bridge or local client. |
When either component contains /, Hovel emits
mesh-scoped:<sha256>. The lowercase hexadecimal digest is
SHA-256 over the canonical UTF-8 provider name, one NUL byte, and the
canonical UTF-8 listener or node ID. This bounded form prevents ambiguous
component boundaries. Prefer the typed domain constructors and retain the
returned assignment consumer ID rather than concatenating components.
An installed module ID may include a version, for example
lab-relay-mesh@v0.1.0. Assignment consumer IDs never include that@versionsuffix. Hovel still binds delivery to the exact installed module version and the SHA-256 digest of its current credential descriptor before releasing material.
Certificate and private key material use separate slots.
One active assignment can be selected twice with distinct request IDs and
slot names: one slot accepts certificate-der with form
public, while the other accepts
private-key-pkcs8 with form private-bytes. This lets
the descriptor forbid private material in the certificate slot and require
it in the key slot. The current external Mesh selector also supports
public-key-spki/public. It does not currently
resolve bundles, chains, trust anchors, CRLs, signer references, protected
files, provider encodings, or stamping targets. See the
credential-provider guide
for those separate contracts.
SDK contract
Mesh providers are module processes. They use the same framed JSON-RPC
runtime, structured log rail, session broker, guardrails, and explicit
result records as other modules. Python modules override Mesh hook methods,
Go exposes typed optional Mesh interfaces, and Rust exposes Mesh hook
methods on the Module trait.
Mesh is optional-interface based, not all-or-nothing. A minimal provider may
implement only mesh.describe and
mesh.open_stream for a single stream-style Mesh. A deep implant
or stager provider may also expose topology, routes, beacons, triggers,
upload_execute, native load, command, and survey
tasks. Structured tasks, listenerTypes, topology,
triggers, and credential-delivery metadata describe their corresponding
contracts. The top-level capabilities string list is optional,
opaque provider vocabulary; Hovel does not infer hook support from those
strings. The implementation and structured descriptor must agree, and every
unsupported method must fail explicitly.
Credential consumption is optional in the same way. A provider that wants
assignment-bound material advertises a
credentialDelivery descriptor and the
runtime capability, then implements
credential.describe and credential.runtime. If the
same descriptor is published independently and inside
mesh.describe, the two canonical descriptors must match.
Listening-post support is optional too. Providers advertise supported kinds
through listenerTypes, and providers that own listener lifecycle
implement list, start, and stop independently of task and stream support. A
listener may be deployment-coupled to the provider
(embedded) or deployed separately, and its lifecycle may be
provider-managed or externally managed. Those labels describe ownership and
deployment, not an assumption that the module subprocess stays alive
between RPC calls.
| Method | Purpose | Python | Go | Rust |
|---|---|---|---|---|
mesh.describe | Report static Mesh capabilities, task specs, declared triggers, cheap topology, and optional credential-delivery metadata. | yes | yes | yes |
mesh.topology | Return current nodes, links, and optional routes. | yes | yes | yes |
mesh.beacons | Return recent beacon records for one node or the Mesh. | yes | yes | yes |
mesh.listeners | List provider-reported listening posts without returning their configuration or credentials. | yes | yes | yes |
mesh.listener.start | Idempotently start or attach a caller-named listening post, optionally after same-process runtime delivery. | yes | yes | yes |
mesh.listener.stop | Idempotently stop or detach a caller-named listening post. Stop is not credential-bearing. | yes | yes | yes |
mesh.task | Run a provider-owned node or route task, optionally after same-process runtime delivery. | yes | yes | yes |
mesh.open_stream | Open a routed session flow as a normal SessionRef, optionally after same-process runtime delivery. | yes | yes | yes |
Runtime contract
The daemon runtime treats Mesh as a first-class provider surface. Module
inspection asks for mesh.describe as optional discovery and
stores the descriptor in module catalog data. The run service and daemon RPC
expose describe, topology, beacon, listener, task, stream, and bridge calls
so front ends do not need to speak module JSON-RPC directly.
StartMeshListener, RunMeshTask,
OpenMeshStream, and OpenMeshBridge accept an optional
top-level credentials array. When that array is present, a
matching credentialContext is required with actor, operation,
and correlation IDs plus approveCredentialUse: true. The caller
chooses only assignment IDs, provider-advertised slots, and non-secret
projection/form pairs. It never receives the resolved bytes.
Hovel inspects the exact installed provider, reconciles its credential and
Mesh descriptors, resolves the active assignment generation, starts one
provider process, calls credential.runtime once per selection,
and invokes the consuming Mesh method in that same process. If a task or
stream returns a live session, the session broker adopts that process for
the session lifetime. A listener-start process is shut down after the call.
A long-lived listener must therefore live in durable provider-owned state or
another service, and the provider owns any protected handoff from its
short-lived control adapter to that runtime.
ListMeshListeners, StartMeshListener, and
StopMeshListener are stable HTTP/JSON daemon methods. A caller
can use a stable listenerId for idempotent provider lifecycle
behavior and correlate results with ListMeshOperations.
Listener IDs are scoped to one provider Mesh, so daemon-wide identity is the
pair of moduleId and listenerId. Start configuration
is write-only and Hovel never copies it into listener read models or Mesh
bookkeeping records. Providers must keep configuration and credential
material out of errors and logs.
Streams, protocols, and bridges
| Operation | Protocol contract | What Hovel preserves |
|---|---|---|
RunMeshTask | protocol is provider-defined task metadata. | The typed request and bookkeeping fields; no generic socket behavior is implied. |
OpenMeshStream | protocol is provider-defined and may name TCP, UDP, ICMP, GRE, raw IP, or another provider contract. | The returned provider-owned SessionRef. Hovel does not reinterpret an opaque protocol as TCP. |
OpenMeshBridge with localNetwork: tcp | The local adapter is a byte stream. The request's protocol remains provider-defined and is never overwritten. | Application byte order after the local capability preface is consumed. |
OpenMeshBridge with localNetwork: udp | The local adapter is a single authenticated UDP peer and requires a returned session with the datagram capability. The remote protocol is independent. | Exactly one datagram per non-empty session write or read, including datagrams received before the local peer authenticates. |
| Opaque or non-socket remote protocol | Providers may map it onto either supported local association when their session contract defines the semantics. | No generic raw, ICMP, TUN, or TAP local adapter is implied; otherwise use a provider-defined task or direct session contract. |
A caller can use OpenMeshBridge to receive a loopback
localHost/localPort pair, the selected
localNetwork, and an ephemeral 256-bit bearer
capability. The first TCP bytes must be the capability
followed by LF; for UDP, the first complete datagram is the capability.
Hovel consumes that preface and never forwards it to the provider. The Go,
Python, and Rust SDKs provide helpers that validate the endpoint and perform
this handshake before returning a normal connection.
A Mesh Bridge does not encrypt, terminate, originate, or validate TLS. It does not select certificates for the local client, inject trust anchors, provide SNI or ALPN, or turn UDP into DTLS or QUIC. The bearer capability authenticates one local connection or peer; it is not transport encryption. TLS must be implemented by the local application or by the Mesh provider. If a local TLS client speaks end-to-end through a TCP bridge, the bridge forwards those ciphertext bytes unchanged.
Loopback plus the capability is a local process boundary, not protection against a compromised operator account or daemon host. Keep the capability in memory, never log or persist it, and close the bridge when the consumer finishes. Open one bridge for each independent TCP connection or UDP peer association.
The daemon keeps bookkeeping for Mesh listener lifecycle calls, tasks, streams, and bridges. Records capture the provider module, run, task kind, node, route, destination, protocol, session, state, and timestamps. Bridge records also carry the daemon-owned local address; listener records carry the action, stable listener ID, and provider-reported state. Credential deliveries have a separate secret-free PKI execution ledger containing identities, digests, sizes, and terminal receipts, never resolved bytes or protected paths.
Live topology, beacon, listener, and Mesh operation records are sensitive
control-plane data. Their daemon RPC methods require the owner-controlled
Unix transport (or a future authenticated proxy with equivalent authority),
even though they are reads. The loopback TCP development endpoint is not a
local-user authentication mechanism and rejects these methods with
permission-denied.
Safe certificate rotation
Renewal and rotation create a new immutable certificate generation; they do not silently replace the generation currently assigned to a provider, listener, or node. Assignment changes use revision-fenced stage and activate transitions:
- Renew or rotate to create a new generation. Rotation creates a distinct key; renewal reuses and revalidates the source key.
- Stage the new generation on each assignment with its current expected revision. Peer-trust purposes also pin the then-active trust generation.
- Prepare and verify the consumer cutover. Staging alone is not delivery: the current Mesh selector resolves only the assignment's active generation, not its staged generation.
- Activate with the new expected revision only when the consumer is ready. Activation atomically promotes the staged certificate and pinned trust generation.
- Restart, reload, or reopen provider-owned resources as their actual lifecycle requires, then inspect Mesh and PKI operation records before retiring old material.
Hovel does not currently enforce a generic consumer acknowledgement during assignment activation, and it does not hot-reload a running listening post or existing stream. Providers must expose or document their own reload and overlap behavior. Follow the complete, recovery-aware sequence in TLS Operations.
Pivoted destination contract
A task or stream can address either a Mesh node itself or a destination
reachable from that node. Providers advertise this with
MeshTaskSpec.targetScopes: node,
route, and destination. Requests then combine
nodeId or route with
destinationHost, optional destinationPort, and a
provider-defined protocol.
This is the SDK contract for “throw through a tunnel” without making Mesh
providers implement exploit execution. Hovel can open a routed session flow
to a destination reachable from a node, bind a daemon-owned TCP or UDP
loopback bridge with OpenMeshBridge, and run a compatible tool
against the authenticated local bridge, or it can ask a provider for an
explicit upload_execute task when the provider owns that
delivery mechanism. The SDK supports that task contract, but the current
direct daemon task route dispatches only survey; execution-capable
kinds remain blocked until they are bound to the persisted planning,
confirmation, dangerous-module, and audit path above the SDK.
Implant loading follows the same rule. A provider that can place and start
an implant through its nodes should advertise an
upload_execute task for the common “copy bytes, then run them”
flow. Providers with a native in-memory or protocol-specific loader can
advertise load instead. The request carries the pivot, reached
destination, and either inline bytes via inputData plus
inputEncoding or provider-defined artifact references in
config. Mesh records the contract and bookkeeping; payload
generation, credential stamping, confirmation, and loader behavior remain
provider or higher-layer responsibilities.
Planning and safety rules
handshake,schema,mesh.describe, andcredential.describemust be cheap, deterministic, and side-effect free.mesh.topologyandmesh.beaconsmay inspect provider-local state, but must not mutate target-side state.mesh.listenersis read-only; listener start and stop are explicit, idempotent lifecycle mutations recorded by the daemon.- Credential selection requires explicit approval, exact assignment-to-consumer binding, an active or degraded assignment, an active certificate generation, and a slot compatible with its purpose, profile, compatibility target, projection, form, and private-material policy.
- Target interaction belongs in
mesh.taskormesh.open_streamafter the operator has a persisted plan and recorded confirmation. - Provider-authored triggers and beacons are evidence and routing context; they never bypass Hovel guardrails.
- Mesh streams become ordinary sessions, so existing session I/O, commands, transcript capture, and cleanup paths still apply.
Example topology
{
"root": "controller",
"nodes": [
{ "id": "controller", "kind": "controller", "state": "online" },
{ "id": "relay-1", "parentId": "controller", "kind": "relay", "state": "online" },
{ "id": "leaf-1", "parentId": "relay-1", "kind": "agent", "state": "online" }
],
"links": [
{ "id": "link-controller-relay-1", "source": "controller", "target": "relay-1", "state": "up" },
{ "id": "link-relay-1-leaf-1", "source": "relay-1", "target": "leaf-1", "state": "up" }
],
"routes": [
{ "id": "route-leaf-1", "nodes": ["controller", "relay-1", "leaf-1"] }
]
}