Artifact Delivery¶
Artifact Delivery is the host-owned delivery and admission plane for moving
schema-bound artifacts between Orbiplex components, nodes, and public/federated
substrates without forcing every component to own transport connections or
interpret peer topology.
Status: partial
Date: 2026-05-08
Executive Summary¶
Artifact Delivery separates a component's intent from the concrete transport used to satisfy that intent.
A component should be able to say:
deliver this artifact envelope under delivery plan P
The host-owned Artifact Delivery runtime then performs:
outbound authorization
-> recipient resolution
-> transport selection
-> transport execution
-> inbound admission
-> exactly one domain acceptor
INAC becomes the private/direct node-to-node transport adapter inside this plane,
not the top-level abstraction. Agora remains the public/federated record
substrate. Middleware and node-attached components can opt into Artifact Delivery
through explicit inbound acceptor and outbound send declarations, visible to the
operator as data. The delivery plan may contain a single recipient selector,
multiple destinations executed in parallel, or staged fallback routes such as
node -> on failure -> agora.
The key decision is strict: inbound artifact admission is single-owner, not a middleware chain. A given artifact kind has at most one authoritative acceptor. If a domain needs fan-out, it must do so behind one explicit acceptor that owns that domain semantics.
Proposal 088: Pull-Based Artifact Acquisition defines future operator-admitted pull from independent sources. It reuses this solution's inbound admission and exact acceptor registry after carrier-neutral verification; it does not become another outbound transport adapter and does not change Artifact Delivery's single-owner rule.
P088 may also consume bounded plural location advice received through INAC. Such advice can point to an exact alternate, an exact fallback, or a related artifact, including a parseable carrier that embeds a portable package. It remains inert retrieval guidance: resolving a URI, extracting a candidate, and verifying its bytes do not select or bypass an Artifact Delivery acceptor. Only the ordinary inbound admission path may hand the verified artifact to one authoritative domain owner.
Context and Problem Statement¶
The Inter-Node Artifact Channel (INAC) solution defines a direct artifact transfer surface between authenticated nodes. During design review, a sharper separation emerged:
- INAC should own private/direct node-to-node artifact transport.
- A higher layer should own component-facing delivery intent, recipient resolution, outbound permission, operator-visible routing, and inbound admission dispatch.
- Components must not each create their own peer listeners, Matrix rooms, retry loops, or transport-specific connection managers.
- Existing middleware
input_chainsare useful precedent for declarative runtime registration, but their chain semantics are too broad for artifact admission.
Without this layer, every component that needs inter-node artifact movement would need to know whether to call INAC, Agora, Seed Directory, peer-message dispatch, or Matrix mailbox. That would duplicate transport logic and make policy invisible to the operator.
With this layer, components request artifact delivery through one host-owned interface, and the host keeps transport, policy, and admission routing coherent.
Current implementation status:
node/artifact-delivery-coreowns pure envelope validation, route expansion, recipient resolution for the MVP selector set, outbound authorization, deterministic delivery ids, target deduplication, and failure classes. Startup/config validation now also rejects duplicate route/default/group ids, unknown outbound route refs, recursive or unresolved configured defaults, nested groups, empty groups, and invalid route plans before the first delivery request is accepted.node/artifact-deliveryowns the host runtime, exact transport adapter selection by resolvedadapter_scheme, stage/target outcomes, idempotent retry by deterministic delivery id, and a SQLite delivery ledger at<node-data-dir>/storage/artifact-delivery.sqlite. The SQLite ledger uses explicituser_versionmigrations,busy_timeout,foreign_keys, and WAL-oriented pragmas; idempotent retry preserves the original submission timestamp.node/ad-hostis the in-process host-service composer for Artifact Delivery. It owns the composed runtime handle, the background recovery worker, Matrix mailbox transport/worker/chunk-store ownership, AD route/status snapshots, AD-owned transport cache status, and inbound acceptor construction. It does not know daemon HTTP routes,EndpointRuntimeContext, or daemon config types; the daemon maps layered config intoAdHostConfig, owns lifecycle/local HTTP, and supplies future peer/INAC/middleware/object-store seams through consumer-side traits rather than reverse dependencies. The token-protected object-store fetch path is likewise mediated through thead-hostobject-store seam, so AD HTTP routes do not reach into daemon store internals directly. Theinac-direct,agora-publish, Matrix mailbox, andobject-store-indirecttransport adapters,agora-record:andinac-peer-artifact:payload resolvers, peer-artifact transport cache, tracing observer, supervised channel acceptor, in-process acceptors, and JSON-e Flow acceptors now live behind this boundary; the daemon supplies only consumer-sidePeerSender,ArtifactObjectStore,ArtifactObjectFetchIssuer,InacAdmissionBridge, middleware, Memarium, contact-request, and Matrix sealing shims until future host-service crates take over their native seams. The daemon-local AD HTTP routes are now a domain route module over theAdHostAPI, not a place where transport adapters, recovery workers, acceptors, or object-store internals are composed.- The daemon exposes
artifact.delivery.sendand the narrower host-localartifact.delivery.retain. Retain accepts one caller-owned, digest-and-size bound object, returns its immutableartifact-store:ref and a P081 receipt, and grants neither read, delivery, publication, nor recipient authority. Its first consumer is P084 Sensorium Web representation retention. The daemon also exposesGET /v1/artifact-delivery/routes,GET /v1/artifact-delivery/deliveries, and per-delivery lookup. - Node UI exposes
/admin/artifact-deliveryas an operator view over routes, adapters, config diagnostics, and recent deliveries. - The daemon registers the
agora-defaultpublish adapter. It accepts byte-identicalagora-record.v1artifacts and submits them to the configured Agora HTTP endpoint. That endpoint may be a local supervisedagora-serviceor a remote/thin-node relay endpoint configured in the adapter. Artifact Delivery must not require every publishing node to run a local Agora service. agora-defaultdoes not sign records. Components that need host custody use a separate host capability such asagora.record.signfirst, then pass the already signedagora-record.v1through Artifact Delivery. Story-005's local acceptance profile is a narrow exception for repeatable laptop smoke tests: public Whisper candidates may be signed by a deterministic nym fixture only when the middleware is started with an explicit acceptance mode guard. That fixture certificate is topic-scoped and short-lived, and is not a production signing path.ad-hostowns theinac-directadapter. Local targets use the localInacRuntimeshort-circuit; remote targets use the daemon-suppliedPeerSenderseam over the peer supervisor's authenticated WSS peer-message session withmsg = "inac.v1".ad-hostalso owns the Matrix mailbox store-and-forward transport, inbound worker, and sealed chunk store; the daemon supplies only the concrete node-key sealer shim and Matrix sink configuration. Matrix mailbox is enabled only through explicit routes/fallbacks. Theinac/authorization.modecarried by an AD target is explicit and mode-bound:invitation-passportfor receiver-issued invitation passports,capability-passportfor generic capability-scoped pushes such asmessaging-receive@v1, andcustody-passportfor Memarium custody pushes. AD forwards this proof; INAC remains the receiver-side authority gate. Remote WSS inboundoffer/pushframes are fail-closed by default and must matchinac_peer_transport.inbound_allowed_peersbefore they reach Artifact Delivery admission. A configured-but-unavailable peer session is retryable; a disabled peer transport is a permanent delivery configuration failure.- Matrix mailbox is a carrier, not an authority. Outbound mailbox delivery
requires explicit INAC authorization in the target addressing, seals the
inac-control.v1frame asartifact-mailbox-sealed.v1to the recipient node, and posts it as a Matrix event. The receiver unseals, revalidates size andsha256:*, feeds the frame through the same INAC/Artifact Delivery admission path used by WSS, including the same explicit invitation/capability/custody passport modes, and only caches inline payload bytes in the host-owned peer artifact store after the frame is accepted. Replay protection is not delegated to Matrix ordering: it relies on AD idempotency keys and INAC invitation/passport consumption semantics. The inbound worker exposes bounded operator-visible status (enabled,running, received/accepted/rejected counters, panic count, cache write failures, last error, last event time) through the AD status surface without exposing payloads, passport bodies, or Matrix event bodies. - The local
inac-directadapter is governed by Artifact Delivery outbound allowlists when it is reached throughartifact.delivery.send. Direct component calls to INAC host capabilities such asinac.offer,inac.request, orinac.pushare governed by the separate INAC outbound allow gate. These gates are intentionally distinct until a shared policy registry is proven necessary. - The runtime has the first inbound acceptor registry with single-owner conflict
detection for
(artifact schema, content type)classes, deterministic inbound admission ids, receiver-local admission/refusal ledger, and operator-visible admission status APIs. Inline inbound artifacts are checked forsize/bytesand non-emptysha256:*byte identity before an acceptor is invoked. Exact content-type acceptors may coexist with one wildcard acceptor for the same schema; exact matches win and the wildcard remains the fallback.ad-hostnow owns concrete host/component acceptor adapters for supervised HTTP middleware, in-processinac.push,agora-record.v1,memarium-blob.v1,contact-request.v1, and explicitly configured pure JSON-e Flow acceptors. The daemon supplies only the effect shims for middleware calls, Memarium custody, contact-request persistence/notification, and local INAC admission.POST /v1/artifact-delivery/admissionsremains the shared inbound admission path. Remote INAC WSS peer messages feed this same admission path on the receiver. - Current production adapters can resolve referenced payloads through an
explicit resolver registry. The initial production resolver is
artifact-store:, rooted at<node-data-dir>/storage/artifact-store; it canonicalizes paths, rejects symlink/path escapes, limits relative ref suffix length, opens without following final symlinks on Unix, caps size, and verifies the declaredsha256:digest before invoking a transport adapter or acceptor. Small resolved payloads remain inline; larger local/resolved payloads become file-backed values so stream-capable adapters can avoid materializing one large JSON body. The daemon also registers separate public-space Memarium resolver schemes:memarium-public-entry:<entry-id>resolves to the entry payload body andmemarium-public-fact:<fact-id>resolves to the full fact, both as canonical JSON with size and digest verification. Scoped Memarium resolver schemesmemarium-entry:<space>:<entry-id>andmemarium-fact:<space>:<fact-id>are also available for spaces such aspersonal,community,public, andcrisis, but they require outbound component context and reuse the host'smemarium.readpassport gate before reading.agora-record:<record-id>is available as an explicit opt-in resolver: it fetches the record from the configured Agora endpoint, rejects declared or advertised response sizes above the resolver limit, and still treats the envelope digest as the source of truth.inac-peer-artifact:<node-id>:<artifact-id>is also available as an explicit opt-in resolver backed by the host-owned peer artifact cache; it is a digest-bound local cache lookup, not a remote fetcher and not a trust source. The peer artifact cache is an AD transport cache, not Memarium and not a temporal/fact store: it carries no provenance, may be evicted by TTL/count/byte policy, and is reconstructable from the transport source. Other schemes such ashttp:orfile:are not implicitly enabled. - The runtime records, validates, and enforces the mechanical subset of
policy: route/selector allowlists, fan-out and byte caps, delivery timeout, retry budget, idempotency, and the privacy-vs-transport invariant thatprivacy = private | private-directcan use only adapter schemes explicitly reviewed as private-safe. The reviewed private-safe set isinac-direct,matrix-mailbox, andobject-store-indirect; public/federated or merely unreviewed adapters fail closed. Envelopes may carry optional top-levelclassification; guarded INAC/private routes require it and apply the shared classification egress guard before transport. Domain retention and revocation freshness semantics remain owned by adapters or domain acceptors and must fail closed if unsupported. - Story-005 uses both sides of this path: public candidates go through
AD -> agora-default, while private/direct candidates are signed as byte-identicalagora-record.v1artifacts and transported throughAD -> inac-directto the configured peer. A private/direct record is not a public Agora publication. The Story-005 smoke pre-warms the direct A -> B peer session with the host-ownedpeer.session.establishcapability and gives the private AD route a longer timeout than public Agora publication, because private/direct delivery may have to wait for a WSS peer session. capability-many,participant,org, androuting-subjectselectors may carrymax/nodesto bound fan-out at resolution time before the route-level target cap is enforced. Participant and routing-subject resolution is host-composed through Seed Directory projections and has a daemon-side TTL cache;orgresolution is fail-closed through an explicit host-composed resolver that currently maps configured org custodians to participant node candidates. The core crate only sees lookup traits. Public routing-subject lookup only returnspublic-unlinkedbindings. Other disclosure modes are stored facts, not enumerable public routing results until a separate authorized presentation path exists.- Discovery-backed direct delivery now has a transport evidence bridge for
node endpoints. When Seed Directory can provide
node-address-attestation.v1for a resolved participant or routing-subject node, the daemon records the selectedendpoint/certificatefingerprint and advisory route id in the peer supervisor's endpoint evidence companion. The subsequent WSS dial carries the fingerprint intoDialCandidate.expected_tls_certificate_sha256and the advisory route id into the TLS CN consistency check; both fail closed on mismatch before INAC or Artifact Delivery payload exchange. Forrouting-subjectselectors, the daemon additionally requires the attested endpoint certificate advisory route id to equal the requestedrouting:did:key:...subject before producing a direct target. Advisoryroute:ids must be non-empty, and delegatedrouting:did:key:...advisory ids are protocol-validated as Ed25519 did:key material before endpoint evidence is accepted. This bridge keeps transport evidence in daemon composition; Artifact Delivery core continues to see only resolved targets and route policy. - Discovery and routing freshness are governed by
peer_discovery.freshness_policy. Deployment-class defaults encode the current public profile recommendation: endpoint advertisements are short-lived reachability candidates, endpoint attestations are short-to-medium evidence, service CA material and routing bindings live longer, and resolver cache TTLs remain short. - For direct subject-node delivery, endpoint evidence is not merely diagnostic.
A resolved subject node is promoted to a concrete direct target only when the
Seed Directory attestation provides usable
endpoint/certificateevidence for the selected endpoint.freshevidence can be used immediately;usableevidence still carries a certificate pin but may require a fresh probe or peer handshake before sensitive payload exchange. Stale or dead evidence is not used for direct/private delivery. - Service CA material follows a separate trust-policy path. The daemon exposes
service_ca_trust_policyand an operator evaluation endpoint forservice-ca-material.v1; cryptographic signature verification must succeed before local policy evaluation is meaningful, and accepting a candidate under local policy does not automatically install it as a runtime trust root.
The hard-MVP implementation now includes the generic object-store indirect
transport: the schema and selector vocabulary are backed by a daemon adapter,
token-bound HTTP fetch, sealed Matrix mailbox control dispatch, and
receiver-side rehydration before normal admission. INAC authorization/invitations,
agora-record: payload resolution, public/scoped Memarium referenced payload
resolution, configurable Memarium custody target-space policy for
memarium-blob.v1, lightweight AD profiling counters, WSS stream chunks above
the inline ceiling, the inac.stream.chunk.binary.v1 chunk carrier with
JSON/base64url fallback, Matrix mailbox store-and-forward with room lifecycle
repair and larger-object event chunking, metadata-only read-only observers, and
inac-peer-artifact: peer artifact resolution are implemented at MVP level.
The MVP also includes a P055-style
deferred submit mode, a manual recovery pass, and a daemon background recovery
worker enabled by default:
artifact.delivery.send?mode=deferred persists an accepted delivery and returns
deferred-operation.v1 with a stable operation/id, expires_at,
audit/outcome-ref, and a status_href that resolves to canonical
deferred-operation-status.v1. POST /v1/artifact-delivery/recover retries
recoverable ledger records, preserves previous attempts in retry/history, and
returns schema-gated artifact-delivery-recovery.v1. The background worker is
host-owned, has configurable interval/batch/pass-deadline limits, shuts down
cooperatively with the daemon, and is visible in the Artifact Delivery operator
status. If a daemon exits while a delivery is running, the SQLite ledger
marks that interrupted record as failed-retryable on the next open; normal
recovery still only executes accepted and failed-retryable records, so live
in-process streams are not duplicated.
The daemon-level knobs are intentionally small:
artifact_delivery_recovery.enableddefaults totrue;artifact_delivery_recovery.interval_ms,batch_limit, andpass_deadline_msbound automatic recovery work;artifact_delivery_acceptors.http_admission_allowed_source_adaptersis the control-plane HTTP admission source allowlist. Empty means deny-all forPOST /v1/artifact-delivery/admissions; in-process transport adapters such as the WSS INAC peer handler may still call the runtime admission path directly after their own transport policy gates;artifact_delivery_acceptors.supervised_channeldeclares supervised channel middleware acceptors withcomponent_id,artifact_schema, optionalcontent_type,invoke_path, request timeout, and response size limit. The referenced component must exist in the daemon middleware config at startup;artifact_delivery_acceptors.in_processdeclares host-composed acceptors built byad-host; the daemon supplies only effect shims. The MVP supportsinvoke = "inac.push",invoke = "agora-record.ingest",invoke = "memarium-blob.accept", andinvoke = "contact-request.receive".artifact_delivery_acceptors.json_e_flowdeclares pure JSON-e Flow inbound acceptors. These flows are compiled once at daemon startup, must contain arespondstep that yields anInboundAdmissionResult, and cannot declare host capability calls.inac_peer_transport.inbound_allowed_peersis the receiver-side remote WSS INAC allowlist. Empty means deny-all; this prevents ambient authority unless a later INAC gate accepts an invitation or capability passport.artifact_delivery_adapters.matrix_mailbox.enableddefaults tofalse. Enabling it requires a Matrix homeserver URL, access token, mailbox room alias prefix, server name, event type, event payload byte limit, and retention window for the host-owned peer artifact cache. The current implementation always seals mailbox control frames before posting them;seal_plain_payloadsremains a conservative configuration marker and must not be treated as a plaintext bypass. Single Matrix events carryartifact-mailbox-sealed.v1. When the sealed payload would exceed the configured event byte limit, the sender emitsartifact-mailbox-chunk.v1events in the same deterministic recipient room. The receiver stores chunks under<data-dir>/storage/artifact-delivery/matrix-mailbox-chunks/, validates per-chunk digests, total sealed digest, count, size, TTL, and duplicate consistency, then unseals and enters the normal AD/INAC admission path. Roomensureis retried around send failures and status exposes room readiness, room errors, chunked transfer counters, chunk event counters, and inbound worker counters without payload/passport bodies. The current Matrix mailbox chunker still materializes the sealed payload locally and caps that path at the AD default resolved-payload budget (64 MiB); Matrix media or a streaming object-store protocol remains a later layer. Peer artifact cache cleanup runs at startup and opportunistically after a bounded number of writes; corrupt indices, expired entries and orphaned content files are removed before count/byte cap eviction. Resolver reads also fail as retryable cache misses when an index is expired or its content file has disappeared, so evicted cache state does not surface later as an acceptor-level file I/O failure.peer_artifact_cache_max_entriesdefaults to4096,peer_artifact_cache_max_bytesdefaults to256 MiB, and zero is invalid when Matrix mailbox is enabled. These limits are transport-cache hygiene, not Memarium retention policy.
Proposed Model / Decision¶
Artifact Delivery is a node-attached host runtime composed by the daemon or host. It is not a separate public protocol and not a workflow engine.
Its responsibilities are:
- Accept component delivery requests.
- Authorize outbound artifact schemas per component.
- Resolve recipient selectors into concrete delivery targets.
- Choose and invoke a transport adapter.
- Receive artifacts from transport adapters.
- Authorize inbound delivery.
- Dispatch to exactly one authoritative acceptor.
- Expose route tables, conflicts, counters, and refusals to the operator.
Layering¶
flowchart TD
C["Component intent"] --> AD["Artifact Delivery runtime"]
AD --> OA["Outbound allow table"]
AD --> R["Recipient resolver"]
R --> TS["Transport selector"]
TS --> INAC["INAC transport adapter"]
TS --> AGORA["Agora publish adapter"]
TS --> MATRIX["Matrix mailbox adapter"]
INAC --> ADIN["Artifact Delivery inbound admission"]
MATRIX --> ADIN
AGORA --> AGORAA["Agora admission"]
ADIN --> IA["Inbound acceptor table"]
IA --> D["Domain component acceptor"]
The diagram is intentionally asymmetric: publishing to Agora is not the same as private delivery to a node. Agora is a public/federated record substrate with its own admission rules. INAC WSS and Matrix mailbox transports feed Artifact Delivery inbound admission for private/direct artifacts.
Core Vocabulary¶
| Term | Meaning |
|---|---|
| Artifact | A byte-identical payload with a declared schema/kind, content type, digest, size, and optional envelope id. |
| Component | A supervised middleware, in-process node-attached component, or other host-composed actor allowed to request delivery or accept inbound artifacts. |
| Outbound allow | A config/report declaration that a component may send artifacts of selected schemas through Artifact Delivery. |
| Delivery envelope | One single-arity request object carrying component identity, artifact, delivery plan, and policy. |
| Recipient selector | A transport-neutral target expression such as node, participant, capability-first, configured-default, or agora-default. |
| Recipient resolver | Host-owned resolver that turns a recipient selector into one or more concrete transport targets. |
| Delivery plan | A declarative route plan containing one or more stages. Each stage has targets, execution mode, success policy, and optional fallback behavior. |
| Destination group | A locally configured set of recipient selectors, for example future user-defined groups such as friends, family, work, or synchronizers. |
| Transport adapter | A concrete delivery mechanism such as INAC over WSS, Matrix mailbox store-and-forward, or Agora publish. |
| Inbound acceptor | Exactly one authoritative component endpoint for one artifact schema/content-type class. |
| Admission | The host-owned inbound gate that checks transport context, authorization, budgets, idempotency, and acceptor lookup before calling the acceptor. |
Delivery Envelope Shape¶
The eventual wire/control shape should be formalized as a schema such as
artifact-delivery-envelope.v1. The important point is single arity: the
component submits one delivery envelope, and the delivery/plan inside that
envelope chooses the recipient mode, fan-out, and fallback behavior. The
component API should not grow separate methods for send_to_node,
send_to_participant, send_to_capability, send_to_group, or
publish_to_agora.
The host capability name is artifact.delivery.send.
The default call is synchronous from the caller's perspective: the host persists
the delivery and attempts transport execution before returning
artifact-delivery-result.v1. A caller that explicitly opts into
?mode=deferred receives canonical deferred-operation.v1 instead; the
delivery can then be observed through
/v1/artifact-delivery/deliveries/{delivery-id}/operation-status and retried by
the host's recovery pass. ?mode=deferred is the only canonical URI selector;
boolean aliases such as ?deferred=true are not part of the contract. This
keeps P055 as the shared deferred response contract while Artifact Delivery
continues to own its own delivery ledger and transport semantics.
The solution-level shape is:
{
"schema": "artifact-delivery-envelope.v1",
"component/id": "vendor.middleware",
"artifact": {
"schema": "vendor.foo.v1",
"content/type": "application/json",
"digest": "sha256:...",
"size/bytes": 1234,
"bytes/base64": "..."
},
"delivery/plan": {
"mode": "sequence",
"stages": [
{
"stage/id": "direct-node",
"mode": "parallel",
"targets": [
{
"selector/kind": "node",
"node/id": "node:did:key:..."
}
],
"success/policy": "all"
}
]
},
"policy": {
"privacy": "private",
"delivery": "at-least-once",
"max/recipients": 1,
"timeout/ms": 5000
}
}
The artifact bytes remain domain-owned. Artifact Delivery may validate the outer envelope, content type, size, digest, authorization, delivery plan, recipient selectors, and delivery policy, but it must not reinterpret the domain payload.
For ergonomic MVP implementations, a single recipient selector may be accepted
as syntactic sugar and normalized immediately into a one-stage delivery/plan.
The normalized plan is the internal contract.
Delivery Plans¶
The delivery plan is the place where multiple destinations and fallback live. It is not a workflow engine: it only describes how to deliver the same artifact to one or more transport targets.
A delivery/plan may either contain inline stages or a route/ref pointing to
a configured route. Route references are normalized to concrete stages before
authorization and execution.
Route ids are stable operator-facing names. A route may carry an internal
route/version; the delivery ledger records the expanded normalized plan so
historical runs remain auditable after a route changes.
Recommended plan vocabulary:
| Field | Meaning |
|---|---|
route/ref |
Optional configured route id. Mutually exclusive with inline stages in the request envelope. |
mode |
Plan execution mode. MVP: sequence. Later: parallel for independent top-level stages. |
stages |
Ordered stage list. In sequence mode, the next stage runs only if the current stage fails under its success policy. |
stage/id |
Stable local id for diagnostics and idempotency. |
targets |
One or more recipient selectors resolved by the host. |
success/policy |
Stage success rule: all, any, or quorum. |
quorum/min-success |
Required when success/policy = quorum. |
on/failure |
Optional next stage id for explicit fallback routing. If omitted in sequence mode, the next listed stage is the fallback. |
Validation and resolution failures are not delivery failures. A malformed
selector, such as selector/kind = node without node/id, fails before any
transport adapter is invoked. An unresolved configured default or group is also
a route/configuration error. Fallback applies only after a valid target has been
resolved and a delivery attempt fails under the stage's success policy.
Direct node with Agora fallback:
{
"mode": "sequence",
"stages": [
{
"stage/id": "direct-node",
"mode": "parallel",
"targets": [
{
"selector/kind": "node",
"node/id": "node:did:key:..."
}
],
"success/policy": "all",
"on/failure": "public-agora"
},
{
"stage/id": "public-agora",
"mode": "parallel",
"targets": [
{
"selector/kind": "agora-default"
}
],
"success/policy": "all"
}
]
}
Send to an explicit node, a configured group, and Agora at once:
{
"mode": "sequence",
"stages": [
{
"stage/id": "fanout",
"mode": "parallel",
"targets": [
{
"selector/kind": "node",
"node/id": "node:did:key:..."
},
{
"selector/kind": "group",
"group/id": "friends"
},
{
"selector/kind": "agora-default"
}
],
"success/policy": "all"
}
]
}
Group fallback where the group succeeds if at least one member succeeds:
{
"mode": "sequence",
"stages": [
{
"stage/id": "primary-node",
"targets": [
{
"selector/kind": "node",
"node/id": "node:did:key:..."
}
],
"success/policy": "all",
"on/failure": "synchronizers"
},
{
"stage/id": "synchronizers",
"targets": [
{
"selector/kind": "group",
"group/id": "synchronizers"
}
],
"success/policy": "any"
}
]
}
Group expansion is policy-sensitive. A group selector resolves to zero or more
concrete recipient selectors, and the stage's success/policy decides whether
the expanded group fails on the first member failure (all), only when every
member fails (any), or when fewer than a configured number succeed (quorum).
Stages may mix explicit targets and configured targets. For example, one stage
may include explicit node selectors, a configured-default, a group, and a
future capability-many selector such as "up to two acceptable agora.relay
providers". All selectors are first resolved into concrete canonical targets,
then deduplicated, and only then counted against fan-out limits and executed.
Resolution order:
inline targets / route ref
-> resolve configured-default
-> expand groups
-> resolve capability selectors
-> produce canonical concrete targets
-> deduplicate by canonical target key
-> enforce fan-out and target allow limits
-> execute transport adapters
Example canonical target keys:
inac:node:<node-id>for direct node delivery;agora:default:<topic-or-route-id>for the local default Agora route;agora:node:<node-id>:<topic-or-route-id>for a future explicit node-hosted Agora target;matrix-mailbox:node:<node-id>for the Matrix mailbox store-and-forward transport.
This deduplication happens after full resolution because only then can the host
see that an explicit node/id, a configured default, a group member, and a
capability resolver result all point to the same concrete destination.
When multiple selectors resolve to the same concrete target, the retained target keeps the first provenance in this priority order:
explicit -> configured-default -> group -> capability/subject/org
This keeps operator diagnostics stable: the target is delivered once, but the UI can still explain why that target was selected.
Configured Routes and Groups¶
The preferred operational shape is config-driven. Components may request a named route, and the host config decides whether that route delivers to one node, several nodes, a future user-defined group, Agora, or staged fallback targets.
The config may also pin a named default to a concrete node/id. This does not
create a separate selector kind. The selector kind remains node; the configured
default simply resolves to a concrete node selector. This keeps the protocol
small while allowing an operator to say "the default private destination for
this component is exactly this node".
Example effective config fragment:
{
"artifact_delivery": {
"defaults": [
{
"name": "primary-recipient-node",
"selector": {
"selector/kind": "node",
"node/id": "node:did:key:..."
}
}
],
"groups": [
{
"group/id": "synchronizers",
"members": [
{
"selector/kind": "node",
"node/id": "node:did:key:..."
},
{
"selector/kind": "node",
"node/id": "node:did:key:..."
}
]
}
],
"routes": [
{
"route/id": "private-whisper-default",
"plan": {
"mode": "sequence",
"stages": [
{
"stage/id": "direct-recipient",
"targets": [
{
"selector/kind": "configured-default",
"name": "primary-recipient-node"
}
],
"success/policy": "all",
"on/failure": "synchronizer-group"
},
{
"stage/id": "synchronizer-group",
"targets": [
{
"selector/kind": "group",
"group/id": "synchronizers"
}
],
"success/policy": "any"
}
]
}
}
]
}
}
The component-facing envelope can then stay small:
{
"schema": "artifact-delivery-envelope.v1",
"component/id": "whisper-intake",
"artifact": {
"schema": "whisper-private-transfer.v1",
"content/type": "application/json",
"digest": "sha256:...",
"size/bytes": 1234,
"bytes/base64": "..."
},
"delivery/plan": {
"route/ref": "private-whisper-default"
}
}
Route references are still subject to outbound authorization. A component is not allowed to smuggle a wider route through a named config entry unless its outbound allow permits that route id, target selector classes, and fan-out limits.
recipient/selectors in an outbound allow is an authorization allowlist, not a
routing plan and not a fallback order. For example, allowing both node and
agora-default means the component may use either selector when the
delivery/plan or configured route/ref explicitly selects it. It MUST NOT
mean "try node, and if node/id is missing, publish to Agora".
configured-default may also be used as one target inside a larger stage, not
only as a whole route/ref. This lets a sender combine explicit recipients with
operator-owned defaults without knowing whether the default expands to one node,
a group, or a local public/federated route:
{
"mode": "sequence",
"stages": [
{
"stage/id": "fanout",
"mode": "parallel",
"targets": [
{
"selector/kind": "node",
"node/id": "node:did:key:A"
},
{
"selector/kind": "configured-default",
"name": "private-whisper-default-targets"
}
],
"success/policy": "any"
}
]
}
Recipient Selectors¶
Implemented recipient selector kinds:
| Kind | Meaning | Likely transport |
|---|---|---|
node |
Send to one explicit node id. | INAC WSS |
configured-default |
Resolve a named local default from host config; the default may resolve to a concrete node selector. |
Configured adapter |
agora-default |
Publish an Agora record to the default local/federated Agora route. | Agora publish |
capability-first |
Pick the first acceptable node advertising a capability. | Seed Directory + policy |
capability-many |
Pick up to N acceptable nodes advertising a capability. | Seed Directory + policy |
participant |
Resolve an explicitly public/operator participant to one or more reachable node ids under local policy. This is opt-in disclosure, not the privacy-preserving default. | Seed Directory + policy |
org |
Resolve an organization through an explicit host-composed org lookup. The current daemon resolver uses configured org custodians plus Seed Directory participant projections and fails closed when no evidence is available. | Org custody/policy + Seed Directory |
routing-subject |
Resolve a delegated, scoped contact/delivery identity to one or more reachable node ids without publishing the root participant id. | Seed Directory + policy |
contact-lookup |
Resolve a lookup-safe contact index through a host-composed Contact Catalog consumer path, then normalize the result to a routing-subject or concrete node target. MVP supports lookup/mode = invitation-only, blinded-digest or psi, purpose = messaging, selector/purpose = contact-request/messaging, and max/nodes = 1. A node may use a local provider when one is configured, but the consumer role is separate from running contact-catalog-service. |
contact.lookup host capability / Contact Catalog client + Seed Directory subject lookup |
Later recipient selector kinds:
| Kind | Meaning | Resolver |
|---|---|---|
group |
Resolve a user-defined local group such as friends, family, work, or synchronizers. | Host config + policy |
agora-node |
Publish to an explicitly selected node-hosted Agora endpoint. | Agora endpoint resolver |
matrix-mailbox-node |
Leave a private artifact control message in a node mailbox room. | Matrix mailbox adapter |
capability-first and capability-many were added after the initial MVP once
the route/default/group core was stable. They remain transport-neutral
selectors: Seed Directory resolves candidates, Artifact Delivery still performs
local outbound authorization and adapter selection.
contact-lookup is deliberately a recipient selector, not an artifact/ref
resolver scheme. For the Story-010 messaging path, selector/purpose is
contact-request/messaging: the first segment names the AD use case and the
second segment names the domain being approached. This allows host policy to
permit contact lookup for contact-request.v1 while still denying full message
delivery until the receiver has issued a scoped messaging-receive passport.
Slash-separated values are acceptable for this field because they are semantic
tags, not route
paths or resolver scheme names.
Contact lookup is not the Contact Catalog synchronization channel. It performs
one recipient-resolution lookup for a delivery plan. Provider-to-provider
Contact Catalog sync belongs to the catalog/provider surface: host-authorized
control plane, direct catalog data plane, and no Agora publication path. The
host-composed AD resolver first tries the local Contact Catalog lookup
surface and may fall back to trusted remote Contact Catalog providers discovered
through Seed Directory when the local provider is unavailable. Remote fallback
is mode-aware: the provider passport must advertise the requested
lookup/mode, so a psi or blinded-digest selector is not silently routed to
an invitation-only provider. The result is only addressability: it
produces routing-subject or node delivery candidates. It does not authorize
the eventual push; INAC/AD passport and admission gates still decide whether the
artifact is accepted.
For the inbound side of the same first-contact flow, AD/INAC runs a
schema-registered contact-request.v1 preflight before the contact-request
acceptor. The hook is deliberately narrow: it can opt out by local config,
extract block candidates from the request, deny candidates that match Local
Relationship status = blocked, and attach a logical dedupe hint. It cannot
accept the artifact by itself, cannot create a relationship, and does not widen
unknown-peer admission for message-envelope.v1 or other schemas.
The daemon exposes the same Contact Catalog consumer path to the user UI through
the host contact.lookup capability, but that path is a discovery/read-model
surface rather than a delivery admission surface. It may surface a
routing-subject match whose Seed Directory binding has no fresh
endpoint/certificate evidence yet, so the operator can see that the contact is
known. Direct AD delivery to a routing-subject remains stricter: the delivery
resolver still requires usable endpoint evidence with
endpoint/certificate.advisory/route-id matching the requested routing subject.
The current capability selector filter is intentionally narrow: it supports
target/node-ids as a local allowlist/intersection filter. Issuer,
endorsement, and passport-profile filters belong to the next policy iteration;
until then the daemon accepts only Seed Directory entries whose capability
passport verifies against the configured sovereign authority set and whose
node_id / capability_id match the requested capability.
Participant and routing-subject resolution are deliberately split:
participantis for explicit public/operator disclosure. The expected Seed Directory source is a verifiednode-operator-binding.v1bundle, where the participant issued thenode-primary-operatorpassport and the node signed the corresponding acceptance.routing-subjectis the privacy-preserving contact/delivery direction. It is a delegated, scoped identity that can be indexed by Seed Directory without publishing the rootparticipant:did:key.- Both selectors resolve to concrete node candidates before transport
execution. Transport adapters still connect to
node-id; neither participant nor nym becomes a transport-layer identity. - Candidate ordering should use the local Seed Directory projection timestamp
(
accepted_at/received_at) descending, withnode_idascending as a deterministic tie-break. Remote-declared timestamps are input facts, not the ranking authority.
For nym-authored public posts, Artifact Delivery should treat contact as a separate routing problem:
- the public post may carry an optional
contact/refor equivalent hint; - that hint resolves to a
routing-subject, not to the hidden root participant; - AD then resolves
routing-subject -> node candidates, encrypts or carries the already encrypted private reply according to the artifact contract, and delivers to the selectednode-id; - if the post has no contact hint, AD must not infer a route from the nym's author identity alone.
Outbound Allow Table¶
Inbound support must not imply outbound authority. A component may accept
vendor.foo.v1 and still be forbidden from sending it.
A component can send through Artifact Delivery only when an explicit outbound allow exists:
{
"component/id": "vendor.middleware",
"schema": "vendor.foo.v1",
"recipient/selectors": ["node", "configured-default", "group", "agora-default"],
"route/refs": ["private-whisper-default"],
"target/node-ids": ["node:did:key:..."],
"artifact/ref-schemes": ["memarium-public-entry"],
"fanout/max-targets": 3,
"fallback/max-stages": 2,
"max/bytes": 262144,
"requires/capability": "artifact-delivery.send"
}
If a component needs both input and output for the same schema, the schema must appear in both the inbound acceptor table and outbound allow table.
Referenced payload schemes are also authorization surface. When an outbound
allow carries artifact/ref-schemes, Artifact Delivery checks the artifact/ref
prefix before invoking a resolver. This lets operators authorize
memarium-public-entry without also authorizing memarium-public-fact, even
though both resolvers are backed by the same Memarium runtime.
Missing transport-specific selector parameters are a fast failure with
diagnostics. A node selector without node/id should produce a malformed or
semantically invalid envelope response, while a configured-default that does
not resolve to a concrete target should produce a route/configuration conflict
or disabled route. Neither case may silently fall back to agora-default.
Policy fields are part of the audited envelope contract, but they are not a license to assume behavior the selected route cannot provide. Artifact Delivery enforces the mechanical policy subset it owns directly: selector/route allowlists, fan-out, byte caps, timeouts, retry/idempotency, and private policy adapter allowlisting. Other policy terms must either be proved at the selected route/adapter or domain acceptor boundary, or the delivery must be rejected with a clear diagnostic. Examples include retention class, revocation freshness, classification semantics, and operator-review requirements.
Route and adapter diagnostics should expose a policy/support projection rather
than leaving policy coverage implicit. The projection is descriptive, not the
source of authority: enforcement remains in route validation, outbound
authorization, adapter selection, and domain acceptor gates. Its purpose is to
let the operator see which policy fields are enforced by the core, which are
enforced by the selected adapter/domain layer, and which would fail closed as
unsupported before a delivery is accepted.
Inbound Acceptor Table¶
Inbound admission is single-owner:
{
"schema": "vendor.foo.v1",
"content/types": ["application/json"],
"component/id": "vendor.middleware",
"target/kind": "supervised-http",
"invoke/path": "/v1/artifacts/accept",
"max/bytes": 262144
}
For in-process node-attached components the same declarative row should exist, but its target is a host-composed function rather than a loopback HTTP path:
{
"schema": "memarium-blob.v1",
"component/id": "memarium",
"target/kind": "in-process",
"invoke": "memarium.inac.accept"
}
This is deliberately data-first: the operator should be able to inspect that
memarium-blob.v1 delivered through Artifact Delivery will be accepted by
Memarium even if Memarium is compiled into the host rather than running as a
supervised channel middleware.
Artifact Delivery is a host-owned runtime service composed by the daemon, not a separate supervised middleware process. The communication path to the concrete acceptor depends on component placement:
- in-process Rust components are invoked through host-composed trait objects or functions;
- supervised middleware is invoked through daemon/middleware-runtime loopback HTTP using the component endpoint, auth token, lifecycle state, and timeout known to the host;
- JSON-e or JSON-e Flow participate only behind an explicit host-composed acceptor declaration.
The artifact-delivery runtime crate owns the admission registry, idempotency,
ledger, and status model. It must not own loopback HTTP clients, supervised
process lifecycle, or middleware auth details; those belong to daemon
composition.
Before payload resolution and acceptor invocation, the runtime may run
schema-registered ArtifactAdmissionPreflight hooks. A preflight is weaker than
an acceptor: it can deny cheaply or attach non-authoritative hints, but it
cannot accept an artifact, create domain state, issue passports, create
notifications, or fetch remote artifact/ref payloads. The runtime passes only
the artifact descriptor and inline bytes that fit the hook's declared
max_input_bytes budget, plus an explicit payload availability state so hooks
can distinguish inline bytes from missing or oversized payloads. This lets
domains such as Messaging add fast block, quota, or logical-dedupe checks
without teaching AD/INAC the semantics of contact-request.v1 or
message-envelope.v1.
Before invoking an acceptor, the runtime validates the local artifact boundary:
schema and content type must be non-empty, the digest must be a non-empty
sha256:* content address, inline bytes must match both size/bytes and
digest, and non-inline artifacts must carry a non-empty artifact/ref. Inline
bytes and artifact/ref are mutually exclusive, so admission never has to infer
which payload location is authoritative. A byte-identity failure is recorded as
a receiver-local rejected admission and must not invoke the domain acceptor.
Acceptor Response Contract¶
An acceptor returns a small admission result:
{
"status": "accepted",
"artifact/ref": "memarium-public-entry:...",
"idempotency/key": "sha256:..."
}
Allowed status values:
acceptedalready-presentrejectedretryable
Artifact Delivery maps these to transport-level responses and operator-visible counters. The acceptor owns any domain workflow it starts after admission.
retryable is an observation of an unsuccessful attempt, not final admission.
An exact subsequent request may rerun validation and the acceptor. The receiver
serializes concurrent work for the same admission identity, preserves prior
attempt facts, and projects the latest result. Accepted, already-present and
rejected outcomes remain final for exact replay. A crash during domain handling
still requires the acceptor's own idempotency contract; transport serialization
does not promise exactly-once domain execution.
A deferred-operation handle proves queue admission only. Dator may mark its result delivery complete only after a successful domain delivery result, not on HTTP 202 or a handle alone. Artifact bytes and idempotency identity remain unchanged through delivery retries; no retry reexecutes the role.
JSON-e Flow may be an inbound acceptor only through an explicit declaration bound to a concrete JSON-e Flow instance/template. A valid schema alone never injects an artifact into JSON-e Flow. The operator-visible route table must show which instance accepts a given schema/content-type class.
Arca/Dator Service-Order Transport Consumer¶
The first marketplace consumer pair for Artifact Delivery is Arca/Dator remote service-order execution:
- Arca sends private
service-order.dispatch.request.v1artifacts throughartifact.delivery.send?mode=deferredand records the deferred operation, status href, audit reference, request id, and correlation id on the workflow step. - Dator owns the single
service-order.dispatch.request.v1supervised acceptor, admits and deduplicates byrequest_idplus buyer/order identity, and starts provider-side execution only after admission gates pass. - Dator sends terminal
service-order.result.v1artifacts back to the request's domainreply/target. - Arca owns the single
service-order.result.v1supervised acceptor, correlates by(workflow/run-id, workflow/phase-id, request_id), treats identical redelivery asalready-present, rejects conflicting result digests, and closes the workflow step exactly once.
The provenance-bearing profile additionally admits service-order.result.v2
in Dator's outbound rule and Arca's acceptor allowlist. It preserves the exact
committed bytes and delegates independent inference-policy evaluation to the
buyer host. P090-008c proves this result path over real local WSS/AD with
supervised roles, restart and one paid release; its deterministic inference
server and explicit input/workflow preconditions are not physical federation
or end-to-end workflow-runner acceptance. External descriptor resolution remains
outside that profile.
Status: the direct node-to-node, inline-JSON thin slice is implemented in the
Node reference Arca and Dator modules. Private-safe staged fallback
(matrix-mailbox / object-store-indirect) and object-store-indirect result
payloads for this specific service-order path remain later hardening layers.
The older peer-message service-order dispatch path remains as a compatibility
fallback, not as the source of new transport semantics.
Implementation Guidance¶
The implementation should be stratified into four layers:
- Pure core: synchronous, side-effect-free DTOs, validation, normalization, recipient resolution traits, target deduplication, outbound authorization, dispatch-plan construction, and stable failure classes. This layer must not depend on networking, SQLite, the daemon, Tokio, Agora, INAC, Memarium, or the system clock.
- Runtime: adapter registries, inbound acceptor registries, durable delivery and admission ledgers, status lookup, retries, deadlines, bounded stage execution, idempotency, and error classification. This layer exposes acceptor traits but remains transport- and lifecycle-agnostic. Runtime deadlines are cooperative for synchronous adapters: every transport adapter must set its own I/O timeout or explicitly honor the deadline passed through the bounded work context.
- Host-service composition:
ad-hostowns narrow edge implementations for INAC, Agora publish, Matrix mailbox delivery, supervised channel acceptors, in-process acceptors, explicitly configured JSON-e Flow acceptors, recovery workers, and AD-owned transport caches. It receives daemon-owned effects through consumer-side traits instead of importing daemon types. - Daemon composition: effective config loading, module-report ingestion, readiness diagnostics, host capability exposure, operator APIs, UI, and concrete shims for middleware, Memarium, contact-request, Matrix sealing, object-store, peer, and INAC effects.
The pure core pipeline should be explicit:
validate envelope
-> expand route/default/group plan
-> resolve targets
-> deduplicate by canonical target key
-> authorize outbound plan
-> build canonical dispatch plan
Failure classes are part of the wire/ledger contract and should not be derived from internal error strings. The initial stable set is:
envelope-malformedenvelope-invalidroute-unresolvedadmission-conflictkind-not-supportedoutbound-deniedadapter-transientadapter-permanentstage-timeoutadmission-timeoutledger-error
Transport adapters are selected by exact adapter_scheme from each resolved
target, not by a soft can_deliver(target) probe. An adapter receives one
DispatchTarget plus one shared immutable artifact reference. It must not see
the whole dispatch plan and must not decide fallback order.
Inline payload fan-out should avoid copying large byte arrays. A simple
Arc<[u8]>-style shared byte value is sufficient for the first implementation;
the protocol does not need a separate bytes abstraction just for fan-out.
The runtime must persist the accepted submission and canonical dispatch plan
before invoking the first transport adapter. The dispatch plan should be stored
as canonical JSON using the project-wide JCS profile. A deterministic
delivery/id should be computed from a domain-separated, length-framed tuple of
sender-local fields, such as component id, artifact digest, normalized delivery
plan digest, and idempotency key. The receiver must not treat this sender-local
delivery/id as its admission identity; inbound admission needs its own
receiver-local admission/id and idempotency key.
The durable ledger should live in its own SQLite file under the node data dir and record at least:
- submissions and canonical expanded plans;
- per-stage and per-target outcomes;
- inbound admissions;
- route and acceptor conflicts;
- timestamps, deadlines, and retry counters.
SQLite setup should follow existing node storage practice: explicit migrations, WAL where appropriate, busy timeout, foreign keys, and transaction boundaries around state transitions.
Effective config validation should fail readiness for unknown route refs, unresolved configured defaults, empty groups, recursive groups, invalid quorum, missing adapter schemes, and conflicting inbound acceptors.
Multiple Dispatch Rule¶
Multiple authoritative acceptors for the same schema/content-type class are not allowed in the MVP.
Startup/readiness behavior:
- no acceptor: inbound request returns
kind-not-supported; - one acceptor: route is active;
- more than one acceptor: readiness conflict, route disabled until resolved.
If a domain genuinely needs fan-out, it should register one explicit router acceptor. That router then owns the domain fan-out semantics after Artifact Delivery admission.
Relationship to Middleware input_chains¶
Artifact Delivery should reuse the useful parts of the module report idiom:
- component declares capabilities as data;
- declarations include invoke paths and limits;
- host compiles the effective route table at startup;
- conflicts are operator-visible before first request.
It should not reuse chain semantics. Artifact Delivery admission is not an
input_chains pass, not a fan-out, and not a general middleware pipeline.
Preferred naming:
inbound_acceptors
outbound_allows
Avoid naming this surface handles_artifact_kinds, because handles suggests a
chain or general workflow. acceptor says this is an admission boundary.
JSON-e and JSON-e Flow¶
JSON-e and JSON-e Flow are not automatic Artifact Delivery consumers.
They may participate only when explicitly configured as an acceptor target or when a domain component uses them internally after accepting an artifact. A valid JSON schema alone must never cause an artifact to be injected into JSON-e Flow.
Valid models:
artifact schema X -> DomainComponent acceptor -> DomainComponent may run JSON-e Flow
or, explicitly:
artifact schema X -> configured json-e-flow acceptor template T
The second model requires an operator-visible declaration and normal outbound or inbound capability checks.
Relationship to INAC¶
INAC is the private/direct transport adapter under Artifact Delivery.
It owns:
- WSS peer session use;
- Matrix mailbox transport use;
- INAC control messages such as offer/request/push;
- session-scoped stream chunks when payloads exceed the inline ceiling;
- transport-level retries, peer budgets, and transport diagnostics.
It does not own:
- component-facing recipient selector semantics;
- capability-first or participant recipient resolution;
- outbound schema permissions;
- the global inbound acceptor table;
- domain workflow after an artifact is admitted.
The AD-to-INAC path and the direct INAC host capability path have separate
authorization gates in the current implementation. Artifact Delivery outbound
allowlists authorize components to use an AD route that happens to resolve to
inac-direct; INAC outbound allowlists authorize direct inac.* host
capability calls. This prevents a component that may use one surface from
implicitly gaining authority on the other.
Relationship to Agora¶
Agora is not a private artifact courier. It is the durable public/federated record substrate.
Artifact Delivery may route an outbound envelope to Agora only when the
recipient selector is explicitly Agora-shaped, for example agora-default or
agora-node, and the artifact is an acceptable Agora publication artifact. This
path goes through Agora's own publish/admission policy.
Do not silently replace a private node delivery request with an Agora publish.
For privacy = private | private-direct, Artifact Delivery denies every adapter
scheme except those explicitly marked private-safe by the host runtime. Today
that means inac-direct, matrix-mailbox, and object-store-indirect; new
adapters are denied under private policy until reviewed. Guarded INAC/private
routes also require a first-class classification label and reject missing or
over-restrictive labels before transport dispatch.
Relationship to Seed Directory¶
Seed Directory is a recipient resolver input, not a transport.
Recipient selectors such as capability-first and capability-many may use Seed
Directory to find candidate nodes, but Artifact Delivery still owns the local
policy decision that selects a concrete target and transport.
participant and routing-subject selectors follow the same boundary.
Seed Directory provides projections such as public operator
participant-id -> node candidates and privacy-preserving
routing-subject-id -> node candidates, but AD remains responsible for outbound
authorization, route selection, deduplication, and adapter execution. The
privacy default is not to publish root participant-id -> node-id; that mapping
is reserved for explicit public/operator disclosure.
The daemon-side lookup treats remote Seed Directory responses as untrusted
input: response bodies are size-limited before JSON deserialization, entries are
verified against capability passport rules, and only matching node_id /
capability_id pairs are returned to AD.
The same daemon-owned Seed Directory query policy applies to AD capability,
participant, routing-subject, org-custodian, and Contact Catalog provider
discovery: preferred-directory, quorum, or weighted-trust selects discovery
sources, while AD still owns route policy and INAC/passport authorization.
Deferred lookup refinements are tracked in the implementation notes rather than treated as current contract bugs: short negative caching for empty capability results, endpoint-level retry/backoff, a shared blocking HTTP client, and query-level result limits. These should be added only with explicit cache TTL, diagnostic, and partial-result semantics.
Relationship to Memarium¶
Memarium may be an inbound acceptor for memarium-blob.v1 and a custody target
for domains that explicitly choose custody. Artifact Delivery must not decide on
its own to store arbitrary unknown artifacts in Memarium. Opaque storage is only
valid when the envelope kind itself is the contract, such as memarium-blob.v1,
or when a configured custody acceptor explicitly owns that policy.
Relationship to Local Relationship Layer¶
Selector kind = "group" is resolved against Solution 032 (Local
Relationship Layer). Artifact Delivery calls
local-relationship.group.resolve(group_id) and receives a list of
ResolvedRelationshipCandidate records carrying contact/ref,
class/id, relationship/fact-id, and optional route hints. AD does
not know what friends or contacts mean — it only consumes resolved
candidates and applies its own passport/capability checks against each.
Group resolution is candidate selection, never authorization.
Relationship membership is one input to AD policy; the actual delivery
authority still flows through passports, outbound allows, and admission
gates. AD inbound acceptors may additionally require relationship-policy
predicates for autonomous custody / acceptance decisions; the predicate
evaluator returns a relationship-policy-decision.v1 that AD treats as
one decision input alongside its existing checks.
Where Recipient Selector tables and examples in this document mention
groups like friends as illustrative names, the actual class semantics
live in Solution 032; this document references them but does not own
them.
Must Implement¶
Host-Owned Artifact Delivery Runtime¶
Based on:
doc/project/40-proposals/042-inter-node-artifact-channel.mddoc/project/60-solutions/017-inter-node-artifact-channel/017-inter-node-artifact-channel.mddoc/project/60-solutions/019-middleware/019-middleware.md
Responsibilities:
- expose one component-facing send surface;
- expose
artifact.delivery.sendas the component-facing host capability; - keep transport connections centralized in the host;
- compile route tables from factory config, effective config, in-process manifests, and middleware module reports;
- return delivery ids for accepted asynchronous deliveries and expose delivery status lookup;
- keep pure validation, normalization, recipient resolution, target deduplication, and outbound authorization in a synchronous side-effect-free core layer;
- persist accepted submissions before transport execution;
- expose route table and conflict state to the operator.
Status:
mvp-implemented: the Node workspace now hasartifact-delivery-core,artifact-delivery, JSON schemas, schema-gate coverage, daemon configuration,artifact.delivery.send, route/status operator APIs, deterministic delivery ids, in-memory test ledger, SQLite-backed runtime ledger, the Agora publish adapter, a local INAC direct short-circuit adapter, inbound acceptor registry conflict detection, receiver-local inbound admission ledger, admission idempotency, admission status APIs, bounded transport retry/deadline execution throughbounded-work-runtime, P055 deferred submit, P055 operation-status endpoint, manual recovery pass withretry/historyandartifact-delivery-recovery.v1, daemon background recovery enabled by default, supervised channel and in-processinac.pushacceptor adapters, explicit pure JSON-e Flow acceptors,artifact-store:referenced payload resolution, remote INAC WSS peer transport feeding the sharedPOST /v1/artifact-delivery/admissionspath, capability-first/many recipient resolution through the daemon's Seed Directory capability lookup, participant/routing-subject recipient resolution through Seed Directory projections, Story-005 public Whisper viaagora-default, fail-closed org recipient resolution through configured org custodians plus Seed Directory participant projections, public and capability-gated scoped Memarium referenced payload resolution,agora-record:<record-id>referenced payload resolution with allowlist and digest validation, mechanical delivery-policy enforcement including private-to-Agora rejection, Story-005 private/direct Whisper viainac-direct, a full three-daemon Story-005 AD observability smoke that asserts A/B are thin Agora clients publishing to node C rather than running a localagora-service, large private/direct payload streaming over the existing WSS peer-message session withinac.stream.chunk.binary.v1preferred and JSON/base64url chunks retained as compatibility fallback, Matrix mailbox store-and-forward with operator-visible worker metrics and bounded peer artifact cache limits,inac-peer-artifact:peer artifact resolution, Matrix mailbox room lifecycle repair, larger-object mailbox event chunking, and regression tests. INAC WSS now provides invitation, genericinac-push@v1, andmemarium-custody@v1passport authorization before AD inbound admission. The baselinememarium-blob.v1acceptor requires explicitsignature.key/public, rejects plaintext custody, and records accepted custody facts in the daemon-local target space selected byartifact_delivery_acceptors.memarium_blob_custody. The default target space remainspublic;personalandcommunitymay be allowed by local config;crisisis rejected until a separate crisis-custody contract exists.
Outbound Authorization and Recipient Resolution¶
Based on:
doc/project/60-solutions/006-capability-binding/006-capability-binding.mddoc/project/60-solutions/007-capability-advertisement/007-capability-advertisement.mddoc/project/60-solutions/021-agora-authority/021-agora-authority.md
Responsibilities:
- require explicit outbound schema declarations per component;
- normalize single-recipient envelopes into
delivery/plan; - resolve named route refs plus
node,configured-default, andagora-defaultrecipient selectors for the MVP; - allow one stage to mix explicit targets, configured defaults, groups, and later capability resolver targets;
- deduplicate concrete targets after full resolution and before transport execution;
- treat selector allowlists as permissions, not fallback order;
- fail malformed or unresolved selector targets before transport execution;
- execute staged delivery plans with
all,any, andquorumsuccess policies; - support configured fallback such as direct node first, then Agora or a configured group;
- authorize route refs, selector classes, concrete target node ids, fan-out limits, and fallback depth per component;
- keep route ids stable, carry route versions inside route definitions, and persist expanded normalized plans in the ledger;
- resolve capability-first/many, participant, routing-subject, org, and contact-lookup selectors through host-composed resolver adapters;
- deny valid-schema sends when the component lacks an outbound allow.
Status:
mvp-implemented: explicit component/schema outbound allowlists, route refs, configured defaults, groups,node,agora-default, fan-out limits, fallback-depth limits, runtime stage target caps, deterministic dedupe, content digest validation, route-plan validation, andall/any/quorumstage evaluation are implemented in the pure core/runtime split. Capability (capability-first,capability-many), subject (participant,routing-subject), organization (org), and contact-lookup selectors are also implemented as host-composed resolver layers: the core crate owns typed selector DTOs and lookup traits, the runtime accepts injected lookup adapters, and the daemon composes those adapters from Seed Directory projections, configured organization custodian policy, Contact Catalog lookup, endpoint evidence, and local outbound allow rules. These resolver layers are part of the current MVP implementation, not deferred post-MVP work.
Single-Owner Inbound Admission¶
Based on:
doc/project/60-solutions/017-inter-node-artifact-channel/017-inter-node-artifact-channel.mddoc/project/60-solutions/019-middleware/019-middleware.md
Responsibilities:
- enforce at most one authoritative acceptor per schema/content-type class;
- fail readiness on conflicting acceptors;
- return
kind-not-supportedwhen no acceptor is available; - persist receiver-local admission/refusal records with deterministic
admission/idand idempotent replay behavior; - declare supervised channel and in-process acceptors in the same effective route-table shape;
- call supervised channel, in-process, or explicitly configured JSON-e Flow acceptors through the same conceptual admission contract;
- require JSON-e Flow acceptors to be bound to explicit operator-visible instances/templates;
- record receiver-local admission ids separately from sender-local delivery ids.
Status:
mvp-foundation-implemented: the runtime has an inbound acceptor trait, single-owner registry conflict detection, exact and wildcard content-type lookup, deterministic receiver-localadmission/idgeneration, a persistent admission/refusal ledger, idempotent replay of already-recorded admissions, pre-acceptor inline byte-identity checks, route snapshots that expose registered acceptors, read APIs for recent admissions and admission detail, operator UI coverage, and a daemon-ownedPOST /v1/artifact-delivery/admissionsingress for explicitly allowlisted control-plane transport adapters. Concrete host/component acceptor adapters remain outside the pure runtime:ad-hostcurrently composes supervised channel middleware acceptors, in-processinac.push,agora-record.v1,memarium-blob.v1,contact-request.v1acceptors, and explicit pure JSON-e Flow acceptors, while the pure Artifact Delivery runtime still owns no loopback HTTP client, supervised process lifecycle, or middleware auth logic. The daemon supplies the concrete effect shims; it no longer owns the AD acceptor implementations.
Transport Adapter Registry¶
Based on:
doc/project/60-solutions/017-inter-node-artifact-channel/017-inter-node-artifact-channel.mddoc/project/60-solutions/008-agora/008-agora.md
Responsibilities:
- register INAC as the MVP private/direct node transport;
- register Agora publish as the MVP public/federated publication adapter;
- keep Matrix mailbox support behind the same adapter boundary;
- select adapters by exact adapter scheme from resolved targets;
- pass only one dispatch target and one shared immutable artifact reference to each adapter, keeping fallback decisions in the runtime;
- ensure transport adapters feed inbound admission instead of owning their own domain-specific dispatch tables.
Status:
mvp-foundation-implemented: the runtime has an exact adapter-scheme registry, conflict detection, route/status exposure, the productionagora-defaultpublish adapter, and aninac-directadapter that uses a local short-circuit for local targets and authenticated WSS peer messages for remote direct-node targets. Remote WSS INAC push frames feed the shared Artifact Delivery inbound admission path. Matrix mailbox is now present as the first explicit store-and-forward fallback adapter and feeds the same inbound admission path after unsealing and revalidation.object-store-indirectis enabled for the hard-MVP path: the sender writes payload bytes into the daemon object store, emits a smallartifact-object-pointer.v1through sealed Matrix mailbox control, keeps the fetch token in sealed metadata only, and the receiver fetches, verifies, rehydrates, and admits the original artifact rather than the pointer.
Implemented and Optional Extensions¶
Capability-Based Recipient Resolver¶
Based on:
doc/project/40-proposals/025-seed-directory-as-capability-catalog.mddoc/project/60-solutions/007-capability-advertisement/007-capability-advertisement.md
Responsibilities:
- resolve
capability-firstandcapability-manyrecipient selectors through Seed Directory and local policy; - preserve revocation freshness and capability profile checks;
- expose candidate selection diagnostics.
Status:
implemented:artifact-delivery-coreowns the transport-neutral selector DTOs andCapabilityNodeLookuptrait; the daemon composes a Seed Directory-backed lookup that reuses the existing capability discovery cache, verifies capability passports, applies local filters, and returns concrete node targets to Artifact Delivery. The blocking Seed Directory fetch path also enforces a bounded response body before deserialization.
Subject-Based Recipient Resolver¶
Based on:
doc/project/20-memos/nym-layer-roadmap-and-revocable-anonymity.mddoc/project/40-proposals/025-seed-directory-as-capability-catalog.md
Responsibilities:
- resolve public/operator
participantselectors only when the operator chose explicit disclosure, typically via a publishednode-operator-binding.v1; - resolve
routing-subjectselectors as scoped contact/delivery identities without requiring root participant disclosure; - support nym reply/contact flows where a public nym-authored artifact carries an optional contact hint that resolves to a routing subject;
- preserve the invariant that transport still routes to
node-id, notnymor participant identity; - expose ordering and privacy diagnostics to the operator.
Status:
implemented:artifact-delivery-coreownsParticipantNodeLookup,RoutingSubjectNodeLookup, and subject-aware recipient resolution. The daemon composes those lookups with the same Seed Directory query path used for capability discovery. Seed Directory exposesGET /participant/{participant-id}from acceptednode-operator-binding.v1entries andGET /routing-subject/{routing-subject-id}from acceptedrouting-subject-binding.v1entries. Both projections sort candidates by local accepted/received time descending and then bynode_idfor stable results. Participant and routing-subject selectors acceptmax/nodes; the daemon caches positive subject lookup results with the same short TTL class as capability lookup. Public routing-subject reads return onlypublic-unlinkedentries.
Organization Custodian Recipient Resolver¶
Based on:
doc/project/40-proposals/017-organization-subjects-and-org-did-key.mddoc/project/40-proposals/025-seed-directory-as-capability-catalog.md
Responsibilities:
- resolve
orgrecipient selectors through explicit host policy, not through a public globalorg -> nodedirectory; - map an organization id to configured participant custodians under local operator policy;
- reuse Seed Directory participant projections to turn those custodians into reachable node candidates;
- fail closed when the organization has no configured custodians, the custodian list is empty, or no custodian node has usable endpoint evidence;
- keep organization policy/evidence metadata in the resolved target so the ledger can explain why that node was selected.
Status:
implemented:artifact-delivery-coreownsOrgNodeLookup,ResolvedOrgNode,RecipientSelector::Org, selector-class authorization, validation, target conversion, and unit coverage for org provenance. The daemon composesOrgNodeLookupfromorganization_custodians, Seed Directory participant candidate lookup, and endpoint certificate evidence. The resolver skips custodian candidates without usable endpoint evidence and returns concrete AD targets withorg/id, policy ref, evidence digest, route kind, endpoint URL, and certificate pin metadata.
Matrix Mailbox Transport Adapter¶
Based on:
doc/project/60-solutions/008-agora/008-agora.mddoc/project/60-solutions/017-inter-node-artifact-channel/017-inter-node-artifact-channel.md
Responsibilities:
- use deterministic node-mailbox room aliases for asynchronous artifact control messages;
- treat Matrix as transport only, never as trust source;
- act as the first store-and-forward fallback transport for private delivery;
- require the same envelope/INAC authorization/passport proof that WSS would require; receiving a Matrix event is never authority by itself;
- seal plaintext/JSON payloads by default into an
artifact-mailbox-sealed.v1transport envelope encrypted to the recipient key before posting to Matrix; - split sealed mailbox payloads into
artifact-mailbox-chunk.v1events when a single Matrix event would exceed the configured event byte limit; - keep the current implementation fail-closed by always sealing the
inac-control.v1frame before posting to Matrix. A future already-encrypted opaque-custody bypass would require an explicit route policy and a separate review; - unseal on the receiver, then revalidate the original artifact descriptor
(
size/bytes,sha256:*, schema, content type) before normal AD/INAC admission; - feed the same Artifact Delivery inbound admission path used by INAC WSS.
- maintain deterministic recipient mailbox room lifecycle through idempotent room ensure and send-time repair.
Status:
implemented: Matrix mailbox is disabled by default and explicit-route only. It seals plaintext/JSON payloads asartifact-mailbox-sealed.v1, chunks oversized sealed payloads asartifact-mailbox-chunk.v1, validates chunks before unseal/admission, maintains deterministic room ensure/repair, and exposes room/chunk/worker counters through AD status/UI.
Shared Exclusive Route Registry Primitive¶
Based on:
doc/project/60-solutions/019-middleware/019-middleware.md
Responsibilities:
- extract a small generic registry only after Artifact Delivery proves that the same exclusive route-table primitive is needed elsewhere;
- preserve simple domain-specific data shapes until reuse is real.
Status:
optional
Out of Scope¶
- Replacing INAC, Agora, Seed Directory, or Memarium.
- Becoming a general workflow engine.
- Running middleware-owned peer listeners or transport loops.
- Fan-out to multiple authoritative acceptors for one artifact kind.
- Treating valid schema as sufficient send authority.
- Silent opaque storage of unknown artifact kinds.
- Public enumeration of private/direct deliveries.
- Domain interpretation of accepted artifacts.
Failure Modes and Mitigations¶
| Failure mode | Mitigation |
|---|---|
| Artifact Delivery becomes a second middleware chain | Enforce single authoritative acceptor per schema/content-type class. |
| Components bypass host policy by opening their own transports | Keep transports host-owned and grant components only a send capability. |
| Inbound support accidentally grants outbound authority | Maintain separate inbound_acceptors and outbound_allows. |
| A component smuggles a broad fan-out through a named route | Authorize route refs, selector classes, concrete target node ids, maximum targets, and maximum fallback stages per component. |
| Missing node target silently falls back to Agora | Treat missing transport-specific selector parameters as malformed or unresolved target errors; require an explicit delivery/plan stage for Agora fallback. |
| The same target is delivered twice because it appears both explicitly and through a default/group/capability result | Resolve all selectors to canonical target keys and deduplicate before fan-out limit enforcement and transport execution. |
| Group delivery semantics are ambiguous | Put success/policy on the stage after group expansion: all, any, or quorum. |
| Two modules claim the same artifact kind | Fail readiness and expose conflict diagnostics. |
| Agora is used as an accidental private delivery channel | Require explicit Agora-shaped recipient selectors and Agora publish policy. |
| Seed Directory lookup hides policy decisions | Keep Seed Directory as resolver input; local Artifact Delivery policy selects targets. |
| JSON-e Flow becomes an implicit generic artifact consumer | Allow JSON-e Flow only behind explicit acceptor declarations. |
| Early generic abstraction becomes too large | Start with Artifact Delivery-specific route tables; extract a generic primitive only after reuse is proven. |
Resolved Implementation Decisions¶
- The component-facing host capability is
artifact.delivery.send. - Delivery may complete synchronously for fast plans, but the runtime has a
ledger from the beginning and may return
202 acceptedfor longer plans. The response carries adelivery/id, and the sender can query status by that id. - MVP group use is route-driven. Components should use
route/reffor operator-owned group routes; inline groups can remain test-only or later surface area. - MVP user-defined groups are static local config groups only.
agora-defaultaccepts already-formedagora-record.v1first. Host-side wrapping of arbitrary domain payloads is a later explicit adapter, not hidden behavior.- Concrete
target/node-idsin outbound allows are optional. If present, they restrict the resolved concrete node targets. - Supervised middleware module reports may request outbound allows, but only effective host config can approve them.
- JSON-e Flow can be an inbound acceptor only through explicit instance/template-bound acceptor configuration. The MVP in-process adapter is pure; flows with host capability calls are rejected at daemon startup.
- Multiple adapters may support the same selector class, but adapter selection
must be explicit in route config or deterministic by priority. The default
direct
nodeadapter is INAC/WSS; Matrix mailbox is an explicit store-and-forward fallback stage. - Route refs are stable names; route versions live inside route definitions, and the ledger stores the expanded normalized plan used by each delivery.
- Capability routing entered after the initial AD MVP as a host-composed
resolver layer.
capability-firstandcapability-manyare recipient selectors, not transport adapters: Seed Directory and local policy resolve candidates, then AD still performs outbound authorization, deduplication, stage planning, and adapter selection. Staticagora-defaultremains valid when the operator wants a fixed local/federated Agora route. - The delivery ledger is required even for synchronous successes.
- Missing transport-specific selector parameters, such as
nodewithoutnode/id, fail validation/resolution and never imply fallback to Agora. - In-process acceptors and supervised channel acceptors are declared in the same effective config/route table shape so operator visibility does not depend on runtime placement.
Resolved Implementation Decisions¶
- Observer hooks are allowed only as read-only, metadata-only observers outside the authoritative acceptor path. Observer failures are logged and counted but never change the delivery or admission result.
- Lower-level WSS zero-copy is not implemented without profiling evidence. The
runtime records materialization counts, materialized bytes, materialization
time, source class (
inline/file-backed), stream chunk counts and transport payload bytes so the next optimization decision is data-led. - Generic object-store indirect delivery is implemented for the hard-MVP path: HTTP fetch is token-bound, ref-bound, TTL-bound and digest/size verified; Matrix mailbox remains the sealed control carrier for the pointer and token metadata.
Next Actions¶
- Use profiling counters from operator/status snapshots to decide whether a lower-level zero-copy WebSocket frame split is worth adding beyond the authenticated application-frame carrier.
- Keep Matrix media deferred; prefer the generic object-store path for payloads that outgrow the current Matrix mailbox sealed chunk carrier.
Related Capability Data¶
023-artifact-delivery-caps.edn