HOVEL // chapter 19
Chapter 19 Part 04 / Module Development

Developing Credential Providers

A credential provider is an ordinary Hovel module with one small discovery contract and only the execution hooks it actually supports. Hovel owns workspace PKI custody, assignment policy, authorization, exact provider binding, and secret-free bookkeeping. The provider owns how short-lived material is installed, encoded, stamped, or consumed by its runtime.

Start with the module development overview. Use the module wire protocol for complete request and response shapes, Providers and Workspace PKI for the custody model, and Mesh provider development for listener, task, and stream behavior.

Implementation status: contract versus public control path

SurfaceImplemented nowNot a public initiation path today
credential.describe Go, Python, and Rust dispatch; strict domain validation; catalog discovery; canonical descriptor digesting. It is an internal module RPC, not a daemon front-end endpoint.
Runtime delivery into Mesh StartMeshListener, RunMeshTask, OpenMeshStream, and OpenMeshBridge accept non-secret selectors and deliver material in the consuming provider process. The selector accepts only runtime with certificate DER/public, public-key SPKI/public, or private-key PKCS #8/private-bytes.
credential.files Typed SDK dispatch, validation, runner support, same-process delivery type, and secret-free execution bookkeeping. The public Mesh selector does not materialize files, and no daemon mutation endpoint initiates file delivery.
credential.encode Typed SDK dispatch, provider-schema validation, bounded result verification, runner support, and digest-only execution bookkeeping. No public daemon mutation endpoint initiates provider encoding.
credential.stamp Standard and advanced target models, provider dispatch, immutable plan and result types, runner verification, persistence, and stamp list/inspect views. No production external control path initiates the complete plan, resolve, provider, ingest, and terminal-transition workflow.
Credential bookkeeping ListPKICredentialExecutions, InspectPKICredentialExecution, ListPKICredentialStamps, and InspectPKICredentialStamp are public read methods. Read access to a ledger does not imply an execution or stamping command.
Other consumers The descriptor can name implants, stagers, payloads, C2 services, services, listening posts, and external consumers. General non-Mesh assignment-to-provider orchestration remains an extension point.
Do not infer a front-end feature from an SDK type. The SDKs expose all five provider methods so providers can implement and contract-test them. The current daemon API exposes runtime credential selection only on the four active Mesh operations above. There is no credential-provider CLI flag or MCP tool to substitute for a missing daemon mutation method.

Start with the smallest optional contract

Every provider first implements its language's normal module contract. Credential delivery is optional. The smallest legal credential descriptor advertises none, with no slots, target kinds, address spaces, or provider schemas. Use this only while deliberately exposing an empty versioned contract; a module that never consumes credentials should omit credential discovery entirely.

Go

func (Provider) DescribeCredentialDelivery() (
    hovel.CredentialDeliveryDescriptor, error,
) {
    return hovel.CredentialDeliveryDescriptor{
        SchemaVersion: hovel.CredentialDeliverySchemaV1,
        Capabilities: []hovel.CredentialDeliveryCapability{
            hovel.CredentialDeliveryNone,
        },
    }, nil
}

Python

def describe_credential_delivery(self) -> CredentialDeliveryDescriptor:
    return CredentialDeliveryDescriptor(
        capabilities=[CredentialDeliveryCapability.NONE],
    )

Rust

fn describe_credential_delivery(
    &self,
) -> Result<CredentialDeliveryDescriptor, String> {
    Ok(CredentialDeliveryDescriptor {
        capabilities: vec![CredentialDeliveryCapability::None],
        ..CredentialDeliveryDescriptor::default()
    })
}

The smallest useful provider advertises one non-none capability, at least one complete CredentialSlot, and the matching hook. Do not add no-op hooks to make the provider look complete.

Design the descriptor, slots, and capabilities

credential.describe is deterministic, offline catalog metadata. It must not resolve assignments, export keys, open protected files, generate payload bytes, contact a target, or mutate provider state. The fixed schema is hovel.pki.credential-delivery/v1.

Descriptor fieldContract
deliveryCapabilitiesUnique and non-empty. none is exclusive. Other values are runtime, files, stamp-standard, and stamp-advanced.
credentialSlotsRequired for every non-none descriptor. Names are provider-defined and unique.
stampTargetKindsRequired when stamping is advertised. Standard stamping requires named-slot.
addressSpacesDeclare file for file offsets and the exact ELF, PE, or Mach-O space used by virtual-address targets.
providerTargetSchemasVersioned JSON Schemas for advertised provider-defined targets. They do not imply support for any built-in provider.
providerEncodingSchemasVersioned transformations accepted by credential.encode. Encoding is advertised by these schemas, not by another delivery-capability string.

Make every slot strict

Slot constraintQuestion it answers
purpose and endpointRoleIs this TLS server, TLS client, mutual TLS, code signing, or a custom purpose, and which endpoint role is valid?
consumerTypeIs the material for a Mesh provider, listener, node, implant, stager, payload, service, or another declared consumer?
Accepted bundle, profile, and compatibility valuesWhich active assignment generations are compatible with the provider?
Accepted projections and material formsDoes the hook receive a certificate, key, bundle, signer reference, provider encoding, or another typed projection, as public bytes, private bytes, or a scoped reference?
maximumEncodedBytesWhat hard bound must Hovel and the provider enforce?
privateMaterialAre private forms forbidden, allowed, or required?
remainderPolicyFor bounded stamping, is unused capacity preserved, zero-filled, or required to be exact?

Declare separate slots when a consumer needs both a certificate and a private key. The following descriptor matches the material pairs accepted by the current public Mesh selector:

{
  "schemaVersion": "hovel.pki.credential-delivery/v1",
  "deliveryCapabilities": ["runtime"],
  "credentialSlots": [
    {
      "name": "tls-certificate",
      "purpose": "mtls-server",
      "endpointRole": "server",
      "consumerType": "mesh-listener",
      "acceptedBundleVersions": ["hovel.pki.bundle/v1"],
      "acceptedProfiles": ["mtls-server"],
      "acceptedCompatibilityTargets": ["portable-x509"],
      "acceptedProjections": ["certificate-der"],
      "acceptedMaterialForms": ["public"],
      "maximumEncodedBytes": 65536,
      "remainderPolicy": "preserve",
      "privateMaterial": "forbidden"
    },
    {
      "name": "tls-private-key",
      "purpose": "mtls-server",
      "endpointRole": "server",
      "consumerType": "mesh-listener",
      "acceptedBundleVersions": ["hovel.pki.bundle/v1"],
      "acceptedProfiles": ["mtls-server"],
      "acceptedCompatibilityTargets": ["portable-x509"],
      "acceptedProjections": ["private-key-pkcs8"],
      "acceptedMaterialForms": ["private-bytes"],
      "maximumEncodedBytes": 65536,
      "remainderPolicy": "preserve",
      "privateMaterial": "required"
    }
  ]
}
Mesh descriptor parity. A Mesh may embed the same object as MeshDescriptor.credentialDelivery. If standalone and nested forms are both present, their canonical SHA-256 digests must match. Credential-bearing Mesh execution re-runs standalone credential.describe, so a Mesh that intends to consume selected credentials should implement that method even when it also embeds the descriptor.

Implement only the advertised execution hooks

Wire methodGoPythonRust
credential.describeCredentialDescriber.DescribeCredentialDeliverydescribe_credential_deliveryModule::describe_credential_delivery
credential.runtimeCredentialRuntimeProvider.LoadRuntimeCredentialload_runtime_credentialModule::load_runtime_credential
credential.filesCredentialFilesProvider.LoadCredentialFilesload_credential_filesModule::load_credential_files
credential.encodeCredentialEncodingProvider.EncodeCredentialMaterialencode_credential_materialModule::encode_credential_material
credential.stampCredentialStampProvider.StampCredentialstamp_credentialModule::stamp_credential

Go uses separate optional interfaces, each embedding hovel.Module. Python and Rust put typed hooks on their base module contract; inherited defaults fail explicitly. All secret-bearing requests use hovel.pki.provider-execution/v1 and carry an exact CredentialProviderTarget.

Runtime: install one resolved projection

CredentialRuntimeRequest contains the request and assignment IDs, slot, resolved credential metadata, one bytes-or-scoped-reference material union, and non-secret operation scope. Return a CredentialDeliveryReceipt with the exact request ID. An optional provider reference may identify provider-owned state; an optional receipt digest may bind provider-owned evidence.

Files: consume invocation-scoped protected paths

CredentialFilesRequest contains a bounded list of CredentialFile values with projection, form, byte encoding, media type, protected path, digest, and size. A path is valid only for the invocation unless the provider copies it into storage it owns. If it does, return non-secret reconciliation evidence rather than the path or file contents.

Encoding: honor an advertised schema and hard bound

CredentialEncodingRequest names a provider ID, provider schema, output form, maximum byte count, and resolved source. Return CredentialEncodingResult with the exact request ID, requested form, encoding identifier, SHA-256, and bytes. Hovel verifies the advertised schema, form, bound, and digest. A provider-encoding schema is valid only when at least one slot accepts the provider-encoding projection and a compatible output form.

Stamp: mutate an exact artifact or provider-owned deployment

CredentialStampExecutionRequest binds a persisted stamp request to an exact input artifact, short-lived resolved material, expected material digests, provider target, stamp ID, and operation scope. Return CredentialStampExecutionResult with the exact stamp ID, one artifact or deployment output, target-resolution status, resolved target, canonical decimal bytes-written count, and material digests.

Choose standard or advanced stamp targets

ModeUse whenTarget contract
stamp-standardThe artifact or deployment has a stable provider-defined slot.Advertise named-slot; the target names one declared CredentialSlot.
stamp-advancedThe operator or build plan must identify a bounded location explicitly.Advertise only the file offset, virtual address, symbol, marker, byte pattern, or provider-defined kinds actually implemented.
Stamp target decision
  1. stable named provider slot
  2. stamp-standard / named-slot
  3. explicit location or provider schema needed
  4. stamp-advanced / advertised target kind
  5. verify bounds, precondition, digests, and output union
Wire kindGo/Python typeRust variantExtra descriptor requirement
named-slotCredentialNamedSlotTargetCredentialStampTarget::NamedSlotThe named slot must exist.
file-offsetCredentialFileOffsetTargetCredentialStampTarget::FileOffsetAdvertise address space file.
virtual-addressCredentialVirtualAddressTargetCredentialStampTarget::VirtualAddressAdvertise the exact ELF virtual, PE RVA, or Mach-O VM address space.
symbolCredentialSymbolTargetCredentialStampTarget::SymbolEnforce the declared maximum length and precondition.
markerCredentialMarkerTargetCredentialStampTarget::MarkerHonor occurrence, bound, remainder policy, and precondition.
byte-patternCredentialBytePatternTargetCredentialStampTarget::BytePatternValidate pattern, mask, occurrence, bound, and precondition.
provider-definedCredentialProviderDefinedTargetCredentialStampTarget::ProviderDefinedMatch an advertised provider ID, schema version, and JSON Schema.

Positional targets carry canonical base-10 strings for offsets, addresses, lengths, and byte counts so the full unsigned 64-bit range survives JSON clients. Advanced targets also carry a none, literal-bytes, or SHA-256 precondition. A provider must fail before writing when the precondition, alignment, capacity, address space, or schema does not match.

Named-slot targets in each SDK

target := hovel.CredentialStampTarget{
    Kind: hovel.CredentialStampTargetNamedSlot,
    NamedSlot: &hovel.CredentialNamedSlotTarget{Name: "tls-certificate"},
}
target = CredentialNamedSlotTarget(name="tls-certificate")
let target = CredentialStampTarget::NamedSlot {
    name: "tls-certificate".into(),
};

Typed runtime provider examples

These small providers advertise one public certificate slot and store the received certificate in process memory. That is useful for framing and same-process contract tests. A production standalone provider must install material into a protected provider-owned runtime before the short-lived RPC process exits. A credential-aware Mesh can instead put these methods on the same object as its consuming Mesh hooks.

Go: compose the two optional interfaces

package main

import (
    "fmt"

    "github.com/vibepwners/hovel/sdk/go/hovel"
)

type RuntimeCredentialProvider struct {
    certificate []byte
}

func (*RuntimeCredentialProvider) Info() hovel.Info {
    return hovel.Info{
        Name: "runtime-credential-go", Version: "v0.1.0",
        Type: hovel.TypePayloadProvider, Summary: "Accept a runtime certificate.",
    }
}

func (*RuntimeCredentialProvider) Schema() hovel.Schema { return hovel.Schema{} }

func (*RuntimeCredentialProvider) Run(*hovel.Context) (hovel.Result, error) {
    return hovel.Ok(nil, hovel.WithSummary("credential provider is RPC-driven")), nil
}

func (*RuntimeCredentialProvider) DescribeCredentialDelivery() (
    hovel.CredentialDeliveryDescriptor, error,
) {
    return hovel.CredentialDeliveryDescriptor{
        SchemaVersion: hovel.CredentialDeliverySchemaV1,
        Capabilities: []hovel.CredentialDeliveryCapability{
            hovel.CredentialDeliveryRuntime,
        },
        Slots: []hovel.CredentialSlot{{
            Name: "tls-certificate", Purpose: hovel.CredentialPurposeMTLSServer,
            EndpointRole: hovel.CredentialEndpointServer,
            ConsumerType: hovel.CredentialConsumerMeshListener,
            AcceptedBundleVersions: []string{"hovel.pki.bundle/v1"},
            AcceptedProfiles: []string{"mtls-server"},
            AcceptedCompatibilityTargets: []string{"portable-x509"},
            AcceptedProjections: []hovel.CredentialProjection{
                hovel.CredentialProjectionCertificateDER,
            },
            AcceptedMaterialForms: []hovel.CredentialMaterialForm{
                hovel.CredentialMaterialPublic,
            },
            MaximumEncodedBytes: 64 << 10,
            RemainderPolicy: hovel.CredentialStampRemainderPreserve,
            PrivateMaterial: hovel.CredentialPrivateMaterialForbidden,
        }},
    }, nil
}

func (p *RuntimeCredentialProvider) LoadRuntimeCredential(
    req hovel.CredentialRuntimeRequest,
) (hovel.CredentialDeliveryReceipt, error) {
    material, ok := req.Material.Bytes()
    if !ok {
        return hovel.CredentialDeliveryReceipt{}, fmt.Errorf(
            "slot %q requires bytes", req.SlotName,
        )
    }
    defer clear(material)
    p.certificate = append(p.certificate[:0], material...)
    return hovel.CredentialDeliveryReceipt{RequestID: req.RequestID}, nil
}

var _ hovel.CredentialDescriber = (*RuntimeCredentialProvider)(nil)
var _ hovel.CredentialRuntimeProvider = (*RuntimeCredentialProvider)(nil)

func main() { hovel.Serve(&RuntimeCredentialProvider{}) }

Python: override only the supported hook

from hovel_sdk import (
    Context,
    CredentialBytes,
    CredentialConsumerType,
    CredentialDeliveryCapability,
    CredentialDeliveryDescriptor,
    CredentialDeliveryReceipt,
    CredentialEndpointRole,
    CredentialMaterialForm,
    CredentialPrivateMaterialPolicy,
    CredentialProjection,
    CredentialPurpose,
    CredentialRuntimeRequest,
    CredentialSlot,
    CredentialStampRemainderPolicy,
    HovelModule,
    Result,
    serve,
)


class RuntimeCredentialProvider(HovelModule):
    name = "runtime-credential-python"
    version = "v0.1.0"
    module_type = "payload_provider"
    summary = "Accept a runtime certificate."

    def __init__(self) -> None:
        self._certificate = b""

    def run(self, _ctx: Context) -> Result:
        return Result.ok(summary="credential provider is RPC-driven")

    def describe_credential_delivery(self) -> CredentialDeliveryDescriptor:
        return CredentialDeliveryDescriptor(
            capabilities=[CredentialDeliveryCapability.RUNTIME],
            slots=[CredentialSlot(
                name="tls-certificate",
                purpose=CredentialPurpose.MTLS_SERVER,
                endpoint_role=CredentialEndpointRole.SERVER,
                consumer_type=CredentialConsumerType.MESH_LISTENER,
                accepted_bundle_versions=["hovel.pki.bundle/v1"],
                accepted_profiles=["mtls-server"],
                accepted_compatibility_targets=["portable-x509"],
                accepted_projections=[CredentialProjection.CERTIFICATE_DER],
                accepted_material_forms=[CredentialMaterialForm.PUBLIC],
                maximum_encoded_bytes=64 << 10,
                remainder_policy=CredentialStampRemainderPolicy.PRESERVE,
                private_material=CredentialPrivateMaterialPolicy.FORBIDDEN,
            )],
        )

    def load_runtime_credential(
        self, request: CredentialRuntimeRequest,
    ) -> CredentialDeliveryReceipt:
        if not isinstance(request.material.value, CredentialBytes):
            raise ValueError(f"slot {request.slot_name!r} requires bytes")
        self._certificate = request.material.value.value
        return CredentialDeliveryReceipt(request_id=request.request_id)


if __name__ == "__main__":
    serve(RuntimeCredentialProvider())

Rust: override the typed trait defaults

use std::cell::RefCell;

use hovel::{
    Context, CredentialConsumerType, CredentialDeliveryCapability,
    CredentialDeliveryDescriptor, CredentialDeliveryReceipt, CredentialEndpointRole,
    CredentialMaterialForm, CredentialPrivateMaterialPolicy, CredentialProjection,
    CredentialPurpose, CredentialRuntimeRequest, CredentialSlot,
    CredentialStampRemainderPolicy, Info, Module, ModuleType, Outcome, Schema,
};

#[derive(Default)]
struct RuntimeCredentialProvider {
    certificate: RefCell<Vec<u8>>,
}

impl Module for RuntimeCredentialProvider {
    fn info(&self) -> Info {
        Info {
            name: "runtime-credential-rust".into(),
            version: "v0.1.0".into(),
            module_type: ModuleType::PayloadProvider,
            summary: "Accept a runtime certificate.".into(),
            description: String::new(),
            tags: vec!["credential-provider".into()],
            discovery_context: Vec::new(),
        }
    }

    fn schema(&self) -> Schema { Schema::default() }

    fn run(&self, _ctx: &mut Context) -> Outcome {
        Outcome::ok(Vec::new()).with_summary("credential provider is RPC-driven")
    }

    fn describe_credential_delivery(
        &self,
    ) -> Result<CredentialDeliveryDescriptor, String> {
        Ok(CredentialDeliveryDescriptor {
            capabilities: vec![CredentialDeliveryCapability::Runtime],
            slots: vec![CredentialSlot {
                name: "tls-certificate".into(),
                purpose: CredentialPurpose::MtlsServer,
                endpoint_role: CredentialEndpointRole::Server,
                consumer_type: CredentialConsumerType::MeshListener,
                accepted_bundle_versions: vec!["hovel.pki.bundle/v1".into()],
                accepted_profiles: vec!["mtls-server".into()],
                accepted_compatibility_targets: vec!["portable-x509".into()],
                accepted_projections: vec![CredentialProjection::CertificateDer],
                accepted_material_forms: vec![CredentialMaterialForm::Public],
                maximum_encoded_bytes: 64 << 10,
                remainder_policy: CredentialStampRemainderPolicy::Preserve,
                private_material: CredentialPrivateMaterialPolicy::Forbidden,
            }],
            ..CredentialDeliveryDescriptor::default()
        })
    }

    fn load_runtime_credential(
        &self,
        request: CredentialRuntimeRequest,
    ) -> Result<CredentialDeliveryReceipt, String> {
        let material = request.material.bytes().ok_or("slot requires bytes")?;
        self.certificate.replace(material.to_vec());
        Ok(CredentialDeliveryReceipt {
            request_id: request.request_id,
            provider_reference: None,
            receipt_sha256: None,
        })
    }
}

fn main() { hovel::serve(RuntimeCredentialProvider::default()); }

The complete language API references are generated at Go SDK API, Python SDK API, and Rust SDK API. The Go, Python, and Rust guides cover packaging and ordinary module scaffolding.

Exact provider-process binding

Credential-bearing Mesh execution does not trust a catalog name alone. The daemon derives a CredentialProviderTarget from the selected installed module: exact module ID, canonical provider ID, provider version, and the canonical credential-delivery descriptor SHA-256.

Credential-bearing Mesh invocation
  1. authorize non-secret assignment selectors
  2. validate active assignment, consumer, slot, and descriptor
  3. record pending secret-free execution plans
  4. start one module process
  5. re-run handshake, credential.describe, and mesh.describe
  6. match module ID, version, provider ID, and descriptor digest
  7. credential.runtime or credential.files
  8. mesh.listener.start, mesh.task, or mesh.open_stream
  1. Hovel resolves every selector against an active or degraded assignment and the exact provider descriptor discovered for the installed module.
  2. The daemon derives allowed Mesh provider, listener, and node consumers from the operation; callers cannot substitute an unrelated consumer ID.
  3. The runner writes pending credential execution plans before launching the provider process. Plans contain provenance, scope, digests, sizes, and encodings—not resolved secrets.
  4. The new process repeats handshake, standalone credential.describe, and mesh.describe. Nested and standalone descriptors are reconciled.
  5. Every delivery target must exactly match the running process before the first secret-bearing hook is called.
  6. Runtime or file deliveries run sequentially in that process. Each exact request ID and receipt is validated and recorded.
  7. The consuming Mesh method runs next in the same process. The provider can use ephemeral state loaded by the preceding hook.

Listener start shuts the short-lived adapter process down after the call. A long-lived listener must therefore copy or install credentials into a protected provider-owned service or deployment. A task or stream process may remain alive when Hovel adopts returned sessions. Do not assume every Mesh hook has that lifetime.

Secret lifecycle and durable bookkeeping

ValueProvider-call visibilityDurable visibility
Resolved bytes or scoped secret referenceTransient request input.Projection, form, encoding, digest, and size only.
Protected file pathInvocation-scoped request input.Path is excluded; file metadata and digest remain.
Provider referenceOptional receipt field.Only its SHA-256 is persisted.
Provider receiptOptional digest or secret-aware stamp deployment receipt.Only a digest and non-secret destination evidence remain.
Encoded output bytesTransient result used by another provider step.Form, encoding, digest, and size only.
Stamp artifact or deployment outputTransient data or protected path plus provider evidence.Verified destination, resolved target, counts, and material digests.
  • Never place credential bytes, references, protected paths, write-only listener configuration, or deployment receipts in descriptors, logs, stdout, errors, Mesh results, topology, beacon, listener, or inspect models.
  • Return only the exact request or stamp ID and the minimum non-secret evidence required by the result type.
  • SDK redaction wrappers protect ordinary string or debug formatting; explicit accessors still reveal the value to provider code.
  • Clear temporary mutable copies when practical. Python immutable bytes/str and ordinary Rust containers do not guarantee zeroization; use provider-owned zeroizing storage when that guarantee is required.
  • Give every provider-owned copy an explicit replacement and cleanup lifecycle. Redaction is not retention policy.

Hovel records a pending credential execution before calling runtime, files, or encoding hooks, then records a succeeded or failed transition. Terminal recording uses a short context independent of caller cancellation. Once credential delivery begins, the process is treated as credential-bearing: provider stderr, JSON-RPC error details, and malformed-result diagnostics are suppressed rather than returned to the caller or copied into logs. Callers receive a stable generic execution summary and diagnostic-suppression marker, while durable failure state records only a bounded stable reason.

Troubleshoot with the request, credential-operation, and correlation IDs plus the secret-free credential-execution and Mesh operation records. Reproduce the same hook through the SDK's framed-RPC contract tests using synthetic non-secret material, where provider stderr can be captured safely, and validate descriptor, target, projection, form, and receipt/result shape before testing with real credentials. Never add real credential bytes or protected paths to diagnostics, and do not disable production suppression to obtain them.

A request ID is an at-most-once execution identity, not a reusable secret handle or a Mesh replay key. On an uncertain outcome, inspect execution and Mesh bookkeeping before retrying. Reusing an ID with another provider, descriptor, assignment, slot, material, or scope conflicts; reusing a terminal ID does not start a fresh provider process. If a new attempt is explicitly judged safe, use fresh request IDs for that new process. Encoded secret bytes and enclosing Mesh results are not persisted for replay.

Integrate credentials into listeners, tasks, streams, and bridges

A front end sends only assignment and slot selectors. It never resolves a certificate or private key itself and never supplies a provider target. Bind, stage, and activate assignments first, then place credentials beside the Mesh request. A matching credentialContext is required and must explicitly approve credential use.

Assignment consumerCanonical consumer IDWhen derived
mesh-provider<provider-name>, without the installed @version suffixEvery credential-bearing Mesh operation.
mesh-listener<provider-name>/<listener-id>Listener start and operations carrying listenerId.
mesh-node<provider-name>/<node-id>Task, stream, or bridge requests carrying nodeId.

Slash-free components retain the readable forms shown above. If either component contains /, Hovel derives a bounded opaque ID as mesh-scoped:<sha256>, where the lowercase hexadecimal digest covers the canonical UTF-8 provider name, one NUL byte, and the canonical UTF-8 listener or node ID. Use the typed domain constructors and retain their result; delimiter escaping and manual concatenation are not safe.

Start a listener with certificate and key slots

POST /hovel.daemon.v1.DaemonService/StartMeshListener
Content-Type: application/json

{
  "moduleId": "mesh-provider@1.2.3",
  "request": {
    "listenerId": "edge",
    "name": "edge HTTPS listener",
    "kind": "https-beacon",
    "deployment": "separate",
    "management": "provider"
  },
  "credentials": [
    {
      "requestId": "edge-listener-cert-v1",
      "assignmentId": "assignment-edge-listener",
      "slotName": "tls-certificate",
      "capability": "runtime",
      "material": {
        "projection": "certificate-der",
        "form": "public"
      }
    },
    {
      "requestId": "edge-listener-key-v1",
      "assignmentId": "assignment-edge-listener",
      "slotName": "tls-private-key",
      "capability": "runtime",
      "material": {
        "projection": "private-key-pkcs8",
        "form": "private-bytes"
      }
    }
  ],
  "credentialContext": {
    "actorId": "operator-alice",
    "operationId": "deploy-edge-listener-v1",
    "correlationId": "web-request-018f",
    "approveCredentialUse": true
  }
}

The daemon resolves both selections, invokes credential.runtime for each slot, and only then calls mesh.listener.start in the same process. The listener result and Mesh operation record must not contain either selection's resolved bytes or the write-only start configuration.

Daemon methodCredential behaviorProvider lifetime note
StartMeshListenerDelivers selected runtime slots before mesh.listener.start.The adapter process exits; a surviving listener needs provider-owned durable runtime state.
RunMeshTaskDelivers slots before mesh.task; derives provider plus optional listener/node consumers.The process is retained only when returned sessions are adopted.
OpenMeshStreamDelivers slots before mesh.open_stream.The process is adopted with the returned session when session brokering is configured.
OpenMeshBridgeUses the same credential-aware stream-open path, then exposes the session through a daemon-owned loopback TCP or UDP endpoint.The Mesh provider owns the remote flow; the daemon owns the local bridge.

DescribeMesh, topology, beacons, listener listing, listener stop, and bridge close do not accept credential selectors. File delivery, provider encoding, and stamping are not silently approximated by the runtime selector. See the daemon RPC guide and the current OpenAPI contract before building a front end.

Testing checklist

  • Drive the real Content-Length framed module dispatcher, not only direct method calls.
  • Verify credential.describe is deterministic, side-effect free, and valid for none or for every declared slot and capability.
  • When a Mesh embeds credentialDelivery, test exact standalone/nested digest equality and reject drift.
  • Reject unsupported schema versions, missing IDs, wrong provider targets, mismatched receipt IDs, and mismatched stamp IDs before accepting results.
  • Test every advertised runtime/file projection and form, private-material policy, profile, compatibility target, slot, size bound, and consumer mismatch.
  • Test file-path lifetime and cleanup, including failure before and after provider-owned copying.
  • Test provider encoding output form, byte bound, encoding identifier, and SHA-256 validation.
  • Test every advertised stamp target, address space, alignment, capacity, remainder policy, precondition, target translation, material digest, and artifact/deployment output union.
  • Assert credential hooks run before the consuming Mesh hook in one process and that identity drift fails before the first delivery.
  • Cover listener process exit, task sessions, stream adoption, bridge cleanup, cancellation, and provider failure.
  • Assert secrets, protected paths, provider references, write-only config, encoded bytes, and deployment receipts are absent from logs, errors, Mesh read models, and list/inspect responses.
  • Cover pending, succeeded, failed, duplicate, conflicting, and terminal retry bookkeeping.

Python framed contract example

from hovel_sdk import CREDENTIAL_DELIVERY_SCHEMA_V1, ModuleRPC


def test_credential_provider_contract() -> None:
    with ModuleRPC(RuntimeCredentialProvider()) as rpc:
        descriptor = rpc.call("credential.describe")

    assert descriptor["schemaVersion"] == CREDENTIAL_DELIVERY_SCHEMA_V1
    assert descriptor["deliveryCapabilities"] == ["runtime"]

Go providers can frame calls through hovel.ServeIO; Rust providers can drive hovel::serve_with. Repository contributors use the Task-backed SDK and documentation tasks described in the language guides. Out-of-tree providers should mirror the same framed tests in their own build system.

Continue from the contract you need