Developing Mesh Providers
This guide is for developers implementing a Mesh provider, adding optional listening-post control, or routing another Hovel module through a Mesh. The SDK defines contracts and dispatch; your provider owns its node protocol, routing, task execution, listener runtime, and durable state.
For the platform model, start with Mesh. For certificate custody and assignment concepts, read TLS and Workspace PKI; for issue, stage, activate, rotate, and recovery procedures, use TLS Operations. This page covers only the Mesh integration seam. The complete runtime, protected-file, encoding, and stamping implementation guide is Developing Credential Providers, while Daemon RPC and the OpenAPI document are the external API references.
Start with the boundary
| Component | Owns | Does not own |
|---|---|---|
| Mesh provider module | Topology, routes, provider protocol, tasks, streams, beacons, triggers, optional listener control, and any TLS handshakes it explicitly implements. | Operator planning, confirmation, dangerous-module policy, workspace PKI custody, or daemon audit storage. |
| Mesh Listener | A provider-reported listening post or rendezvous resource. It may be embedded in the provider deployment or run separately. | The provider adapter process. A listener must not rely on one short-lived RPC subprocess remaining alive. |
| Workspace PKI | Authorities, immutable certificate generations, trust generations, assignments, approvals, and audited resolution. | Starting TLS, choosing provider protocols, or hot-reloading a provider-owned listener. |
| Mesh Bridge | A daemon-owned, bearer-authenticated loopback TCP or UDP endpoint backed by one Mesh session flow. | TLS encryption or termination, remote peer authentication, trust injection, or the provider's route. |
| Consumer module | Its normal survey, exploit, or payload behavior and any end-to-end TLS it implements. | Mesh internals when it uses an SDK-authenticated local bridge connection. |
Choose only the surfaces you need
| Provider shape | Implement | Typical use |
|---|---|---|
| Single routed flow | mesh.describe, mesh.open_stream | Expose one provider-owned path as a normal Hovel session. |
| Observable node tree | Add mesh.topology and mesh.beacons | Show relays, implants, links, routes, and liveness. |
| Tasking provider | Add mesh.task | Survey, command, upload, execute, upload-and-execute, or provider-native load. |
| Listening-post provider | Add mesh.listeners and, when controlled by Hovel, start/stop hooks | Report and manage rendezvous resources independently of the provider adapter. |
| Deep implant/stager Mesh | Combine the optional surfaces you actually support | Tree routing, tasking, triggers, beacons, listeners, streams, and native loaders. |
Do not implement no-op methods to satisfy a large interface. In Go, implement
MeshDescriber and only the optional provider interfaces you need.
In Python and Rust, override only the Mesh hooks you support; defaults fail
explicitly. Describe task kinds, listener types, topology, triggers, and
credential slots in their structured fields. Treat the optional top-level
capabilities list as provider-defined vocabulary, not as a switch
that makes an unimplemented hook available.
Describe capabilities without side effects
mesh.describe is catalog metadata. It must be cheap, deterministic,
offline, and side-effect free. Put dynamic node state in topology, beacon, or
listener calls instead. A provider supporting routed streams, destination
tasking, and an HTTPS listening post might return:
capabilities only for additional
semantics understood by your provider and its callers.
{
"name": "lab relay mesh",
"version": "v0.1.0",
"capabilities": ["mesh.route", "mesh.destination", "mesh.listener"],
"tasks": [
{
"kind": "survey",
"readOnly": true,
"targetScopes": ["node", "destination"]
},
{
"kind": "upload_execute",
"destructive": true,
"targetScopes": ["destination"],
"configSchema": {
"type": "object",
"properties": { "remotePath": { "type": "string" } }
}
}
],
"listenerTypes": [
{
"kind": "https-beacon",
"deployments": ["embedded", "separate"],
"managementModes": ["provider", "external"],
"protocols": ["https"],
"configSchema": {
"type": "object",
"properties": {
"bind": { "type": "string" }
}
}
}
],
"credentialDelivery": {
"schemaVersion": "hovel.pki.credential-delivery/v1",
"deliveryCapabilities": ["runtime"],
"credentialSlots": [
{
"name": "listener-certificate",
"purpose": "tls-server",
"endpointRole": "server",
"consumerType": "mesh-listener",
"acceptedBundleVersions": ["hovel.pki.bundle/v1"],
"acceptedProfiles": ["tls-server"],
"acceptedCompatibilityTargets": ["portable-x509"],
"acceptedProjections": ["certificate-der"],
"acceptedMaterialForms": ["public"],
"maximumEncodedBytes": 65536,
"remainderPolicy": "preserve",
"privateMaterial": "forbidden"
},
{
"name": "listener-private-key",
"purpose": "tls-server",
"endpointRole": "server",
"consumerType": "mesh-listener",
"acceptedBundleVersions": ["hovel.pki.bundle/v1"],
"acceptedProfiles": ["tls-server"],
"acceptedCompatibilityTargets": ["portable-x509"],
"acceptedProjections": ["private-key-pkcs8"],
"acceptedMaterialForms": ["private-bytes"],
"maximumEncodedBytes": 65536,
"remainderPolicy": "preserve",
"privateMaterial": "required"
},
{
"name": "node-client-certificate",
"purpose": "tls-client",
"endpointRole": "client",
"consumerType": "mesh-node",
"acceptedBundleVersions": ["hovel.pki.bundle/v1"],
"acceptedProfiles": ["tls-client"],
"acceptedCompatibilityTargets": ["portable-x509"],
"acceptedProjections": ["certificate-der"],
"acceptedMaterialForms": ["public"],
"maximumEncodedBytes": 65536,
"remainderPolicy": "preserve",
"privateMaterial": "forbidden"
},
{
"name": "node-client-private-key",
"purpose": "tls-client",
"endpointRole": "client",
"consumerType": "mesh-node",
"acceptedBundleVersions": ["hovel.pki.bundle/v1"],
"acceptedProfiles": ["tls-client"],
"acceptedCompatibilityTargets": ["portable-x509"],
"acceptedProjections": ["private-key-pkcs8"],
"acceptedMaterialForms": ["private-bytes"],
"maximumEncodedBytes": 65536,
"remainderPolicy": "preserve",
"privateMaterial": "required"
}
]
}
}
This fictional provider advertises exactly four runtime slots. Certificate
and private-key projections are deliberately separate, and listener/server
metadata is not interchangeable with node/client metadata. Publishing a
slot does not implement TLS: the provider must still consume the matching
values in its TLS stack. If the provider also implements
credential.describe, its standalone descriptor must be identical
to the nested credentialDelivery value.
Report topology, beacons, and triggers
Nodes are operator-addressable Mesh participants. Destinations are ordinary
hosts or services reachable through a node or route. Do not invent a node for
every destination. Use listenerId on nodes, beacons, and triggers
when provenance matters.
{
"root": "controller",
"nodes": [
{ "id": "controller", "kind": "controller", "state": "online" },
{
"id": "relay-west",
"parentId": "controller",
"listenerId": "listener-west",
"kind": "relay",
"state": "online"
}
],
"links": [
{ "id": "controller-west", "source": "controller", "target": "relay-west", "state": "up" }
],
"routes": [
{ "id": "route-west", "nodes": ["controller", "relay-west"] }
]
}
Triggers are declarations in the descriptor; beacons are time-stamped runtime observations. Neither starts work by itself or bypasses Hovel's planning and confirmation path.
Implement tasking
Validate the task kind and required target scope before contacting the target.
Echo correlation and routing fields in the result when they are known. For a
copy-then-run implant flow, use upload_execute; reserve
load for a provider-native in-memory or protocol-specific loader.
{
"runId": "run-lab-1",
"taskId": "task-deploy-1",
"kind": "upload_execute",
"nodeId": "relay-west",
"listenerId": "listener-west",
"destinationHost": "192.0.2.25",
"destinationPort": 443,
"protocol": "tcp",
"config": { "remotePath": "provider-defined-path" },
"inputData": "base64-encoded-artifact",
"inputEncoding": "base64"
}
Provider contract versus current daemon dispatch: implement and advertise every task kind your provider truly supports, including command, upload, execute,
upload_execute, load, and provider-defined kinds. The current directRunMeshTaskdaemon route dispatches onlysurvey. It rejects every other kind before invoking the provider because that route is not yet bound to a persisted throw plan, recorded confirmation, and dangerous-module allowance. Do not weaken the SDK descriptor to match this temporary orchestration limit, and do not add a provider-side bypass.
Provider-native command, survey, and delivery tasks belong here. If an existing module can already operate through a TCP or UDP endpoint, prefer the bridge workflow below instead of duplicating that module inside the provider.
Implement routed streams
mesh.open_stream returns a normal SessionRef. The
provider implements session read, write, and close against its own route. The
daemon can then expose the session through ordinary session APIs or a local
Mesh Bridge.
- For TCP-like flows, preserve byte-stream semantics.
- For UDP-like flows, advertise
datagramand preserve exactly one datagram per non-empty session read or write. - For ICMP, GRE, raw IP, or another non-socket protocol, keep it provider-defined until a concrete consumer requires a generic packet adapter.
- Return an error for unsupported protocols; do not silently reinterpret them as TCP.
Add listening-post lifecycle only when needed
Listener listing is read-only. Start and stop are idempotent mutations using a
caller-selected stable ID. IDs are unique only inside one provider Mesh;
daemon-wide identity is (moduleId, listenerId).
POST /hovel.daemon.v1.DaemonService/StartMeshListener
Content-Type: application/json
{
"moduleId": "lab-relay-mesh@v0.1.0",
"request": {
"listenerId": "listener-west",
"name": "west rendezvous",
"kind": "https-beacon",
"deployment": "separate",
"management": "provider",
"config": {
"bind": "127.0.0.1:8443"
}
},
"credentials": [
{
"requestId": "credential-start-west-certificate-01",
"assignmentId": "assignment-listener-west",
"slotName": "listener-certificate",
"capability": "runtime",
"material": { "projection": "certificate-der", "form": "public" }
},
{
"requestId": "credential-start-west-private-key-01",
"assignmentId": "assignment-listener-west",
"slotName": "listener-private-key",
"capability": "runtime",
"material": { "projection": "private-key-pkcs8", "form": "private-bytes" }
}
],
"credentialContext": {
"actorId": "operator-alice",
"operationId": "credential-operation-start-listener-west-01",
"correlationId": "mesh-listener-start-west-01",
"approveCredentialUse": true
}
}
{
"operationId": "mesh-op-42",
"listener": {
"id": "listener-west",
"kind": "https-beacon",
"state": "active",
"deployment": "separate",
"management": "provider",
"addresses": ["https://127.0.0.1:8443"]
}
}
The result deliberately has no config. Treat start configuration
as write-only, keep it out of listener read models, errors, and logs, and never
store credentials directly in daemon bookkeeping. The top-level selections are
non-secret; Hovel resolves the assignment's active generation only after explicit
approval. A provider-managed listener must survive the short-lived module RPC
invocation; use a separate service, supervisor, or another durable
provider-owned runtime, and make any handoff from the adapter process to that
runtime explicit and protected.
Wire assignment-bound TLS into consuming calls
The daemon API does not accept certificate bytes. A front end sends one or
more non-secret assignment/slot selections beside the Mesh request. Hovel
derives the consumers permitted by that operation, inspects the exact
installed provider and descriptor digest, authorizes use, resolves the active
assignment generation, and sends each resolved projection through
credential.runtime immediately before the Mesh method.
| Resource | Assignment consumer | Canonical consumer ID | When the daemon derives it |
|---|---|---|---|
| Provider control plane | mesh-provider | lab-relay-mesh | Every credential-aware listener-start, task, stream, or bridge-open call. |
| Listener / listening post | mesh-listener | lab-relay-mesh/listener-west | The request explicitly carries listenerId: listener-west. |
| Node | mesh-node | lab-relay-mesh/relay-west | The request explicitly carries nodeId: relay-west. |
| Standalone listening post | listening-post | Operator-selected stable ID. | Not derived by the current Mesh resolver; this is a broader assignment extension point. |
| Implant or stager | implant or stager | Operator-selected stable ID. | Not derived by current Mesh calls. Use mesh-node only when that resource is actually exposed as the selected Mesh Node. |
These examples have no slash inside either component. If a provider,
listener, or node component contains a slash, Hovel uses the bounded
mesh-scoped:<sha256> form: lowercase SHA-256 over the
canonical UTF-8 provider name, one NUL byte, and the canonical UTF-8
listener or node ID. Retain the typed constructor's result rather than
reconstructing the assignment consumer ID in a client.
Use the canonical provider name in assignment consumer IDs, without an
@version suffix. The daemon still binds delivery to the exact
installed module, such as lab-relay-mesh@v0.1.0, its version,
provider ID, and descriptor SHA-256. A route does not implicitly authorize
every node it contains; include the intended nodeId explicitly.
- inspect exact installed module
- start one provider process
- reconcile
credential.describeandmesh.describe credential.runtimefor certificate slotcredential.runtimefor private-key slotmesh.listener.start,mesh.task, ormesh.open_stream- clear daemon copies and persist secret-free receipts
In provider code, keep pending material keyed by the correlated operation and
slot, require the complete pair before constructing a TLS configuration, and
clear temporary copies after the consuming hook takes ownership. Return only
a CredentialDeliveryReceipt with the matching request ID and
optional non-secret reference or digest. Never put certificate-key bytes,
protected paths, or bearer references in logs, errors, listener results, task
outputs, or session metadata.
The current external Mesh selector supports only
runtimedelivery ofcertificate-der/public,public-key-spki/public, andprivate-key-pkcs8/private-bytes. It does not deliver a bundle, chain, trust set, CRL, signer reference, protected file, provider encoding, or stamp target. A TLS-client or mTLS assignment can pin trust lifecycle state, but its trust bytes are not projected through this API today; the provider must obtain peer trust through an explicit provider-owned deployment path until that extension is implemented.
Credential-bearing RunMeshTask
POST /hovel.daemon.v1.DaemonService/RunMeshTask
Content-Type: application/json
{
"moduleId": "lab-relay-mesh@v0.1.0",
"request": {
"runId": "run-lab-1",
"taskId": "task-survey-west-1",
"kind": "survey",
"nodeId": "relay-west"
},
"credentials": [
{
"requestId": "credential-task-west-certificate-01",
"assignmentId": "assignment-node-relay-west",
"slotName": "node-client-certificate",
"capability": "runtime",
"material": { "projection": "certificate-der", "form": "public" }
},
{
"requestId": "credential-task-west-private-key-01",
"assignmentId": "assignment-node-relay-west",
"slotName": "node-client-private-key",
"capability": "runtime",
"material": { "projection": "private-key-pkcs8", "form": "private-bytes" }
}
],
"credentialContext": {
"actorId": "operator-alice",
"operationId": "credential-operation-task-west-01",
"correlationId": "mesh-task-west-01",
"approveCredentialUse": true
}
}
Credential-bearing OpenMeshStream
POST /hovel.daemon.v1.DaemonService/OpenMeshStream
Content-Type: application/json
{
"moduleId": "lab-relay-mesh@v0.1.0",
"request": {
"runId": "run-lab-1",
"nodeId": "relay-west",
"destinationHost": "192.0.2.25",
"destinationPort": 443,
"protocol": "tcp"
},
"credentials": [
{
"requestId": "credential-stream-west-certificate-01",
"assignmentId": "assignment-node-relay-west",
"slotName": "node-client-certificate",
"capability": "runtime",
"material": { "projection": "certificate-der", "form": "public" }
},
{
"requestId": "credential-stream-west-private-key-01",
"assignmentId": "assignment-node-relay-west",
"slotName": "node-client-private-key",
"capability": "runtime",
"material": { "projection": "private-key-pkcs8", "form": "private-bytes" }
}
],
"credentialContext": {
"actorId": "operator-alice",
"operationId": "credential-operation-stream-west-01",
"correlationId": "mesh-stream-west-01",
"approveCredentialUse": true
}
}
protocol: tcp describes the provider request; it does not force
the provider to perform TLS. The provider must deliberately use the selected
certificate and key in its TLS client stack. A direct stream may use another
provider-defined protocol, including a non-socket protocol, when the provider
advertises and implements it.
Credential-bearing OpenMeshBridge
POST /hovel.daemon.v1.DaemonService/OpenMeshBridge
Content-Type: application/json
{
"moduleId": "lab-relay-mesh@v0.1.0",
"localNetwork": "tcp",
"request": {
"runId": "run-lab-1",
"nodeId": "relay-west",
"destinationHost": "192.0.2.25",
"destinationPort": 443,
"protocol": "tcp"
},
"localHost": "127.0.0.1",
"localPort": 0,
"credentials": [
{
"requestId": "credential-bridge-west-certificate-01",
"assignmentId": "assignment-node-relay-west",
"slotName": "node-client-certificate",
"capability": "runtime",
"material": { "projection": "certificate-der", "form": "public" }
},
{
"requestId": "credential-bridge-west-private-key-01",
"assignmentId": "assignment-node-relay-west",
"slotName": "node-client-private-key",
"capability": "runtime",
"material": { "projection": "private-key-pkcs8", "form": "private-bytes" }
}
],
"credentialContext": {
"actorId": "operator-alice",
"operationId": "credential-operation-bridge-west-01",
"correlationId": "mesh-bridge-west-01",
"approveCredentialUse": true
}
}
This calls the credential-aware stream opener before creating the live bridge. The certificate and key go to the provider process, not to the daemon's local socket adapter and not to the eventual local client. Every request ID in one array must be unique, and an assignment/slot pair may appear only once. These IDs claim credential delivery at most once; they do not make bridge creation replayable. A terminal delivery cannot be reused in a new provider process, so a deliberate replacement bridge must use fresh request IDs and a new credential operation ID.
Rotate without silently changing a live consumer
- Renew or rotate to create a new immutable certificate generation. Rotation creates a new key; renewal reuses and revalidates the source key.
- Stage that generation on the assignment with its current
expectedRevision. The old active generation continues to resolve for Mesh calls. - Prepare or test the consumer cutover through its explicit deployment path. The current Mesh selector cannot request the staged generation.
- Activate with the assignment's new revision only when cutover is ready. The next credential-bearing Mesh invocation resolves the newly active generation.
- Reload, restart, or reopen the provider-owned listener or session as required, verify PKI and Mesh operation records, then retire old material.
Assignment activation does not currently wait for a generic consumer acknowledgement and does not hot-reload an existing listener or stream. Document provider-specific overlap and rollback behavior, and use the operator runbook for revision fences, trust changes, revocation, and recovery.
Using a Mesh with another module
The clean integration is authenticated endpoint substitution: the front end asks the daemon for a bridge, then a small SDK-aware boundary authenticates the returned loopback endpoint before handing a normal connection to consumer protocol code. The consumer does not need to understand Mesh topology or the provider's route.
- Inspect the Mesh and choose a node or route.
- Call
OpenMeshBridgewith the remote destination and provider-definedprotocol, plus an independentlocalNetworkoftcporudp. - Keep the returned
capabilityin memory and use an SDK bridge helper to authenticate the local TCP connection or UDP peer. - Give the resulting connection to the consumer protocol code, then run the module through the normal persisted-plan and confirmation path.
- Close the bridge and inspect
ListMeshOperations.
POST /hovel.daemon.v1.DaemonService/OpenMeshBridge
Content-Type: application/json
{
"moduleId": "lab-relay-mesh@v0.1.0",
"localNetwork": "tcp",
"request": {
"runId": "run-lab-1",
"nodeId": "relay-west",
"listenerId": "listener-west",
"destinationHost": "192.0.2.25",
"destinationPort": 443,
"protocol": "tcp"
}
}
{
"operationId": "mesh-op-43",
"sessionId": "run-lab-1-session-1",
"localHost": "127.0.0.1",
"localPort": 43117,
"localNetwork": "tcp",
"capability": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"localAddress": "127.0.0.1:43117"
}
The shown capability is an example value. A real response carries a fresh,
sensitive 256-bit bearer capability. Send the capability plus LF as the first
TCP bytes, or as the first complete UDP datagram; Hovel consumes the preface
instead of forwarding it. Go provides DialMeshBridge, Python
provides connect_mesh_bridge, and Rust provides the generic
connect_mesh_bridge helper plus
connect_mesh_bridge_tcp and connect_mesh_bridge_udp
for callers that handle only one local adapter. Keep the capability out of
logs, results, caches, and durable configuration.
request.protocol is delivered to the provider unchanged and may
name TCP, UDP, ICMP, GRE, raw IP, or a custom flow. It does not select the
daemon socket. localNetwork selects only the loopback TCP stream
or UDP datagram adapter and is returned in the response and Mesh operation
bookkeeping.
ListMeshOperations, so it cannot be recovered or guessed. The
original bridge may nevertheless be active. List active
bridge operations using the known module and run filters, match
the route, destination, session, and local endpoint, and close the orphan with
CloseMeshBridge by operation or session ID. Only after the old
bridge is closed or confirmed absent should an operator-approved replacement
call be made. When credentials are selected, that new call must use fresh
credential request IDs plus a new credential operation and correlation ID; it
returns a new capability.
CloseMeshBridge closes the local TCP or UDP endpoint first, then
asks the provider to close its session. If provider cleanup times out or
fails, the operation is recorded as failed but the daemon retains the bridge
selector as a cleanup handle. Retry the same operation ID or session ID; do
not open a replacement merely because the first close returned an error. A
successful retry removes the handle and transitions the bookkeeping record
to closed. The closed local endpoint never reopens during a retry.
The daemon bridge moves bytes or datagrams; the Mesh provider owns the remote route; the consumer protocol remains ordinary after the helper authenticates. Open one bridge per independent TCP connection or UDP peer association. A raw third-party tool that cannot send the capability preface needs a small local adapter rather than being pointed directly at the returned port.
A bridge does not add TLS, terminate TLS, validate certificates, inject trust, or turn UDP into DTLS or QUIC. Its bearer preface authenticates the local peer but does not encrypt traffic. End-to-end TLS bytes from a local client pass through unchanged; provider-terminated TLS is the provider's responsibility.
Use RunMeshTask instead when the provider itself owns the operation
(for example, provider-native survey, command, upload-and-execute, or load).
Today only survey is directly dispatchable through the daemon RPC;
execution-capable kinds require the planned-task orchestration described above.
Use a bridge when reusing a protocol client or module that already knows how
to talk to a host and port. These approaches can coexist in one provider.
SDK method map
| Wire method | Go | Python | Rust |
|---|---|---|---|
mesh.describe | MeshDescriber.DescribeMesh | describe_mesh | Module::describe_mesh |
mesh.topology | MeshTopologyProvider.MeshTopology | mesh_topology | Module::mesh_topology |
mesh.beacons | MeshBeaconProvider.ListMeshBeacons | list_mesh_beacons | Module::list_mesh_beacons |
mesh.listeners | MeshListenerProvider.ListMeshListeners | list_mesh_listeners | Module::list_mesh_listeners |
mesh.listener.start/stop | MeshListenerLifecycleProvider | start_mesh_listener/stop_mesh_listener | Module::start_mesh_listener/stop_mesh_listener |
mesh.task | MeshTaskProvider.RunMeshTask | run_mesh_task | Module::run_mesh_task |
mesh.open_stream | MeshStreamProvider.OpenMeshStream | open_mesh_stream | Module::open_mesh_stream |
credential.describe | CredentialDescriber.DescribeCredentialDelivery | describe_credential_delivery | Module::describe_credential_delivery |
credential.runtime | CredentialRuntimeProvider.LoadRuntimeCredential | load_runtime_credential | Module::load_runtime_credential |
See the repository SDK guides for language-specific starter code: Go, Python, and Rust. The wire protocol contains request and response examples for every Mesh method. Use the credential-provider guide for complete provider implementations; the two credential rows above are the only ones required by the current external Mesh selector path.
SDK starters
These excerpts show the optional-interface pattern. They intentionally do not open a real listener, contact a target, or advertise credential delivery. Replace the returned sample state with calls into your durable provider runtime, implement only the methods you advertise, and add the separate credential interfaces or hooks before using the credential-bearing requests above.
Go: compose optional interfaces
type LabMesh struct{}
func (LabMesh) DescribeMesh(hovel.MeshDescribeRequest) (hovel.MeshDescriptor, error) {
return hovel.MeshDescriptor{
Name: "lab relay mesh",
Tasks: []hovel.MeshTaskSpec{{
Kind: hovel.MeshTaskSurvey,
ReadOnly: true,
TargetScopes: []hovel.MeshTargetScope{hovel.MeshTargetNode},
}},
ListenerTypes: []hovel.MeshListenerSpec{{
Kind: "https-beacon",
Deployments: []hovel.MeshListenerDeployment{hovel.MeshListenerDeploymentSeparate},
ManagementModes: []hovel.MeshListenerManagement{hovel.MeshListenerManagementProvider},
Protocols: []string{"https"},
}},
}, nil
}
func (LabMesh) ListMeshListeners(req hovel.MeshListenerListRequest) ([]hovel.MeshListener, error) {
// Query provider-owned durable state here. An empty list must encode as [].
return []hovel.MeshListener{}, nil
}
func (LabMesh) StartMeshListener(req hovel.MeshListenerStartRequest) (hovel.MeshListener, error) {
// Idempotently ensure req.ListenerID exists; never return req.Config.
return hovel.MeshListener{
ID: req.ListenerID, Kind: req.Kind,
State: hovel.MeshListenerStateActive,
Deployment: req.Deployment, Management: req.Management,
}, nil
}
func (LabMesh) StopMeshListener(req hovel.MeshListenerStopRequest) (hovel.MeshListener, error) {
return hovel.MeshListener{ID: req.ListenerID, State: hovel.MeshListenerStateStopped}, nil
}
Add these methods to the normal hovel.Module type shown in the
Go module guide. Add
RunMeshTask, MeshTopology,
ListMeshBeacons, or OpenMeshStream only when the
provider supports those surfaces.
Python: override the hooks you support
class LabMesh(HovelModule):
name = "lab-relay-mesh"
version = "v0.1.0"
module_type = "exploit"
def describe_mesh(self, _request: MeshDescribeRequest) -> MeshDescriptor:
return MeshDescriptor(
name="lab relay mesh",
tasks=[MeshTaskSpec(
kind=MESH_TASK_SURVEY,
read_only=True,
target_scopes=[MESH_TARGET_NODE],
)],
listener_types=[MeshListenerSpec(
kind="https-beacon",
deployments=[MESH_LISTENER_DEPLOYMENT_SEPARATE],
management_modes=[MESH_LISTENER_MANAGEMENT_PROVIDER],
protocols=["https"],
)],
)
def list_mesh_listeners(self, _request: MeshListenerListRequest) -> list[MeshListener]:
return []
def start_mesh_listener(self, request: MeshListenerStartRequest) -> MeshListener:
# Idempotently ensure request.listener_id exists in durable provider state.
return MeshListener(
id=request.listener_id,
kind=request.kind,
state=MESH_LISTENER_STATE_ACTIVE,
deployment=request.deployment,
management=request.management,
)
def stop_mesh_listener(self, request: MeshListenerStopRequest) -> MeshListener:
return MeshListener(id=request.listener_id, state=MESH_LISTENER_STATE_STOPPED)
Import the model and constant names from hovel_sdk, add the normal
run hook shown in the Python module
guide, and test through ModuleRPC so framing and dispatch are
covered—not only direct method calls.
Rust: add optional methods to Module
fn describe_mesh(&self, _req: MeshDescribeRequest) -> Result<MeshDescriptor, String> {
Ok(MeshDescriptor {
name: "lab relay mesh".into(),
tasks: vec![MeshTaskSpec {
kind: MESH_TASK_SURVEY.into(),
read_only: true,
target_scopes: vec![MESH_TARGET_NODE.into()],
..MeshTaskSpec::default()
}],
listener_types: vec![MeshListenerSpec {
kind: "https-beacon".into(),
deployments: vec![MESH_LISTENER_DEPLOYMENT_SEPARATE.into()],
management_modes: vec![MESH_LISTENER_MANAGEMENT_PROVIDER.into()],
protocols: vec!["https".into()],
..MeshListenerSpec::default()
}],
..MeshDescriptor::default()
})
}
fn start_mesh_listener(&self, req: MeshListenerStartRequest) -> Result<MeshListener, String> {
Ok(MeshListener {
id: req.listener_id,
kind: req.kind,
state: MESH_LISTENER_STATE_ACTIVE.into(),
deployment: req.deployment,
management: req.management,
..MeshListener::default()
})
}
Place these methods in the normal Module implementation shown in
the Rust module guide. The default trait methods
provide explicit unsupported-method errors for surfaces you omit.
Protocol-level contract test
def test_listener_lifecycle_contract():
with ModuleRPC(LabMesh()) as rpc:
started = rpc.call("mesh.listener.start", {
"listenerId": "listener-west",
"kind": "https-beacon",
"deployment": "separate",
"management": "provider",
"config": {"bind": "127.0.0.1:8443"},
})
assert started["id"] == "listener-west"
assert started["state"] == "active"
assert "config" not in started
Testing checklist
- Descriptor calls are deterministic and perform no target or listener I/O.
- Every advertised task kind and target scope has a success and rejection test.
- Daemon integration tests prove that direct
surveydispatch works and every execution-capable task kind is rejected before its provider hook until persisted-plan orchestration is present. - Unsupported optional methods fail explicitly.
- Topology IDs are stable; routes reference existing nodes and links.
- Listener start/stop reject blank IDs before provider side effects and return the exact requested ID.
- Repeated listener start/stop calls are safe and converge on the same state.
- Listener configuration and credentials never appear in results, errors, logs, or daemon operation records.
- Credential descriptors use separate certificate and private-key slots with exact purpose, endpoint role, consumer type, profile, compatibility, projection, form, size, and private-material policies.
- Credential tests send
credential.runtimecalls and the consuming Mesh method through one framed RPC process, reject wrong consumer IDs and descriptor drift, and clear provider-owned temporary material. - Assignment tests prove that staging leaves the old active generation in use and activation changes the next resolved invocation without claiming to hot-reload a live resource.
- Stream tests cover read, write, close, cancellation, and datagram boundaries when applicable.
- Bridge tests bind only loopback, prove a stalling unauthenticated TCP client cannot block a valid client, reject missing or wrong capabilities, preserve TCP bytes or UDP datagram boundaries after authentication, and close sessions on failure.
- A failed provider-session close retains the operation/session selector; retry succeeds without reopening the local endpoint and moves bookkeeping from failed to closed.
- Bridge documentation and tests never imply that the local adapter adds TLS, trust validation, DTLS, or QUIC.
- Task, stream, listener, and bridge calls appear in
ListMeshOperationswith provider and listener provenance.
task fmt
task check
task ci
Repository development always uses Task. For out-of-tree providers, mirror the SDK's framed-RPC tests in your own build and run them against malformed input, provider failures, duplicate IDs, and cleanup paths—not only the happy path.