Bifaci Protocol
Wire format, frame types, CBOR encoding, streaming, and checksums
Overview
Bifaci is a Latin term meaning “two-face” or “two-sided”, pronounced BYE-FAH-CHEE. Protocol version is 2.
Bifaci is the binary wire protocol between the host runtime and cartridge processes. All communication happens through length-prefixed CBOR frames sent over stdin/stdout pipes.
Wire Format
Every Bifaci message is a length-prefixed CBOR frame:
graph TD
A["4 bytes: u32 big-endian payload length"] --- B["N bytes: CBOR-encoded map (integer keys)"]
style A fill:none,stroke:#888
style B fill:none,stroke:#888
The 4-byte prefix gives the exact byte count of the CBOR payload that follows. The receiver reads exactly that many bytes, then decodes the CBOR map.
Two size limits apply:
- Negotiated max_frame (default 3.5 MB / 3,670,016 bytes): Set during handshake. Frames larger than this are rejected by both reader and writer.
- Hard limit (16 MB / 16,777,216 bytes): An absolute ceiling to prevent memory exhaustion. Cannot be negotiated upward.
Payloads that exceed max_frame must be split into chunks via the streaming protocol.
Frame Structure
The CBOR payload is a map with integer keys. Every frame has three required keys (version, frame_type, id). All other keys are optional and their presence depends on the frame type.
Key Reference Table
| Key | Name | Type | Description |
|---|---|---|---|
| 0 | version | u8 | Protocol version. Always 2. |
| 1 | frame_type | u8 | Discriminant identifying the frame type. |
| 2 | id | bytes[16] or uint | Request ID. UUID for requests, uint for control frames. |
| 3 | seq | u64 | Sequence number within a flow. Assigned by SeqAssigner at output. |
| 4 | content_type | text | MIME-like content type of the payload. |
| 5 | meta | map | String-keyed metadata map. Contents depend on frame type. |
| 6 | payload | bytes | Binary payload data. |
| 7 | len | u64 | Total byte count for a chunked transfer. Set on the first chunk only. |
| 8 | offset | u64 | Byte offset of this chunk within the stream. |
| 9 | eof | bool | True on the final chunk of a stream or on END frames. |
| 10 | cap | text | Cap URN for REQ frames. Identifies which capability to invoke. |
| 11 | stream_id | text | UUID identifying a stream within a request. |
| 12 | media_urn | text | Media URN identifying the data type of a stream. |
| 13 | routing_id | bytes[16] or uint | XID assigned by RelaySwitch for routing. |
| 14 | chunk_index | u64 | Zero-based index of this chunk within its stream. |
| 15 | chunk_count | u64 | Total number of chunks in a stream. Set on STREAM_END. |
| 16 | checksum | u64 | FNV-1a 64-bit hash of the chunk payload. |
Frame Types
The following diagram shows how frame types relate within a request lifecycle:
stateDiagram-v2
[*] --> Hello : connection setup
Hello --> Req : connection live
state "Request Lifecycle" as RL {
Req --> StreamStart : open stream
StreamStart --> Chunk : send data
Chunk --> Chunk : more data
Chunk --> StreamEnd : stream complete
StreamEnd --> StreamStart : open another stream
StreamEnd --> End : all streams done
Req --> End : empty response
Req --> Err : request failed
StreamStart --> Err : request failed
Chunk --> Err : request failed
}
state "Anytime" as AT {
Log : Log (progress, info)
Heartbeat : Heartbeat (health probe)
}
Hello (0)
Sent once by each side during connection setup. The host sends Hello first (no manifest); the cartridge responds with Hello including a JSON-encoded manifest in meta["manifest"].
The meta map also carries limit proposals: max_frame, max_chunk, and max_reorder_buffer. Both sides negotiate limits by taking the minimum of the two proposals.
Hello uses id = Uint(0) — it is not associated with any request.
Handshake Sequence
sequenceDiagram
participant H as Host
participant P as Cartridge
rect rgb(240, 248, 255)
Note over H,P: HELLO Exchange
H->>P: Hello (limits)
P->>H: Hello (limits + manifest)
Note over H,P: Both sides compute min() and update reader/writer
end
rect rgb(245, 255, 245)
Note over H,P: Identity Verification
H->>P: REQ(CAP_IDENTITY)
H->>P: STREAM_START
H->>P: CHUNK(nonce)
H->>P: STREAM_END
H->>P: END
P->>H: STREAM_START
P->>H: CHUNK(nonce echo)
P->>H: STREAM_END
P->>H: END
end
Note over H,P: Connection is now live
Req (1)
Initiates a capability invocation. The cap field (key 10) carries the Cap URN to invoke. The request may include a payload and content_type for inline data, though most arguments arrive as separate streams (STREAM_START/CHUNK/STREAM_END).
The routing_id (key 13) is assigned by the RelaySwitch at routing boundaries. Cartridges sending peer invocations do not set routing_id — it is added by the infrastructure.
The cap URN is validated at construction time — malformed URNs are bugs, not runtime errors.
Chunk (3)
Carries a piece of streaming data within a named stream. Required fields:
stream_id(key 11): Which stream this chunk belongs to.chunk_index(key 14): Zero-based position within the stream. Monotonically increasing.checksum(key 16): FNV-1a 64-bit hash of the payload bytes. Mandatory for corruption detection.
Optional fields set by the chunking layer:
offset(key 8): Byte offset within the total stream.len(key 7): Total byte count. Set on the first chunk only (chunk_index = 0).eof(key 9): True on the last chunk.content_type(key 4): Set on the first chunk only.
End (4)
Terminal frame that signals all streams for a request are complete. No STREAM_START, CHUNK, or other data frames may follow an END for the same request ID. May carry an optional final payload.
The eof field is set to true.
Log (5)
Carries log messages and progress updates. The meta map (key 5) contains:
level(text): One of"info","warn","error","progress", or a custom string.message(text): Human-readable message.progress(float, optional): A value between 0.0 and 1.0. Present only whenlevelis"progress".
Progress frames are LOG frames with level = "progress". They are not a separate frame type — the same frame structure carries both log messages and progress updates.
LOG frames can appear at any point during a request, interleaved with CHUNK frames. They do not affect the data stream.
Err (6)
Terminal frame that signals failure. Like END, no further frames may follow for this request. The meta map carries:
code(text): Machine-readable error code.message(text): Human-readable error description.
ERR and END are mutually exclusive terminals. A request ends with exactly one of them.
Heartbeat (7)
Health monitoring frame. Either side can send a Heartbeat; the receiver must respond with a Heartbeat carrying the same id. The CartridgeHostRuntime sends heartbeats every 30 seconds and expects a response within 10 seconds. A cartridge that fails to respond is marked unhealthy.
Heartbeat frames bypass sequence numbering — they are not flow frames and do not participate in ordering.
StreamStart (8)
Announces a new named stream within a request. Carries:
stream_id(key 11): A UUID uniquely identifying this stream within the request.media_urn(key 12): The Media URN identifying the data type of the stream.
Multiple STREAM_START frames per request enable multi-argument invocations — each argument is a separate stream with its own media URN.
StreamEnd (9)
Marks the end of a specific stream. After this frame, any CHUNK for the same stream_id is a protocol error. Carries:
stream_id(key 11): The stream being ended.chunk_count(key 15): Total number of CHUNK frames sent in this stream. The receiver can verify it received all chunks.
Streaming Sequences
A single stream within a request:
sequenceDiagram
participant S as Sender
participant R as Receiver
S->>R: STREAM_START (stream_id, media_urn)
S->>R: CHUNK (chunk_index=0, checksum)
S->>R: CHUNK (chunk_index=1, checksum)
Note over S,R: ... more chunks ...
S->>R: CHUNK (chunk_index=N, checksum)
S->>R: STREAM_END (chunk_count=N+1)
Multiple streams per request (multi-argument invocation):
sequenceDiagram
participant S as Sender
participant R as Receiver
rect rgb(240, 248, 255)
Note right of S: Stream A
S->>R: STREAM_START (stream A)
S->>R: CHUNK(s)
S->>R: STREAM_END (stream A)
end
rect rgb(245, 255, 245)
Note right of S: Stream B
S->>R: STREAM_START (stream B)
S->>R: CHUNK(s)
S->>R: STREAM_END (stream B)
end
S->>R: END
RelayNotify (10)
Sent by a RelaySlave to its paired RelayMaster to advertise the capabilities available on that host. The meta map carries:
manifest(bytes): JSON-encoded aggregate manifest of all cartridges on the host.max_frame,max_chunk,max_reorder_buffer: Protocol limits for this relay connection.
RelayNotify frames are intercepted by the relay — they never pass through to the other side.
RelayState (11)
Sent by a RelayMaster to its paired RelaySlave to provide host resource information. The payload (key 6) carries an opaque blob containing resource state such as model paths, GPU availability, or capability demands.
Like RelayNotify, RelayState frames are intercepted by the relay and never pass through.
MessageId
Frame IDs come in two variants:
- Uuid: 16 random bytes (v4 UUID). Used for request IDs and stream IDs — anything that needs to be globally unique.
- Uint: A 64-bit integer. Used for control frames (Hello uses
id = 0, Heartbeat can use any integer).
UUIDs are encoded as CBOR byte strings (major type 2, 16 bytes). Integers are encoded as CBOR unsigned integers (major type 0).
Flow Frames vs Control Frames
Frames are classified as flow or non-flow. The distinction matters for sequence numbering and reordering:
- Flow frames: Req, Chunk, End, Log, Err, StreamStart, StreamEnd. These participate in sequencing — the
SeqAssignerassigns monotonically increasingseqvalues per flow, and theReorderBufferat relay boundaries reorders them if they arrive out of sequence. - Non-flow frames: Hello, Heartbeat, RelayNotify, RelayState. These bypass sequence assignment entirely (their
seqstays at 0) and are not buffered by the reorder buffer.
Checksum
Every CHUNK frame carries a checksum in key 16: an FNV-1a 64-bit hash of the payload bytes:
const FNV_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
const FNV_PRIME: u64 = 0x100000001b3;
let mut hash = FNV_OFFSET_BASIS;
for &byte in data {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(FNV_PRIME);
}
The receiver verifies the checksum after decoding. A mismatch is a protocol error — the frame is rejected.
FNV-1a was chosen for speed and simplicity, not cryptographic strength. It detects accidental corruption (bit flips, truncation) but is not designed to resist intentional tampering.
Lifecycle Summary
sequenceDiagram
participant H as Host
participant P as Cartridge
H->>P: Hello (no manifest)
P->>H: Hello (with manifest)
Note over H,P: Connection established
H->>P: Req (cap URN)
H->>P: StreamStart (arg stream)
H->>P: Chunk (data)
H->>P: StreamEnd
P->>H: StreamStart (result stream)
P->>H: Chunk (result data)
P->>H: StreamEnd
P->>H: End
Note over H,P: Heartbeats can occur anytime
H->>P: Heartbeat
P->>H: Heartbeat (echo)