In this section

Connection setup between host and cartridge: frame exchange, limit negotiation, identity verification.

Handshake sequence

The handshake is a two-frame exchange followed by an identity verification request.

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

    rect rgb(255, 250, 240)
        Note over H,P: Policy
        H->>P: Policy (network)
    end

    Note over H,P: Connection is now live

The host always sends first. The cartridge waits for the host’s Hello before responding.

The identity check is the host’s own request and part of the handshake, and the one request a cartridge serves before it has a policy. Once it has passed, the host sends the cartridge its current Policy before it routes the cartridge anything; a host that does not have one yet sends it the moment it is given one, and routes nothing until then. No routed request reaches a cartridge that does not yet know what it may do.

Source: capdag-rs/src/bifaci/io.rs (handshake, handshake_accept).

Limit negotiation

Each Hello frame proposes four parameters in its meta map:

Parameter Default Description
max_frame 3,670,016 (3.5 MB) Maximum size of a single encoded frame in bytes.
max_chunk 262,144 (256 KB) Maximum payload size for a single CHUNK frame.
max_reorder_buffer 64 Maximum number of out-of-order frames the reorder buffer holds per flow.
initial_credit 32 Initial per-stream flow-control window, in CHUNK frames (see Flow control).

After the exchange, both sides compute min(ours, theirs) for each parameter. The resulting limits are applied to both the frame reader and writer immediately. All subsequent frames must respect the negotiated limits.

A credit window of zero is refused. initial_credit that negotiates to zero is a hard handshake failure naming both proposals: under a zero window no chunk could be sent, and with nothing consumed no credit would ever be granted, so every stream would stop at its first chunk permanently. The negotiation is the proved model’s (formal/CapDAG/Bifaci/Credit.lean, negotiate).

Version enforcement: each side declares protocol version 6 in HELLO (meta["version"] and frame key 0). Any other version is a hard handshake failure with a descriptive error naming both versions. There is no cross-version operation and no fallback.

The max_frame default of 3.5 MB provides a safety margin below the hard frame limit of 16 MB. The max_chunk default of 256 KB means large responses are automatically split into chunks of at most 256 KB each.

Source: capdag-rs/src/bifaci/frame.rs (Limits, DEFAULT_MAX_FRAME, DEFAULT_MAX_CHUNK, DEFAULT_MAX_REORDER_BUFFER).

Manifest exchange

The cartridge’s Hello includes a JSON-encoded manifest in meta["manifest"] as a byte string. The host’s Hello does not include a manifest; it only proposes limits.

The manifest declares:

{
  "name": "ggufcartridge",
  "version": "1.0.0",
  "description": "GGUF model inference via llama.cpp",
  "cap_groups": [
    {
      "name": "default",
      "caps": [
        {
          "urn": "cap:in=\"media:enc=utf-8\";out=\"media:enc=utf-8\";generate;model=gguf",
          "title": "Text Generation",
          "slug": "text-generation",
          "args": [ ... ]
        }
      ],
      "adapter_urns": []
    }
  ]
}

The manifest is required: a cartridge Hello without a manifest causes a handshake error. The host extracts the manifest and uses it to build the capability routing table.

After the host receives the manifest, it validates that CAP_IDENTITY (cap:effect=none) is present. Every cartridge must declare the identity capability because the handshake verifies it immediately after the Hello exchange.

Source: capdag-rs/src/bifaci/manifest.rs (CapManifest, validate).

Identity verification

After the Hello exchange, the host sends a capability invocation for CAP_IDENTITY to verify the protocol stack works end-to-end.

The nonce is a deterministic value: CBOR-encoded Text("bifaci"), which produces exactly 7 bytes. The host sends this as a standard streaming request:

  1. REQ: cap = CAP_IDENTITY, with XID and seq assigned.
  2. STREAM_START: stream_id = "identity-verify", media_urn = "media:".
  3. CHUNK: CBOR-encoded nonce bytes with checksum.
  4. STREAM_END: chunk_count = 1.
  5. END: request complete.

The cartridge’s identity handler receives the nonce and echoes it back through the same streaming pattern. The host reads the response, decodes the CBOR chunks, concatenates the bytes, and compares them to the original nonce.

The verifier MUST tolerate interleaved control and side-channel frames (CREDIT, HEARTBEAT, and LOG) while reading the echo. These are legal anywhere in a frame sequence (12.4: LOG interleaves without affecting data flow; CREDIT/HEARTBEAT are the control plane the writer’s terminal gate itself exempts), and a runtime that credits its probe input as the handler consumes it (L10) emits CREDIT frames during the exchange. Any other frame type is a hard verification failure.

A mismatch or timeout is a fatal error. The connection is abandoned because something in the frame encoding, relay forwarding, or handler dispatch is broken.

The nonce is deterministic, and the verification is not a security mechanism; it tests protocol correctness.

Source: capdag-rs/src/bifaci/io.rs (verify_identity, identity_nonce).

Host-side API

pub async fn handshake<R: AsyncRead + Unpin, W: AsyncWrite + Unpin>(
    reader: &mut FrameReader<R>,
    writer: &mut FrameWriter<W>,
) -> Result<HandshakeResult, CborError>

Sends the host’s Hello, reads the cartridge’s Hello, negotiates limits, updates both reader and writer, and returns a HandshakeResult containing the negotiated Limits and the cartridge’s manifest bytes.

Source: capdag-rs/src/bifaci/io.rs.

Cartridge-side API

pub async fn handshake_accept<R: AsyncRead + Unpin, W: AsyncWrite + Unpin>(
    reader: &mut FrameReader<R>,
    writer: &mut FrameWriter<W>,
    manifest: &[u8],
) -> Result<Limits, CborError>

Reads the host’s Hello, sends the cartridge’s Hello (with manifest), negotiates limits, updates both reader and writer, and returns the negotiated Limits.

Source: capdag-rs/src/bifaci/io.rs.

Swift equivalent

The Swift CartridgeRuntime in capdag-objc/Sources/Bifaci/CartridgeRuntime.swift follows the same handshake protocol. The run() method reads the host’s Hello from stdin, sends the cartridge’s Hello with manifest to stdout, and negotiates limits identically. The identity handler is registered automatically.

The main structural difference is that Swift uses synchronous I/O on stdin/stdout (with FileHandle reads) rather than async I/O, since the Swift runtime blocks the main thread with a DispatchSemaphore during run().