cap: Documentation
Browse Sign In

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 when level is "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 SeqAssigner assigns monotonically increasing seq values per flow, and the ReorderBuffer at relay boundaries reorders them if they arrive out of sequence.
  • Non-flow frames: Hello, Heartbeat, RelayNotify, RelayState. These bypass sequence assignment entirely (their seq stays 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)