Module Wire Protocol
This is the contract between the Hovel daemon and a module process. The SDKs
implement it for you; you only need this page if you are writing an SDK, debugging
a module by hand, or porting Hovel modules to a new language. The runtime is
identified as jsonrpc-stdio.
Framing
Messages are JSON-RPC 2.0
objects, each prefixed with an HTTP-style Content-Length header. This
framing (rather than newline-delimited JSON) lets session payloads carry arbitrary
bytes without escaping games.
Content-Length: 58\r\n
\r\n
{"jsonrpc":"2.0","id":1,"method":"handshake","params":{}}
- Header lines end with
\r\n; a bare\r\nterminates the header block. - Header names are case-insensitive; only
Content-Lengthis required. - The body is exactly
Content-Lengthbytes of UTF-8 JSON.
Stdout is exclusively the framed protocol stream. Any unframed bytes on
stdout — banners, debug prints, child-process output, progress bars — are a
protocol error. Module authors should send operator-visible progress through
module/log, session output through module/session plus
session/read, and large captured output through artifacts. Stderr
is captured as process diagnostics and may be attached to module startup,
schema, execution, or shutdown failures.
Message kinds
Three shapes travel over the wire:
- Request (daemon → module): has an integer
idand amethod. Expects a response. - Response (module → daemon): echoes the
idwith eitherresultorerror. - Notification (module → daemon): has a
methodbut noid. No response. Used for logs and session events.
{"jsonrpc":"2.0","id":1,"result":{ ... }}
{"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"boom"}}
The daemon reads only error.message; the code is advisory.
SDKs should return JSON-RPC -32600 for structurally invalid
requests, -32601 or -32000 for unknown methods, and
-32000 for module/runtime failures. Hovel treats any error response
as a failed RPC call and records the message in the relevant catalog, plan,
execute, session, provider, or cleanup path.
Methods (daemon → module)
This table is the complete module RPC method set. SDKs may implement only the subset their language surface supports, but unknown or unsupported methods must fail with a JSON-RPC error rather than silently succeeding.
| Method | Params | Result | Implemented by |
|---|---|---|---|
handshake | {} | ModuleInfo | Python, Go, Rust |
schema | {} | ModuleSchema | Python, Go, Rust |
execute | ExecuteRequest | ModuleResult | Python, Go, Rust |
session/write | SessionWriteRequest | { "status": "ok" } | Python, Go, Rust |
session/read | SessionReadRequest | SessionReadResult | Python, Go, Rust |
session/close | SessionCloseRequest | { "status": "ok" } | Python, Go, Rust |
shutdown | {} | { "status": "ok" } | Python, Go, Rust |
step.describe | {} | StepContractSet | Python, Go |
step.prepare | StepPrepareRequest | StepPrepareResult | Python, Go |
step.execute | StepExecuteRequest | StepExecuteResult | Python, Go |
step.cleanup | StepCleanupRequest | StepCleanupResult | Python, Go |
credential.describe | {} | CredentialDeliveryDescriptor | Python, Go, Rust |
credential.runtime | CredentialRuntimeRequest | CredentialDeliveryReceipt | Python, Go, Rust |
credential.files | CredentialFilesRequest | CredentialDeliveryReceipt | Python, Go, Rust |
credential.encode | CredentialEncodingRequest | CredentialEncodingResult | Python, Go, Rust |
credential.stamp | CredentialStampExecutionRequest | CredentialStampExecutionResult | Python, Go, Rust |
list_payloads | PayloadQuery | PayloadInfo[] | Go |
resolve_payload | PayloadQuery | PayloadInfo | Go |
prepare_listener | PrepareListenerRequest | ListenerRef | Go |
generate_payload | GeneratePayloadRequest | PayloadArtifactSet | Go |
connect_session | ConnectSessionRequest | SessionRef | Go |
cleanup_payload | CleanupPayloadRequest | CleanupResult | Go |
read_payload_chunk | ReadPayloadChunkRequest | PayloadChunk | Go |
mesh.describe | MeshDescribeRequest | MeshDescriptor | Python, Go, Rust |
mesh.topology | MeshTopologyRequest | MeshTopology | Python, Go, Rust |
mesh.beacons | MeshBeaconRequest | { "beacons": MeshBeacon[] } | Python, Go, Rust |
mesh.listeners | MeshListenerListRequest | { "listeners": MeshListener[] } | Python, Go, Rust |
mesh.listener.start | MeshListenerStartRequest | MeshListener | Python, Go, Rust |
mesh.listener.stop | MeshListenerStopRequest | MeshListener | Python, Go, Rust |
mesh.task | MeshTaskRequest | MeshTaskResult | Python, Go, Rust |
mesh.open_stream | MeshStreamRequest | SessionRef | Python, Go, Rust |
handshake
Reports module identity. Called while building the catalog and again before each throw.
{
"name": "mock-survey",
"version": "v0.0.0-example",
"moduleType": "survey", // survey | exploit | payload_provider
"summary": "Collect example target facts.",
"description": "Example survey module ...",
"tags": ["example", "survey", "python"]
}
schema
Reports the configuration the module requires, split into chain-level and target-level.
{
"chainConfig": [ Requirement, ... ],
"targetConfig": [ Requirement, ... ],
"outputs": {}
}
credential.describe
Reports the optional credential slots and delivery, encoding, or stamping
capabilities a module actually implements. This is a catalog metadata call: it
must not resolve an assignment, open protected files, export private material,
contact a target, or mutate provider state. A module may expose this descriptor
without exposing Mesh. When the same descriptor also appears inside
MeshDescriptor.credentialDelivery, Hovel requires the standalone and
nested canonical descriptors to have the same SHA-256 digest.
{
"schemaVersion": "hovel.pki.credential-delivery/v1",
"deliveryCapabilities": ["runtime", "files", "stamp-standard"],
"credentialSlots": [{
"name": "control-plane-mtls",
"purpose": "mtls-server",
"endpointRole": "server",
"consumerType": "mesh-listener",
"acceptedBundleVersions": ["hovel.pki.bundle/v1"],
"acceptedProfiles": ["mtls-server"],
"acceptedCompatibilityTargets": ["portable-x509"],
"acceptedProjections": ["bundle"],
"acceptedMaterialForms": ["private-bytes"],
"maximumEncodedBytes": 16384,
"remainderPolicy": "preserve",
"privateMaterial": "allowed"
}],
"stampTargetKinds": ["named-slot"]
}
The capability list is not decorative. none is exclusive;
runtime, files, stamp-standard, and
stamp-advanced authorize only their matching requests. Provider target
and encoding extensions are admitted only through advertised, versioned JSON
schemas.
credential.runtime
Delivers one resolved projection directly to the provider process. The request is
bound to an assignment, named slot, exact module/provider/version, and descriptor
digest. Material is a closed union: public and private-byte forms carry
data; private-reference carries a provider-scoped
reference. Exactly one variant is allowed.
{
"schemaVersion": "hovel.pki.provider-execution/v1",
"provider": {
"moduleId": "lab-mesh",
"providerId": "lab-credential-loader",
"providerVersion": "1",
"descriptorSha256": "8f...64-hex-characters...2a"
},
"requestId": "credential-request-01",
"assignmentId": "assignment-listener-west",
"slotName": "control-plane-mtls",
"credential": {
"bundleVersion": "hovel.pki.bundle/v1",
"purpose": "mtls-server",
"consumerType": "mesh-listener",
"profileId": "mtls-server",
"compatibilityTargetId": "portable-x509"
},
"material": {
"projection": "bundle",
"form": "private-bytes",
"encoding": "json+der-base64",
"sha256": "6d...64-hex-characters...91",
"data": "eyJzY2hlbWFWZXJzaW9uIjoiLi4uIn0="
},
"scope": {
"operationId": "mesh-operation-01",
"listenerId": "listener-west"
}
}
{
"requestId": "credential-request-01",
"providerReference": "listener-slot:listener-west",
"receiptSha256": "91...64-hex-characters...ac"
}
The receipt is deliberately non-secret. It may identify provider-owned state and hash a provider-owned receipt, but it must not echo material, protected paths, or write-only configuration. Hovel records a secret-free execution plan and terminal receipt. For a consuming Mesh listener, task, or stream, Hovel sends this call in the same module process immediately before the consuming call; a standalone RPC process does not preserve in-memory credentials for a later invocation.
credential.files
Delivers bounded, owner-protected, invocation-scoped files. Each entry names its projection, material form, byte encoding, media type, digest, and size. Paths are valid only for this invocation unless the provider deliberately copies them into provider-owned storage and reports that action through a non-secret receipt.
{
"schemaVersion": "hovel.pki.provider-execution/v1",
"provider": CredentialProviderTarget,
"requestId": "credential-request-02",
"assignmentId": "assignment-listener-west",
"slotName": "control-plane-mtls",
"credential": ResolvedCredentialMetadata,
"files": [{
"projection": "certificate-der",
"form": "public",
"encoding": "raw",
"mediaType": "application/pkix-cert",
"path": "/owner-protected/invocation/certificate.der",
"sha256": "a7...64-hex-characters...10",
"size": 812
}],
"scope": CredentialOperationScope
}
{
"requestId": "credential-request-02",
"providerReference": "listener-files:listener-west"
}
credential.encode
Asks one advertised provider encoding schema to transform a resolved projection. The request supplies a hard byte limit. The result carries the requested bytes, their form, encoding name, and SHA-256; the daemon validates the request id, schema, form, bound, and digest before those bytes can enter another provider call. Encoded bytes remain secret-bearing transient data, not a normal list or inspect response.
{
"schemaVersion": "hovel.pki.provider-execution/v1",
"provider": CredentialProviderTarget,
"requestId": "credential-encode-01",
"providerId": "picblobs",
"providerSchema": "picblobs.nacl-config/v1",
"outputForm": "private-bytes",
"maximumEncodedBytes": 4096,
"source": ResolvedCredentialMaterial,
"scope": CredentialOperationScope
}
{
"requestId": "credential-encode-01",
"form": "private-bytes",
"encoding": "picblobs.nacl-config/v1",
"sha256": "b5...64-hex-characters...3e",
"data": "AAECAwQFBgcICQ=="
}
credential.stamp
Applies one persisted, descriptor-validated stamp plan to the exact input artifact and short-lived resolved material. Standard stamping uses a named slot. Advanced providers may advertise file offsets, virtual addresses, symbols, markers, byte patterns, or versioned provider-defined targets with bounds and preconditions. Offsets, addresses, and byte counts use canonical base-10 strings where the full unsigned 64-bit range must survive JSON clients.
{
"schemaVersion": "hovel.pki.provider-execution/v1",
"provider": CredentialProviderTarget,
"stampId": "credential-stamp-01",
"request": CredentialStampRequest,
"input": {
"id": "artifact-01",
"sha256": "42...64-hex-characters...9f",
"encoding": "raw",
"path": "/owner-protected/invocation/payload.bin"
},
"resolvedMaterial": ResolvedCredentialMaterial,
"expectedDigests": [{
"projection": "bundle",
"reference": "generation:leaf-01",
"sha256": "6d...64-hex-characters...91"
}],
"scope": CredentialOperationScope
}
{
"stampId": "credential-stamp-01",
"output": {
"artifact": {
"name": "payload-stamped.bin",
"encoding": "raw",
"path": "/owner-protected/invocation/payload-stamped.bin"
}
},
"targetResolution": "unchanged",
"resolvedTarget": { "kind": "named-slot", "namedSlot": { "name": "control-plane-mtls" } },
"bytesWritten": "4096",
"materialDigests": [{
"projection": "bundle",
"reference": "generation:leaf-01",
"sha256": "6d...64-hex-characters...91"
}]
}
Input and artifact output each carry exactly one of data or
path. The stamp output carries exactly one of artifact or
deployment. A deployment response contains an opaque reference and a
secret-aware receipt; Hovel hashes the receipt and stores only non-secret
destination evidence in durable stamp bookkeeping.
step.describe
Reports the module's current chain-step contracts. The current host calls this during catalog inspection and uses the resulting catalog snapshot during validation, module inspection, planning, and capability-step execution. Persisting the exact returned snapshot when the operator confirms, then blocking execution if a referenced contract changes after confirmation, is a target safeguard that is not implemented yet.
handshake, schema, and step.describe
are inspection paths. They must be deterministic and side-effect free: no
target connections, filesystem mutation, listener startup, payload byte
generation, credential creation, or random prepared values. Put those actions
in step.prepare, step.execute, or
execute according to the lifecycle rules below.
Planning treats the returned requirements as typed capability predicates.
Hovel compares requirement type, attributes, and allowed states against the
current capability set, then records either a deterministic capability binding
or a typed missing requirement. module inspect --json exposes that
same resolved view for operator tooling.
{
"steps": [
{
"id": "squatter.connect_smb",
"kind": "session.connector",
"configSchema": { "type": "object", "properties": {} },
"requires": [
{
"type": "PayloadInstance",
"schemaVersion": "v1",
"attributes": {
"provider": "squatter",
"transport": "smb-named-pipe"
},
"states": ["installed", "connected", "unreachable"]
},
{
"type": "CredentialCapability",
"schemaVersion": "v1",
"attributes": { "protocol": "smb" },
"states": ["active"]
}
],
"produces": [
{
"type": "SessionRef",
"schemaVersion": "v1",
"attributes": {
"provider": "squatter",
"transport": "smb-named-pipe"
}
}
],
"prepare": { "materializes": [] },
"cleanup": null
}
]
}
step.prepare
Validates step config and materializes planned values. This method is pure and local: it may generate random names, credentials, paths, endpoints, and planned cleanup handles, but it must not connect to a target, authenticate, stage files, start listeners that accept target traffic, or produce final payload bytes. Repeated prepare for an existing prepared plan preserves materialized values unless the operator explicitly regenerates or edits them.
{
"preparedPlanId": "prep-1",
"stepId": "windows.credential.create_local_admin",
"config": { "usernamePolicy": "random", "passwordLength": 24 },
"inputs": [
{ "capabilityId": "cap_remote_execution_f8a21b", "type": "RemoteExecutionCapability" }
],
"existingPreparedValues": {}
}
{
"plannedOutputs": [
{
"id": "cap_credential_6mb8pq",
"type": "CredentialCapability",
"schemaVersion": "v1",
"state": "planned",
"attributes": {
"protocol": "smb",
"username": "m7q4z92d",
"password": "plain-high-entropy-password",
"sensitive": true
}
}
],
"preparedValues": {
"username": { "value": "m7q4z92d", "editable": true },
"password": { "value": "plain-high-entropy-password", "editable": true }
},
"operatorSummary": {
"warnings": [],
"targetSideArtifacts": ["local admin user m7q4z92d"]
}
}
step.execute
Executes a confirmed step. The daemon supplies only the capabilities declared by the step contract plus minimal run metadata. A step should be idempotent where possible and must use confirmed prepared values exactly; confirmed collisions fail rather than silently changing target-side names.
{
"runId": "run-1",
"stepId": "squatter.install_smb",
"confirmedPreparedValues": {
"stagedPath": "C:\\Windows\\Temp\\n4x9q2.exe",
"serviceName": "q7mcz91a",
"pipeName": "\\\\.\\pipe\\a8kq27mf"
},
"inputs": [
{ "capabilityId": "cap_remote_execution_f8a21b", "type": "RemoteExecutionCapability" },
{ "capabilityId": "cap_credential_6mb8pq", "type": "CredentialCapability" },
{ "capabilityId": "cap_payload_artifact_vc9s31", "type": "PayloadArtifact" }
]
}
{
"status": "succeeded",
"capabilities": [ Capability, ... ],
"installedPayloads": [ InstalledPayloadDescriptor, ... ],
"stateTransitions": [ CapabilityTransition, ... ],
"evidence": [ Evidence, ... ]
}
installedPayloads is optional and is the only way a module or
provider can ask Hovel to create or update installed payload inventory. Hovel
validates each descriptor against the named payload provider before persistence.
Logs, artifacts, and generic evidence are never promoted into installed payload
records by inference.
step.cleanup
Executes or verifies cleanup for a durable CleanupHandle. Cleanup
defaults to the module that created the handle. Another module may consume a
cleanup handle only when both modules explicitly declare compatibility. Cleanup
is retriable until the handle reaches removed,
cleanup_verified, or the operator marks it
abandoned.
{
"runId": "run-1",
"stepId": "squatter.cleanup_smb",
"cleanupHandleId": "cap_cleanup_74m2wq",
"mode": "execute" // execute | verify
}
{
"status": "removed",
"stateTransitions": [ CapabilityTransition, ... ],
"evidence": [ Evidence, ... ]
}
list_payloads
Lists provider-supported payloads for live discovery. Hovel passes a
PayloadQuery directly as params. The provider returns a JSON array of
PayloadInfo objects; there is no wrapper object.
{
"platform": "windows",
"arch": "x86",
"format": "pe-exe",
"transport": "tcp-bind",
"config": { "lhost": "10.0.0.5" },
"capabilities": ["session.shell"]
}
[
PayloadInfo,
...
]
resolve_payload
Resolves one provider-supported payload from a query. Planning uses this for
compatibility and review metadata; final bytes are still generated after
confirmation by generate_payload.
{
"target": "10.0.0.42",
"platform": "windows",
"arch": "x86",
"format": "pe-exe",
"transport": "tcp-bind"
}
PayloadInfo
prepare_listener
Prepares a provider-managed listener before payload generation when the payload acquisition model requires one. This is local provider setup, not target contact.
{
"runId": "run-1",
"target": "10.0.0.42",
"payloadId": "squatter/windows/x86/windows-7/tcp-bind/pe-exe",
"config": { "lhost": "10.0.0.5", "lport": "9101" }
}
ListenerRef
generate_payload
Generates or materializes payload artifacts after confirmation. The provider
returns a primary artifact plus optional side artifacts. Inline bytes are base64;
large artifacts may be exposed through a provider handle and streamed with
read_payload_chunk.
{
"runId": "run-1",
"target": "10.0.0.42",
"payloadId": "squatter/windows/x86/windows-7/tcp-bind/pe-exe",
"format": "pe-exe",
"config": { "lhost": "10.0.0.5", "lport": "9101" },
"listener": ListenerRef
}
PayloadArtifactSet
connect_session
Connects to an installed payload from a Hovel-persisted descriptor and returns
a normal SessionRef. The provider owns compatibility checks for its
reconnect schema. Hovel owns session registration and installed-payload state
persistence.
{
"runId": "run-1",
"target": "10.0.0.42",
"payloadId": "squatter/windows/x86/windows-7/tcp-bind/pe-exe",
"installedPayloadId": "installed-payload-uuid",
"config": { "timeout": "10s" },
"reconnect": PayloadProviderRecord
}
SessionRef
cleanup_payload
Attempts provider-executed cleanup for an installed payload. A successful
provider result returns { "status": "ok" }; Hovel then marks the
installed payload removed. Failures are recorded as
cleanup-attempt events and leave the record active.
{
"runId": "run-1",
"target": "10.0.0.42",
"payloadId": "squatter/windows/x86/windows-7/tcp-bind/pe-exe",
"installedPayloadId": "installed-payload-uuid",
"reason": "operator cleanup",
"cleanup": PayloadProviderRecord
}
{
"status": "ok"
}
read_payload_chunk
Reads bytes from a provider-owned artifact handle returned by
generate_payload. The result data is base64-encoded.
{
"handle": "provider-artifact-handle",
"offset": 0,
"length": 65536
}
{
"handle": "provider-artifact-handle",
"offset": 0,
"data": "TVqQAAMAAAAEAAAA...",
"eof": false,
"encoding": "base64"
}
mesh.describe
Reports a provider-owned Mesh's static capabilities, task specs, declared triggers, and cheap topology. A Mesh can be one node, a chain, or a routed tree/graph; it is broader than a transport or tunnel. This is the only common method every Mesh provider should implement. Other Mesh methods are optional and should match the descriptor's advertised capabilities.
{
"agentContext": AgentContext
}
{
"name": "routed-node-mesh",
"version": "v0.1.0",
"capabilities": ["topology.tree", "task.survey", "task.command", "task.load", "stream.tcp"],
"tasks": [
{ "kind": "survey", "summary": "survey a node", "readOnly": true, "targetScopes": ["node"] },
{ "kind": "upload_execute", "summary": "upload and execute a file", "destructive": true, "targetScopes": ["node", "destination"] },
{ "kind": "load", "summary": "load a provider-native component or implant", "destructive": true, "targetScopes": ["node", "destination"] },
{ "kind": "command", "summary": "run a node or routed destination command", "targetScopes": ["node", "destination"] }
],
"listenerTypes": [
{
"kind": "https-beacon",
"summary": "HTTPS beacon rendezvous",
"deployments": ["embedded", "separate"],
"managementModes": ["provider", "external"],
"protocols": ["https"],
"capabilities": ["beacon"]
}
],
"triggers": [
{ "id": "beacon-command", "kind": "beacon", "nodeId": "node-2", "state": "armed" }
]
}
mesh.topology
Returns current nodes, links, and optional routes. This may inspect provider-local state but must not mutate target-side state.
{
"root": "controller",
"includeRoutes": true
}
{
"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" }
],
"routes": [
{ "id": "route-leaf-1", "nodes": ["controller", "relay-1", "leaf-1"] }
]
}
mesh.beacons
Returns recent node liveness or status signals. Beacons are evidence and routing context; they never bypass planning, confirmation, or guardrails.
{
"nodeId": "leaf-1",
"since": "2026-07-09T00:00:00Z",
"limit": 50
}
{
"beacons": [
{ "id": "beacon-1", "nodeId": "leaf-1", "state": "alive", "time": "2026-07-09T00:01:00Z" }
]
}
mesh.listeners
Lists dynamic provider-reported listening-post state, optionally filtered by stable listener id or provider-defined state. Listener configuration and credentials are deliberately absent from this read model.
{
"listenerId": "listener-west"
}
{
"listeners": [
{
"id": "listener-west",
"kind": "https-beacon",
"state": "active",
"deployment": "separate",
"management": "provider",
"addresses": ["https://127.0.0.1:8443"],
"protocols": ["https"]
}
]
}
mesh.listener.start
Idempotently starts or attaches a listening post using a caller-selected
stable listenerId. config is provider-specific and
write-only: implementations must not copy it into the returned listener,
logs, or daemon bookkeeping. Long-lived resources must remain durable across
individual module RPC invocations; the module adapter is not itself a
listening-post host.
{
"listenerId": "listener-west",
"name": "west rendezvous",
"kind": "https-beacon",
"deployment": "separate",
"management": "provider",
"config": { "bind": "127.0.0.1:8443", "credentialRef": "vault://listener-west" }
}
{
"id": "listener-west",
"kind": "https-beacon",
"state": "active",
"deployment": "separate",
"management": "provider",
"addresses": ["https://127.0.0.1:8443"]
}
mesh.listener.stop
Idempotently stops or detaches the caller-named listening post. The returned listener must preserve the requested id so external control planes can correlate retries and daemon audit records.
{
"listenerId": "listener-west"
}
{
"id": "listener-west",
"state": "stopped"
}
mesh.task
Runs a provider-owned node, route, or destination task. This is a low-level,
execution-capable primitive exposed only through the daemon's privileged local
control transport. It does not create, review, or confirm a throw plan. A front
end must apply Hovel's normal persisted-plan, confirmation, dangerous-module,
and launch-key checks before invoking it when the requested action is a throw.
Common task kinds are survey,
upload, execute, upload_execute,
command, load, and stream.
Destination-scoped tasks use nodeId or route as
the pivot and destinationHost, destinationPort,
and protocol as the endpoint reachable through that pivot.
Implant loading is modeled the same way: use upload_execute
for "copy bytes, then run them" and load only when the
provider owns a native loader. Payload bytes may be inline
inputData with inputEncoding, or a
provider-defined artifact/payload reference in config.
Providers do not need to implement every task kind; unsupported methods
or task kinds should fail explicitly.
{
"runId": "run-1",
"taskId": "upload-execute-through-node-2",
"kind": "upload_execute",
"nodeId": "node-2",
"destinationHost": "10.10.10.10",
"destinationPort": 445,
"protocol": "tcp",
"config": { "remotePath": "C:\\\\Windows\\\\Temp\\\\payload.exe" },
"inputData": "TVqQAAMAAAAEAAAA...",
"inputEncoding": "base64"
}
{
"taskId": "upload-execute-through-node-2",
"status": "succeeded",
"summary": "uploaded and executed through node-2",
"nodeId": "node-2",
"destinationHost": "10.10.10.10",
"destinationPort": 445,
"protocol": "tcp",
"outputs": { "exitCode": 0 },
"findings": [],
"sessions": []
}
mesh.open_stream
Opens a routed session flow to an endpoint reachable through a Mesh node or
route. The result is a normal SessionRef, so existing session IO,
transcript capture, and TCP/UDP local socket bridging use the same broker.
UDP bridge sessions must advertise the datagram capability;
each non-empty read or write then represents exactly one datagram.
{
"runId": "run-1",
"moduleId": "routed-node-mesh@v0.1.0",
"nodeId": "node-2",
"destinationHost": "10.10.10.10",
"destinationPort": 443,
"protocol": "tcp"
}
SessionRef
execute
Runs the module against one target. The daemon supplies the resolved configuration; the module returns its result.
{
"runId": "run-1",
"moduleId": "mock-survey@v0.0.0-example",
"target": "mock://host",
"inputs": { "failure_mode": "" },
"chainConfig": { "operator.confirmed_lab": true },
"targetConfig": { "target.host": "example.test", "target.port": "443" }
}
{
"status": "succeeded", // or "failed"
"summary": "example survey reached example.test:443",
"findings": [ Finding, ... ],
"artifacts": [ Artifact, ... ],
"outputs": { "facts": { "host": "example.test", "reachable": true } },
"installedPayloads": [ InstalledPayloadDescriptor, ... ],
"sessions": [ SessionRef, ... ]
}
While execute runs, the module may stream module/log and
module/session notifications. The daemon resolves a configuration key
by preferring per-run inputs, then targetConfig, then
chainConfig.
session/write, session/read, session/close
Drive a session the module opened during execute. Payloads are base64 so arbitrary bytes survive JSON.
// write operator input
→ {"method":"session/write","params":{"sessionId":"run-1-session-1","data":"d2hvYW1pCg=="}}
← {"result":{"status":"ok"}}
// read output (timeoutMs < 0 blocks; the daemon usually passes a finite timeout)
→ {"method":"session/read","params":{"sessionId":"run-1-session-1","timeoutMs":1000}}
← {"result":{"sessionId":"run-1-session-1","data":"bW9jay1vcGVyYXRvcgo=","closed":false}}
// close
→ {"method":"session/close","params":{"sessionId":"run-1-session-1","reason":"done"}}
← {"result":{"status":"ok"}}
shutdown
Asks the module to close any sessions and exit. The module replies {"status":"ok"} and then terminates.
Notifications (module → daemon)
module/log
A structured log record. Appears in the throw transcript alongside the daemon's own logs.
{
"level": "info", // debug | info | warning | error
"message": "connecting to target",
"logger": "mock-survey",
"fields": { "host": "example.test", "port": "443" },
"exception": ""
}
module/session
A session lifecycle event. The daemon's session broker tracks these.
{
"event": "session.created", // or "session.closed"
"session": SessionRef,
"fields": { "reason": "closed" } // present on session.closed
}
Notifications (daemon → module)
cancel
Best-effort cancellation notification. It has no id and expects no
response. Current SDKs may log it or use it to interrupt future cancellable
work; the daemon still owns process teardown when a module does not stop
cooperatively.
{
"reason": "operator requested cancel"
}
Data shapes
ModuleInfo
{
"name": "mock-exploit",
"version": "v0.0.0-example",
"moduleType": "exploit",
"summary": "Run an example exploit flow.",
"description": "Example module for the Hovel stdio JSON-RPC runtime.",
"tags": ["example", "exploit"]
}
ModuleSchema
{
"chainConfig": [ Requirement, ... ],
"targetConfig": [ Requirement, ... ],
"outputs": {
"facts": { "type": "object" }
}
}
ExecuteRequest
{
"runId": "run-1",
"moduleId": "mock-exploit@v0.0.0-example",
"target": "mock://host",
"inputs": { "failure_mode": "" },
"chainConfig": { "operator.confirmed_lab": true },
"targetConfig": { "target.host": "example.test", "target.port": "443" }
}
ModuleResult
{
"status": "succeeded",
"summary": "module completed",
"findings": [ Finding, ... ],
"artifacts": [ Artifact, ... ],
"outputs": {},
"sessions": [ SessionRef, ... ],
"installedPayloads": [ InstalledPayloadDescriptor, ... ]
}
Requirement
{
"key": "target.host",
"type": "host", // string | secret | bool | int | float | enum |
// duration | url | host | port | cidr | path | list | map
"required": true,
"default": "",
"description": "Target host name or IP address.",
"allowed": [], // enumerated values, when constrained
"secret": false // hide the value in logs and transcripts
}
Finding
{ "title": "example exploit path verified", "severity": "info", "detail": "..." }
Artifact
Either inline data or a path on disk.
{ "name": "transcript.txt", "kind": "text/plain", "data": "..." }
{ "name": "loot.bin", "kind": "application/octet-stream", "path": "/tmp/loot.bin" }
SessionRef
{
"id": "run-1-session-1",
"runId": "run-1",
"moduleId": "mock-exploit-session",
"target": "mock://host",
"name": "mock shell on mock://host",
"kind": "shell",
"state": "active", // active | closed
"transport": "stdio",
"installedPayloadId": "installed-payload-uuid",
"capabilities": ["read", "write", "exec", "close"]
}
Session Requests
{
"sessionId": "run-1-session-1",
"data": "d2hvYW1pCg==" // session/write, base64 operator input
}
{
"sessionId": "run-1-session-1",
"timeoutMs": 1000 // session/read; negative means block
}
{
"sessionId": "run-1-session-1",
"reason": "done" // session/close
}
SessionReadResult
{
"sessionId": "run-1-session-1",
"data": "bW9jay1vcGVyYXRvcgo=", // base64 session output
"closed": false
}
Capability
Capabilities are typed chain state. Core capability schemas are owned by Hovel and versioned; modules may add namespaced extension attributes. Capability ids are readable structured identifiers with random entropy, and run/step linkage is carried as fields rather than encoded as the only source of truth.
{
"id": "cap_payload_instance_q8m2v4",
"type": "PayloadInstance",
"schemaVersion": "v1",
"state": "installed",
"producerStepId": "squatter.install_smb",
"attributes": {
"provider": "squatter",
"transport": "smb-named-pipe",
"endpointId": "cap_endpoint_smb_pipe_c4a7nx"
},
"extensions": {
"squatter": {
"artifactSha256": "abc123",
"reconnectStrategy": "active_connect"
}
}
}
Core capability types for the Squatter chain are
RemoteExecutionCapability, CredentialCapability,
PayloadArtifact, PayloadInstance,
TransportEndpoint, SessionRef, and
CleanupHandle. A step may still emit partial capabilities with
explicit states and warning evidence, such as an installed payload record that
reaches unreachable when reconnect fails, or a cleanup handle in
cleanup_failed.
InstalledPayloadDescriptor
An installed payload descriptor is an explicit module/provider claim that a target-side payload instance exists and may be tracked. Hovel core validates and persists it; modules and providers never write installed payload rows directly.
{
"provider": "squatter",
"payloadId": "squatter/windows/x86/windows-7/tcp-bind/pe-exe",
"payloadVersion": "0.1.0",
"target": "10.0.0.42",
"targetId": "t1",
"state": "installed",
"transport": "tcp-bind",
"endpoint": "10.0.0.42:9101",
"instanceKey": "provider-stable-key-or-empty",
"stampId": "payload-stamp-uuid",
"artifactIds": ["artifact-squatter-exe"],
"supportsReconnect": true,
"supportsMultipleSessions": false,
"reconnect": {
"providerId": "squatter",
"schema": "squatter.reconnect.tcp_bind",
"schemaVersion": "1",
"descriptor": { "host": "10.0.0.42", "port": 9101 }
},
"cleanup": {
"providerId": "squatter",
"schema": "squatter.cleanup.tcp_bind",
"schemaVersion": "1",
"descriptor": { "remotePath": "C:\\Windows\\Temp\\n4x9q2.exe" }
},
"metadata": {
"profile": "xp-sp3-lab"
}
}
The accepted current states are installed,
connected, unreachable, and removed.
Failed installs are reported as step failure evidence, not installed payload
descriptors. When a provider can prove a repeated report is the same deployed
instance using instanceKey or stampId, Hovel may upsert
the existing record; otherwise it creates a new operator-facing handle.
PayloadQuery
{
"target": "10.0.0.42",
"kind": "pe",
"platform": "windows",
"os": "windows",
"arch": "x86",
"format": "pe-exe",
"tags": ["pe", "windows"],
"transport": "tcp-bind",
"config": { "lhost": "10.0.0.5" },
"capabilities": ["session.shell"]
}
PayloadInfo
{
"id": "squatter/windows/x86/windows-7/tcp-bind/pe-exe",
"name": "Squatter TCP bind PE",
"version": "0.1.0",
"kind": "pe",
"platform": "windows",
"os": "windows",
"arch": "x86",
"minOS": "Windows 7",
"testedOS": ["Windows 7"],
"formats": ["pe-exe", "pe"],
"tags": ["pe", "windows", "agent"],
"capabilities": ["session.shell"],
"transport": {
"kind": "tcp-bind",
"encrypted": false
},
"session": {
"kind": "shell",
"acquisition": "connect",
"requiresPreThrowListener": false,
"requiresPostThrowConnect": true,
"owner": "provider"
}
}
ListenerRef
{
"id": "listener-1",
"runId": "run-1",
"target": "10.0.0.42",
"transport": "tcp-bind",
"host": "10.0.0.5",
"port": 9101,
"pipe": "",
"state": "listening",
"fields": { "interface": "lab0" }
}
PayloadArtifactSet
{
"primary": PayloadArtifact,
"artifacts": [ PayloadArtifact, ... ]
}
PayloadArtifact
{
"name": "squatter.exe",
"role": "primary",
"kind": "pe",
"format": "pe-exe",
"os": "windows",
"arch": "x86",
"tags": ["pe", "windows"],
"encoding": "base64",
"bytes": "TVqQAAMAAAAEAAAA...",
"handle": "",
"size": 123456,
"sha256": "hex-sha256"
}
PayloadProviderRecord
{
"providerId": "squatter",
"schema": "squatter.reconnect.tcp_bind",
"schemaVersion": "1",
"descriptor": { "host": "10.0.0.42", "port": 9101 }
}
Provider Requests
{
"runId": "run-1",
"target": "10.0.0.42",
"payloadId": "squatter/windows/x86/windows-7/tcp-bind/pe-exe",
"config": {},
"listener": ListenerRef, // generate_payload only
"reconnect": PayloadProviderRecord, // connect_session only
"cleanup": PayloadProviderRecord, // cleanup_payload only
"installedPayloadId": "installed-payload-uuid",
"reason": "operator cleanup"
}
CleanupResult
{
"status": "ok"
}
PayloadChunk
{
"handle": "provider-artifact-handle",
"offset": 0,
"data": "TVqQAAMAAAAEAAAA...",
"eof": false,
"encoding": "base64"
}
Credential provider shapes
| Shape | Invariant | Durable visibility |
|---|---|---|
CredentialProviderTarget | Exact module id, provider id/version, and descriptor SHA-256. | Persisted provenance. |
ResolvedCredentialMaterial | Exactly one bytes or provider-scoped reference variant, consistent with form. | Only projection, form, encoding, digest, and size are projected into bookkeeping. |
CredentialFile | Invocation-scoped protected path with byte encoding, media type, digest, and size. | Path is stripped; metadata remains. |
CredentialDeliveryReceipt | Matching request id plus optional non-secret provider reference and receipt digest. | Only SHA-256 digests of the provider reference and receipt are persisted; the raw values are not. |
CredentialEncodingResult | Matching request id, bounded bytes, declared form/encoding, and matching digest. | Only form, encoding, digest, and size are persisted. |
CredentialArtifactInput/Output | Exactly one inline-data or protected-path variant. | Final artifact/destination metadata after daemon verification and ingestion. |
CredentialStampOutput | Exactly one artifact or deployment variant. | Resolved target, counts, material digests, and non-secret destination evidence. |
SDK secret wrappers redact ordinary debug/string formatting. That is defense in depth, not authorization to log secrets: provider code must still avoid logging, retaining, returning in errors, or copying private bytes and protected paths. Credential byte fields use canonical padded RFC 4648 base64 on the wire.
CredentialCapability
Target-created credentials are assessment evidence. They are stored and displayed
in plaintext in normal operator CLI output so the operator can remediate manually
if automated cleanup fails. This chain capability is not a workspace-PKI bundle,
certificate generation, private key, assignment, or provider delivery. The
sensitive marker labels the field for evidence exports and downstream
handling; it is not a CLI redaction request.
{
"id": "cap_credential_6mb8pq",
"type": "CredentialCapability",
"schemaVersion": "v1",
"state": "active",
"attributes": {
"protocol": "smb",
"principal": "local",
"username": "m7q4z92d",
"password": "plain-high-entropy-password",
"secretType": "password",
"targetScope": "10.0.0.42",
"sensitive": true,
"durability": "until_cleanup"
}
}
Evidence
Evidence is operator and audit material, not chain input. It uses a common envelope with flexible module-specific details so modules can report rich diagnostics without forcing core schema churn.
{
"id": "ev_squatter_connect_warning_2m7kq9",
"level": "warn",
"kind": "payload.unreachable",
"sourceStepId": "squatter.connect_smb",
"message": "Squatter installed but no session was established.",
"details": {
"target": "10.0.0.42",
"endpoint": "\\\\10.0.0.42\\pipe\\a8kq27mf",
"error": "NT_STATUS_ACCESS_DENIED"
}
}
Lifecycle summary
- daemon → module
- module → daemon
- daemon → module
- module → daemon
- daemon → module
- module → daemon
- module → daemon
- module → daemon
- daemon → module
- daemon → module
- module → daemon
- module ✕ exit