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.
- Separate implemented paths from extension contracts.
- Implement the smallest honest descriptor.
- Define capabilities and strict slots.
- Add only the execution hooks you advertise.
- Understand exact same-process binding.
- Connect active assignments to Mesh operations.
- Test framing, lifecycle, and secret handling.
Implementation status: contract versus public control path
| Surface | Implemented now | Not 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. |
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 field | Contract |
|---|---|
deliveryCapabilities | Unique and non-empty. none is exclusive. Other values are runtime, files, stamp-standard, and stamp-advanced. |
credentialSlots | Required for every non-none descriptor. Names are provider-defined and unique. |
stampTargetKinds | Required when stamping is advertised. Standard stamping requires named-slot. |
addressSpaces | Declare file for file offsets and the exact ELF, PE, or Mach-O space used by virtual-address targets. |
providerTargetSchemas | Versioned JSON Schemas for advertised provider-defined targets. They do not imply support for any built-in provider. |
providerEncodingSchemas | Versioned transformations accepted by credential.encode. Encoding is advertised by these schemas, not by another delivery-capability string. |
Make every slot strict
| Slot constraint | Question it answers |
|---|---|
purpose and endpointRole | Is this TLS server, TLS client, mutual TLS, code signing, or a custom purpose, and which endpoint role is valid? |
consumerType | Is the material for a Mesh provider, listener, node, implant, stager, payload, service, or another declared consumer? |
| Accepted bundle, profile, and compatibility values | Which active assignment generations are compatible with the provider? |
| Accepted projections and material forms | Does the hook receive a certificate, key, bundle, signer reference, provider encoding, or another typed projection, as public bytes, private bytes, or a scoped reference? |
maximumEncodedBytes | What hard bound must Hovel and the provider enforce? |
privateMaterial | Are private forms forbidden, allowed, or required? |
remainderPolicy | For 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"
}
]
}
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 method | Go | Python | Rust |
|---|---|---|---|
credential.describe | CredentialDescriber.DescribeCredentialDelivery | describe_credential_delivery | Module::describe_credential_delivery |
credential.runtime | CredentialRuntimeProvider.LoadRuntimeCredential | load_runtime_credential | Module::load_runtime_credential |
credential.files | CredentialFilesProvider.LoadCredentialFiles | load_credential_files | Module::load_credential_files |
credential.encode | CredentialEncodingProvider.EncodeCredentialMaterial | encode_credential_material | Module::encode_credential_material |
credential.stamp | CredentialStampProvider.StampCredential | stamp_credential | Module::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
| Mode | Use when | Target contract |
|---|---|---|
stamp-standard | The artifact or deployment has a stable provider-defined slot. | Advertise named-slot; the target names one declared CredentialSlot. |
stamp-advanced | The 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. |
- stable named provider slot
- stamp-standard / named-slot
- explicit location or provider schema needed
- stamp-advanced / advertised target kind
- verify bounds, precondition, digests, and output union
| Wire kind | Go/Python type | Rust variant | Extra descriptor requirement |
|---|---|---|---|
named-slot | CredentialNamedSlotTarget | CredentialStampTarget::NamedSlot | The named slot must exist. |
file-offset | CredentialFileOffsetTarget | CredentialStampTarget::FileOffset | Advertise address space file. |
virtual-address | CredentialVirtualAddressTarget | CredentialStampTarget::VirtualAddress | Advertise the exact ELF virtual, PE RVA, or Mach-O VM address space. |
symbol | CredentialSymbolTarget | CredentialStampTarget::Symbol | Enforce the declared maximum length and precondition. |
marker | CredentialMarkerTarget | CredentialStampTarget::Marker | Honor occurrence, bound, remainder policy, and precondition. |
byte-pattern | CredentialBytePatternTarget | CredentialStampTarget::BytePattern | Validate pattern, mask, occurrence, bound, and precondition. |
provider-defined | CredentialProviderDefinedTarget | CredentialStampTarget::ProviderDefined | Match 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.
- authorize non-secret assignment selectors
- validate active assignment, consumer, slot, and descriptor
- record pending secret-free execution plans
- start one module process
- re-run handshake, credential.describe, and mesh.describe
- match module ID, version, provider ID, and descriptor digest
- credential.runtime or credential.files
- mesh.listener.start, mesh.task, or mesh.open_stream
- Hovel resolves every selector against an active or degraded assignment and the exact provider descriptor discovered for the installed module.
- The daemon derives allowed Mesh provider, listener, and node consumers from the operation; callers cannot substitute an unrelated consumer ID.
- The runner writes pending credential execution plans before launching the provider process. Plans contain provenance, scope, digests, sizes, and encodings—not resolved secrets.
- The new process repeats
handshake, standalonecredential.describe, andmesh.describe. Nested and standalone descriptors are reconciled. - Every delivery target must exactly match the running process before the first secret-bearing hook is called.
- Runtime or file deliveries run sequentially in that process. Each exact request ID and receipt is validated and recorded.
- 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
| Value | Provider-call visibility | Durable visibility |
|---|---|---|
| Resolved bytes or scoped secret reference | Transient request input. | Projection, form, encoding, digest, and size only. |
| Protected file path | Invocation-scoped request input. | Path is excluded; file metadata and digest remain. |
| Provider reference | Optional receipt field. | Only its SHA-256 is persisted. |
| Provider receipt | Optional digest or secret-aware stamp deployment receipt. | Only a digest and non-secret destination evidence remain. |
| Encoded output bytes | Transient result used by another provider step. | Form, encoding, digest, and size only. |
| Stamp artifact or deployment output | Transient 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/strand 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.
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 consumer | Canonical consumer ID | When derived |
|---|---|---|
mesh-provider | <provider-name>, without the installed @version suffix | Every 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 method | Credential behavior | Provider lifetime note |
|---|---|---|
StartMeshListener | Delivers selected runtime slots before mesh.listener.start. | The adapter process exits; a surviving listener needs provider-owned durable runtime state. |
RunMeshTask | Delivers slots before mesh.task; derives provider plus optional listener/node consumers. | The process is retained only when returned sessions are adopted. |
OpenMeshStream | Delivers slots before mesh.open_stream. | The process is adopted with the returned session when session brokering is configured. |
OpenMeshBridge | Uses 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.describeis deterministic, side-effect free, and valid fornoneor 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
- Module Wire Protocol — full credential request, result, and tagged-union shapes.
- Providers, Workspace PKI, Artifacts, and Structured Logging — custody, assignment, and durable evidence boundaries.
- Developing Mesh Providers — listeners, tasks, streams, sessions, and bridges.
- Daemon RPC Contract — public front-end behavior and bookkeeping methods.
- Daemon OpenAPI — exact current public request and response fields.