Solution 042: Inter-Node Artifact Channel (INAC) — Implementation Guidelines¶
Proposal: doc/project/40-proposals/042-inter-node-artifact-channel.md
Related impl notes:
doc/project/60-solutions/008-agora/008-agora-topic-addressed-relay-impl.md(sister surface — topic-addressed publication)doc/project/60-solutions/011-whisper/011-whisper-impl.md(first named consumer)
Purpose of this document¶
Implementation entry point for INAC — the peer-to-peer, capability-gated, three-operation channel for exchanging byte-identical artefacts between two Orbiplex nodes.
This note does not:
- duplicate the proposal (proposal 042 is authoritative on semantics),
- enumerate every consumer's wiring (consumer-side impl lives in
the respective consumer's
60-solutions/NNN-*-impl.md), - track fine-grained tasks (that lives in the sibling
noderepository alongside the transport and peer-dispatch notes).
It exists to:
- map proposal 042's decisions to layers and crates,
- fix the invariants that cross layers (envelope byte-identity, authorization sources, attestation-gate reuse, open kind registry),
- track current status per layer,
- enumerate open decisions that block coherent implementation,
- give a stable commit order.
Architectural posture¶
INAC is a peer-level artefact exchange protocol, layered above
the WSS peer transport (proposal 002) and parallel to the peer
message dispatch pipeline (proposal 027). In the component-facing
architecture it is a private/direct transport adapter under
doc/project/60-solutions/023-artifact-delivery/023-artifact-delivery.md.
It is not a general component send API, not a new identity system, and not a
new authorization authority.
Three operations — offer, request, push — travel as peer
messages with a dedicated msg kind inac.v1. Large payloads
travel as session-scoped stream chunks on the same WSS session.
Authorization flows through the attestation-gate (proposal 041) using one of four
sources: general capability passport, custody passport, invitation
passport, or attestation alone.
Artifact-kind admission is owned by Artifact Delivery, not by a middleware chain. INAC receives or transfers byte-identical artifacts and then feeds the host-owned inbound admission path. That path has a single authoritative acceptor per artifact schema/content-type class. Additional artifact kinds plug in as explicit inbound acceptors, not as fan-out chains.
The first remote WSS implementation is deliberately fail-closed: authenticated
peer session establishment is not sufficient authority to send AD-bound INAC
frames. The receiver applies a host-owned allowlist
(artifact_delivery_adapters.inac_peer_transport.inbound_allowed_peers) before
answering offer or accepting push. An empty list means deny-all. This is a
minimal production guard until the full invitation/passport/revocation freshness
gate is wired.
Matrix mailbox is the first asynchronous store-and-forward transport adapter,
not an INAC authority system. A mailbox event must carry the same envelope/INAC
authorization proof that WSS would require. Plaintext/JSON payloads are sealed
before posting as artifact-mailbox-sealed.v1 encrypted to the recipient key;
oversized sealed payloads are split into artifact-mailbox-chunk.v1 events in
the same deterministic mailbox room. The receiver reassembles chunks only after
TTL, count, size, duplicate, per-chunk digest, and total sealed digest
validation, unseals the envelope, then validates the original descriptor
(size/bytes, sha256:*, schema, content type) before the ordinary Artifact
Delivery inbound admission path. Deterministic mailbox room ensure/repair is a
transport reliability concern and does not add authority.
Invariants that cross layers¶
-
Envelope byte-identity. Whatever an INAC peer receives and accepts, it stores byte-identically. The envelope's signature and
record/id(foragora-record.v1) orblob/id(formemarium-blob.v1) remain valid without re-signing or canonicalization differences across distribution surfaces. -
One attestation-gate, shared with Agora. INAC does not run a parallel verifier. It consumes the same attestation-gate primitive (proposal 041 §9) and shares the
PassportResolutionCache. Any divergence means the security properties drift. -
One passport format. All three authorization-bearing paths (general capability, custody, invitation) are
capability-passport.v1instances distinguished bycapability_id. No parallel credential class. -
Control plane is cheap; payload is separate. Control messages (
offer,request,push, responses) stay small and auditable. Payloads above the configured inline ceiling travel as binary frames, content-addressed through asha256:…reference. The control plane is not where bytes accumulate. -
Signing domains are per-envelope.
agora-record.v1is signed inagora.record.v1;memarium-blob.v1is signed inmemarium.blob.v1. INAC itself has no signing domain; its control messages ride the peer session's authentication. -
No public observability. INAC has no topic index, no public enumeration, no per-topic digest. The only observability is each endpoint's Memarium. Tombstones (proposal 040) do not apply — there is no shared substrate to "forget on behalf of".
-
Open kind registry with a safety floor. Unknown kinds MUST be refused with
kind-not-supported.memarium-blob.v1is the explicit exception: opaque blobs are allowed because the blob envelope is itself the contract.
Layer 0 — JSON Schemas¶
| Schema | Role | State |
|---|---|---|
memarium-blob.v1 |
generic Memarium-native artefact envelope (content-addressed blob/id, domain memarium.blob.v1) |
✅ schema exists and is synchronized into node schema-gate |
inac-control.v1 |
control-message envelope for offer/request/push operations + response frames |
✅ schema exists; local runtime scaffold validates it; push requires exactly one payload location |
agora-record.v1 |
reused from proposal 035 | ✅ exists |
capability-passport.v1 (inac.invitation, generic inac-push@v1, and memarium.custody) |
authorization artifact for invitation, general push, and custody push | ✅ receiver-side WSS push verifies all three passport paths through the shared capability-binding gate |
Layer 1 — Peer-message kind registration¶
INAC operations are peer messages under the PeerMessageChain
(proposal 027).
| Item | State |
|---|---|
Register inac.v1 as a msg kind in the reference peer-message registry |
✅ implemented for WSS peer sessions |
Document the message envelope under peer-message-invoke.v1 forwarding rules |
❌ not started |
| Feed received artifacts into Artifact Delivery inbound admission | ✅ implemented for WSS push frames |
| Project middleware and in-process component acceptor declarations into the Artifact Delivery route table | 🟡 partial — daemon config supports supervised HTTP, in-process, and pure JSON-e Flow Artifact Delivery acceptors; projection from module reports remains later |
Layer 2 — Control protocol¶
Three operations + response frames.
Operations¶
| Operation | Request fields | Response variants |
|---|---|---|
offer |
op, artifact/schema, artifact/id, artifact/content-type?, artifact/size-bytes, reason?, invitation-ref? |
accept, decline { reason }, defer { retry-after-secs } |
request |
op, artifact/id or filter, max-artifacts?, max-bytes?, capability-passport-ref? |
push (payload), decline { reason }, partial { reason } |
push |
op, artifact (full envelope), authorization (custody-passport | invitation-token | own-artifact | attestation-only), correlation-ref? |
ingested, already-present, refused { reason } |
Refusal-code vocabulary¶
Shared with proposal 041 §6 refusal table, extended with INAC-specific codes:
kind-not-supported,encryption-mismatch,content-type-blocked,storage-full,invitation-unknown,invitation-expired,invitation-revoked,invitation-scope-mismatch,- plus all proposal 041 §6 codes (
attestation-insufficient,passport-expired,passport-revoked,nym-certificate-*, etc.).
Registration of these codes in proposal 041 §6 is 042 Follow-Up #12.
Status¶
| Component | State |
|---|---|
Control-message schemas (inac-control.v1 envelopes for each op + response frame) |
✅ implemented for the local MVP scaffold |
| Refusal-code vocabulary extension in proposal 041 §6 | ❌ not started |
Query/filter grammar for request (reuse of proposal 035 §8 selectors where possible) |
❌ not started |
Response metadata is intentionally not a cross-node round-trip contract in the
current implementation. The WSS peer handler terminates inbound meta at the
node boundary and emits response frames without copying caller-provided metadata.
Middleware-visible semantics must not depend on implicit meta propagation.
Layer 3 — Streamed payload transfer¶
Large payloads travel on the same WSS session, tagged with a
session-scoped stream-id. The current implementation uses peer-message
frames with msg = "inac.stream.chunk.v1" carried by the existing WSS binary
envelope. Senders prefer the inac.stream.chunk.binary.v1 payload shape, which
adds offset, size, final flag, artifact ref, and per-chunk sha256; the older
JSON/base64url chunk shape remains a compatibility fallback. In the current
runtime this is still an application-frame JSON payload with bytes/base64url;
binary names the byte-level validation contract, not a raw WebSocket frame.
A future lower level zero-copy WebSocket split can replace only the carrier
plumbing, not the INAC/AD contract.
Rules¶
- the final
pushcarriestransfer.mode = "stream"andartifact/ref = "inac-stream:<stream-id>", - chunks carry
stream/id,chunk/index, final flag, descriptor snapshot, artifact ref, chunk size, chunk offset, optional per-chunk digest, and chunk bytes, - receiver reassembles → recomputes
sha256→ MUST refuse withdigest-mismatchon mismatch, - interrupted or malformed streams do not invoke Artifact Delivery admission or any domain acceptor,
- inline base64 allowed only below the configured ceiling (proposed default 64 KiB, per-node / per-peer / per-kind configurable).
Placement¶
The stream chunk discipline is a transport concern. INAC consumes it and
Artifact Delivery decides when to use it for inac-direct; domain acceptors
only see a byte-identical artifact after size/digest verification.
Status¶
| Component | State |
|---|---|
| WSS peer-message stream carrier | ✅ implemented as inac.stream.chunk.v1 |
| Preferred binary chunk payload shape | ✅ implemented as inac.stream.chunk.binary.v1 with JSON/base64url fallback |
| Sender-side stream serializer | ✅ implemented for AD inac-direct |
| Receiver-side reassembler + hash verifier | ✅ implemented under <data-dir>/storage/artifact-delivery/streams/ |
| Abort frame handling | ❌ not started; malformed/interrupted streams fail closed and are cleaned at startup |
| Per-peer / per-kind inline-ceiling configuration surface | ❌ not started |
artifact/ref and artifact/href are valid control-plane payload locations,
but they are not enough by themselves. A production route must also define a
resolver/fetch or binary-frame streaming contract that turns the reference into
byte-identical artifact bytes before domain admission.
Layer 4 — Authorization (attestation-gate consumer)¶
Every inbound INAC operation passes through the attestation-gate (proposal 041 §9). Four authorization sources map to gate predicates as described in proposal 042 §5.
Status¶
| Component | State |
|---|---|
| Attestation-gate crate (shared with Agora) | ❌ not started |
PassportResolutionCache (shared with Agora) |
❌ not started |
| General-capability-passport verifier path | ✅ implemented for receiver-side WSS push through capability-binding::authorize, OperationRequest::InacPush, and the built-in inac-push@v1 evaluator |
Custody-passport verifier path (enforces max_bytes, max_records, duration from scope) |
✅ implemented for receiver-side WSS push through capability-binding::authorize, the built-in memarium-custody@v1 evaluator, byte-limit enforcement, and durable max_records consumption in the INAC ledger |
| Invitation-passport verifier path (expiry, revocation freshness, single-use consumption log, scope match) | ✅ implemented for receiver-side WSS push before Artifact Delivery admission; missing revocation source is a fail-closed not-authorized condition |
Attestation-only fallback (for offer and for per-kind policies that allow unsolicited accept) |
❌ not started |
Per-peer/per-kind budgets (for offer vs push, schema, content type, size, and per-minute rate) |
✅ implemented as receiver-side artifact_delivery_adapters.inac_peer_transport.inbound_budgets; budget refusals are recorded before AD admission and before invitation notifications; max_per_minute = 0 is an explicit policy-denied deny rule, not a transient quota state |
Layer 5 — Kind dispatch and storage targets¶
Once authorization passes, dispatch routes the artefact to the
registered handler for its schema.
Baseline handlers¶
| Schema | Handler | Storage target | State |
|---|---|---|---|
agora-record.v1 |
Artifact Delivery in-process acceptor that verifies the envelope and re-ingests through local agora-service when available |
Agora relay/store | ✅ implemented; absence of local Agora is an explicit handler-unavailable refusal/transient failure, with no implicit Memarium fallback |
memarium-blob.v1 |
Artifact Delivery in-process acceptor that validates schema, recomputes blob/id, verifies the envelope signature, and writes an accepted custody fact through Memarium |
Memarium | ✅ implemented for encrypted/opaque custody; plaintext/private custody remains fail-closed unless a future explicit policy enables it |
The baseline memarium-blob.v1 acceptor is intentionally stricter than the
portable schema. It requires signature.key/public so verification does not
infer a signing key from author/participant-id; delegated blob signatures are
future work. It also requires a non-plaintext encryption descriptor object and
rejects both blob/encryption = "none" and descriptors with
algorithm = "none". Accepted custody envelopes are recorded as local
Memarium facts in the public space for the MVP; configurable target spaces and
sealed/private custody storage remain future work.
Open-kind handlers¶
Open artifact kinds are registered as Artifact Delivery inbound acceptors.
INAC does not own this registry and does not fan out to middleware chains. It
feeds the received artifact into Artifact Delivery; the host-owned admission
table selects exactly one authoritative acceptor. Safety floor (§Invariant 7)
applies: unknown registered kind with no acceptor → kind-not-supported.
Status¶
| Component | State |
|---|---|
| Artifact Delivery inbound acceptor registry + lookup | ✅ implemented for in-process MVP acceptors, supervised HTTP acceptors, JSON-e Flow acceptors, and WSS inac.v1 push admission |
| Conflict rule on shadowed kinds: duplicate exact authoritative acceptors fail readiness; exact content-type handlers may coexist with one wildcard fallback | ✅ implemented in Artifact Delivery |
| Safety-floor refusal for unknown kinds | ✅ implemented in local INAC runtime as fail-closed kind-not-supported |
| Session-level kind handshake (advisory enumeration, authoritative routing per-op) | ❌ not started |
Layer 6 — Invitation-passport lifecycle¶
Invitation passports (capability_id = "inac.invitation") are
short-lived narrow-scope instances of capability-passport.v1.
Distinctive traits expressed through scope fields:
- short expiry,
- optional single-use consumption,
- directed at a specific peer node id,
- bound to a specific
artifact/idorartifact/schema.
Status¶
| Component | State |
|---|---|
inac.invitation scope grammar |
✅ implemented as inac-invitation@v1 with inac/push, peer_node_ids, artifact_schemas, optional artifact_ids, optional content_types, and single_use defaulting to true |
| Issuer-side: passport builder for invitations | ✅ generic capability.passport.sign exists; Story-005 bootstrap can install A↔B invitation passports, and receiver-side notification Accept can issue a narrow inac.invitation passport for a pending offer; receiver-issued notification passports use artifact_delivery_adapters.inac_peer_transport.invitation_passport_ttl_seconds (default 3600 seconds) |
| Receiver-side: single-use consumption log | ✅ implemented in the local INAC SQLite ledger under <data-dir>/storage/inac.sqlite; repeated use of the same single-use passport for a different transfer is refused before AD admission |
| Receiver-side authorization core | ✅ implemented through the shared capability-binding::authorize path using OperationRequest::InacPush, a synthetic remote-node caller binding, the built-in inac-invitation@v1 evaluator, and the receiver revocation view |
| Revocation channel (reuse of proposal 024 passport revocation) | ✅ receiver gate requires a revocation view source, checks the current revocation view and freshness budget, and fails closed when the view is missing or stale; invitation-specific rate-limit policy remains a later policy layer |
Invitation UX is built on the generic notification queue rather than inside the
INAC transport. A pending offer creates an operator notification with
host-owned inac.invitation.accept and inac.invitation.reject action refs.
Accepting issues the narrow inac.invitation passport; rejecting records a
local refusal. Accept is idempotent for an already accepted offer and does not
issue another passport on a repeated action. Active pending offers are capped
per remote node before another notification is created. Accept also creates a
durable local contact projection in the INAC SQLite ledger when
artifact_delivery_adapters.inac_peer_transport.contact_creation_after_accept
is enabled. That contact is an operator-visible local relationship read model;
it does not grant authority, publish to Seed Directory or Agora, or replace the
broader public Contact Catalog authority surface.
Layer 7 — Peer transport and node-identity¶
INAC reuses, does not replace:
- node-to-node WSS session from proposal 002,
- Ed25519 node identity and security profiles (
CORP_COMPLIANT,E2E_PREFERRED,E2E_REQUIRED) from proposal 002, - discovery and reachability from proposal 014.
Open Question #6 in proposal 042 proposes a future
node-address-attestation.v1 kind for gossip-level address
distribution with multi-sig trust scoring and reflected-DDoS
resistance. That kind would itself be an INAC-carried
memarium-blob.v1 — i.e. INAC transports the very artefact that
helps peers find each other — but this is deferred.
Status¶
| Component | State |
|---|---|
| WSS transport + identity (proposal 002) | 🟡 transport proposal accepted; implementation state lives in node repo |
| Discovery (proposal 014) | 🟡 same as above |
node-address-attestation.v1 endpoint evidence |
✅ implemented as discovery/TLS trust evidence in Seed Directory and peer supervisor integration; carrying it as an INAC artefact for peer-relayed discovery remains deferred |
Layer 8 — Middleware integration¶
INAC lives inside the daemon's peer plane, so its wiring is analogous to proposal 027's peer-message dispatch.
| Component | State |
|---|---|
inac.v1 peer-message handler registered in the PeerMessageChain |
✅ implemented for daemon in-process WSS handling |
| Sidecar-registration path (so Python/other-language modules can declare Artifact Delivery inbound acceptors) | 🟡 partial — supervised HTTP acceptors can be wired through explicit daemon config; module-report-driven self-registration remains later |
| Daemon supervisor readiness: Artifact Delivery route table is resolved at init, not at first request | ✅ implemented for the current config/runtime validation surface; module-report acceptor projection remains later |
The direct INAC host capability surface and the Artifact Delivery route surface remain separate authorization boundaries:
inac.offer,inac.request, andinac.pushuse INAC outbound allowlists;artifact.delivery.senduses Artifact Delivery outbound allowlists, even when a route resolves to the localinac-directadapter.- Story-005 private/direct Whisper uses the Artifact Delivery route surface:
private-whisper-defaultresolves toinac-directand uses the same receiver-side INAC passport gate as other remote WSSpushframes.
This avoids treating permission to use one component-facing surface as implicit permission to use the other.
Remaining follow-ups and settled implementation decisions¶
-
Inline ceiling and streaming. The implementation now has a bounded inline ceiling plus
inac.stream.chunk.v1over the existing WSS peer-message session for larger payloads. A lower-level raw WebSocket binary carrier is a future performance optimization, not a semantic requirement. -
Per-peer and per-kind budget configurability. Receiver-side
offer/pushbudgets exist, but finer per-kind inline-ceiling and stream budget surfaces remain future hardening. -
Single-use invitation passports.
inac-invitation@v1defaultssingle_usetotrue. Reuse is checked by the receiver-side INAC ledger before AD admission. -
Kind-shadowing precedence. Artifact Delivery owns the authoritative acceptor table. Duplicate exact authoritative acceptors are conflicts; exact content-type handlers may coexist with one wildcard fallback.
-
Offer-without-intent policy. Unsolicited
offerintake is controlled by receiver-side peer budgets and invitation notification policy. It must never imply authority topush. -
Opaque-envelope storage for
agora-record.v1on nodes without a local Agora-relay subsystem. The baseline acceptor refuses explicitly when local Agora is unavailable. INAC/AD do not silently store Agora records in Memarium; a future custody path can add that behavior only through an explicit acceptor/configuration. -
Streaming abort semantics. Abort frames and whether an aborted transfer consumes a single-use invitation remain future work. Interrupted or malformed streams fail closed and do not invoke AD admission.
Current production-MVP boundaries¶
- Payload transfer supports inline artefacts and session-scoped stream chunks over the existing WSS peer-message session. A raw lower-level WebSocket binary carrier remains a profiling-led optimization.
- Referenced payload locations (
artifact/ref,artifact/href) are valid control-plane slots only when a configured resolver/fetch or stream contract can turn the reference into byte-identical bytes before domain admission. - Baseline handlers cover
agora-record.v1andmemarium-blob.v1. Open middleware acceptors are represented through Artifact Delivery acceptor declarations rather than by making INAC a middleware chain. - No session-level kind handshake in v1; authoritative routing remains per-operation and per-admission.
node-address-attestation.v1is implemented as discovery/TLS endpoint evidence, but peer-relaying address attestations as INAC-carried artefacts is deferred.- Single-custodian flows are the operational baseline. Multi-custody is allowed by higher-level custody policy, but the INAC protocol does not coordinate custodians; each custodian operates independently.
- No automatic INAC-to-Agora promotion. Cross-surface migration is a consumer's concern, such as a future Whisper durable-promotion workflow; INAC itself does not migrate artefacts between substrates.
Recommended commit order¶
Each step leaves the tree compiling.
- Resolve open decisions 2 (invitation single-use semantics), 3 (shadowing precedence), 4 (offer rate default). Cheap; unblocks schemas and gate.
- Layer 0 — schemas.
memarium-blob.v1first, theninac-control.v1control-message envelopes. Done for the MVP scaffold. - Attestation-gate (proposal 041). Shared primitive; blocks Layer 4.
inac.invitationscope grammar documented in proposal 024. Blocks Layer 6.- Layer 4 — authorization. Verifier paths for invitation, general INAC push, and Memarium custody passports are implemented through the shared capability-binding gate. Attestation-only remains future work.
- Layer 5 — baseline kind dispatch. Runtime registry exists and
fails closed. Concrete
agora-record.v1andmemarium-blob.v1acceptors are wired through Artifact Delivery in-process acceptors. - Layer 2 — control protocol.
offer/request/push+ responses, inline-only payloads. Done locally and over WSS peer-message push. - Layer 1 — peer-message registration. Register
inac.v1with thePeerMessageChain. Done for WSS. - Layer 6 — invitation lifecycle. Issuer + receiver + consumption log.
- Layer 8 — middleware integration. Daemon wiring of the INAC handler.
- MVP ships here. Consumers (whisper direct, custody transfer, crisis fan-out, passport handoff) start depending on INAC.
- Layer 3 — lower-level zero-copy WebSocket split. Optional profiling-led optimization beyond the authenticated stream chunk carrier.
- Layer 5 — Artifact Delivery open acceptor registry. Unlocks third-party middleware kind extensions without making INAC a chain.
- 042 Open Question #6 —
node-address-attestation.v1. Deferred; gated on the Seed Directory federation decision.
Testing posture¶
- Golden vectors for control messages (
offer,request,pushand all response variants) ininac-core/tests/. Federation-level contract; bumps require version increment. - Authorization-path tests: for each authorization source, produce acceptance and refusal cases; refusal codes must match the shared vocabulary exactly (regression guard against silent code drift).
- Envelope byte-identity tests: a record received via INAC
must hash (
record/id) identical to the same record ingested via Agora. Cross-surface equality is the invariant; this test is the enforcement. - Safety-floor test: an
offerorpushfor an unknownschema(with no registered handler) MUST refuse withkind-not-supported; no opaque storage path taken. - Invitation single-use test: a passport marked single-use
consumed once MUST refuse a second use with
invitation-revoked(or the chosen single-use refusal code). - Shared-cache test: a passport resolved on the Agora path
is immediately available on the INAC path and vice versa (same
PassportResolutionCache). - Streaming hash-mismatch test: an
intentional byte-flip in a chunk MUST trigger
digest-mismatchrefusal; no partial-commit residue on the receiver.