In questa sezione

L’impostazione della connessione tra host e cartuccia: scambio di frame, negoziazione dei limiti, verifica dell’identità.

Sequenza dell’handshake

L’handshake è uno scambio di due frame seguito da una richiesta di verifica dell’identità.

sequenceDiagram
    participant H as Host
    participant P as Cartuccia

    rect rgb(240, 248, 255)
        Note over H,P: Scambio HELLO
        H->>P: Hello (limiti)
        P->>H: Hello (limiti + manifest)
        Note over H,P: Entrambi i lati calcolano min() e aggiornano reader/writer
    end

    rect rgb(245, 255, 245)
        Note over H,P: Verifica dell'identità
        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(eco del nonce)
        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: La connessione è ora attiva

L’host invia sempre per primo. La cartuccia attende l’Hello dell’host prima di rispondere.

La verifica di identità è la richiesta dell’host stesso e fa parte dell’handshake: è l’unica richiesta che una cartuccia serve prima di avere una policy. Superata la verifica, l’host invia alla cartuccia la sua Policy corrente prima di instradarle qualsiasi cosa; un host che non ne ha ancora una la invia nel momento in cui la riceve, e fino ad allora non instrada nulla. Nessuna richiesta instradata raggiunge una cartuccia che non sa ancora cosa le è consentito.

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

Negoziazione dei limiti

Ogni frame Hello propone quattro parametri nella sua mappa meta:

Parametro Predefinito Descrizione
max_frame 3,670,016 (3.5 MB) Dimensione massima di un singolo frame codificato, in byte.
max_chunk 262,144 (256 KB) Dimensione massima del payload di un singolo frame CHUNK.
max_reorder_buffer 64 Numero massimo di frame fuori ordine che il buffer di riordino trattiene per flusso.
initial_credit 32 Finestra iniziale di controllo di flusso per stream, in frame CHUNK (vedi Controllo di flusso).

Dopo lo scambio, entrambi i lati calcolano min(ours, theirs) per ciascun parametro. I limiti risultanti vengono applicati immediatamente sia al lettore sia allo scrittore dei frame. Tutti i frame successivi devono rispettare i limiti negoziati.

Una finestra di credito pari a zero viene rifiutata. Un initial_credit che dalla negoziazione risulta zero è un fallimento definitivo dell’handshake che nomina entrambe le proposte: con una finestra nulla non si potrebbe inviare alcun chunk, e senza nulla consumato non verrebbe mai concesso alcun credito, quindi ogni stream si fermerebbe per sempre al suo primo chunk. La negoziazione è quella del modello dimostrato (formal/CapDAG/Bifaci/Credit.lean, negotiate).

Imposizione della versione: ciascun lato dichiara la versione di protocollo 6 nell’HELLO (meta["version"] e chiave 0 del frame). Qualsiasi altra versione è un fallimento definitivo dell’handshake, con un errore descrittivo che nomina entrambe le versioni. Non esiste alcun funzionamento tra versioni diverse né alcun ripiego.

Il valore predefinito di max_frame di 3.5 MB fornisce un margine di sicurezza al di sotto del limite rigido dei frame di 16 MB. Il valore predefinito di max_chunk di 256 KB fa sì che le risposte grandi vengano suddivise automaticamente in chunk di al massimo 256 KB ciascuno.

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

Scambio del manifest

L’Hello della cartuccia include un manifest codificato in JSON in meta["manifest"], come stringa di byte. L’Hello dell’host non include un manifest; propone soltanto i limiti.

Il manifest dichiara:

{
  "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": []
    }
  ]
}

Il manifest è obbligatorio: un Hello di cartuccia senza manifest provoca un errore di handshake. L’host estrae il manifest e lo usa per costruire la tabella di instradamento delle capacità.

Dopo aver ricevuto il manifest, l’host verifica che CAP_IDENTITY (cap:effect=none) sia presente. Ogni cartuccia deve dichiarare la capacità di identità, perché l’handshake la verifica subito dopo lo scambio degli Hello.

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

Verifica dell’identità

Dopo lo scambio degli Hello, l’host invia un’invocazione di capacità per CAP_IDENTITY per verificare che lo stack di protocollo funzioni da un capo all’altro.

Il nonce è un valore deterministico: Text("bifaci") codificato in CBOR, che produce esattamente 7 byte. L’host lo invia come una richiesta di streaming standard:

  1. REQ: cap = CAP_IDENTITY, con XID e seq assegnati.
  2. STREAM_START: stream_id = "identity-verify", media_urn = "media:".
  3. CHUNK: byte del nonce codificati in CBOR, con checksum.
  4. STREAM_END: chunk_count = 1.
  5. END: richiesta completata.

L’handler di identità della cartuccia riceve il nonce e lo restituisce con lo stesso schema di streaming. L’host legge la risposta, decodifica i chunk CBOR, concatena i byte e li confronta con il nonce originale.

Il verificatore DEVE tollerare frame di controllo e di canale laterale interfogliati (CREDIT, HEARTBEAT e LOG) mentre legge l’eco. Questi sono legittimi in qualsiasi punto di una sequenza di frame (12.4: LOG si interfoglia senza influire sul flusso dei dati; CREDIT/HEARTBEAT sono il piano di controllo che il cancello terminale dello scrittore stesso esenta), e un runtime che accredita l’input della sua sonda man mano che l’handler lo consuma (L10) emette frame CREDIT durante lo scambio. Qualsiasi altro tipo di frame è un fallimento definitivo della verifica.

Una discrepanza o un timeout è un errore fatale. La connessione viene abbandonata perché qualcosa nella codifica dei frame, nell’inoltro tramite relay o nello smistamento agli handler è rotto.

Il nonce è deterministico, e la verifica non è un meccanismo di sicurezza; verifica la correttezza del protocollo.

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

API lato host

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

Invia l’Hello dell’host, legge l’Hello della cartuccia, negozia i limiti, aggiorna sia il lettore sia lo scrittore e restituisce un HandshakeResult che contiene i Limits negoziati e i byte del manifest della cartuccia.

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

API lato cartuccia

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

Legge l’Hello dell’host, invia l’Hello della cartuccia (con il manifest), negozia i limiti, aggiorna sia il lettore sia lo scrittore e restituisce i Limits negoziati.

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

Equivalente in Swift

Il CartridgeRuntime per Swift in capdag-objc/Sources/Bifaci/CartridgeRuntime.swift segue lo stesso protocollo di handshake. Il metodo run() legge l’Hello dell’host da stdin, invia su stdout l’Hello della cartuccia con il manifest e negozia i limiti in modo identico. L’handler di identità viene registrato automaticamente.

La principale differenza strutturale è che Swift usa I/O sincrono su stdin/stdout (con letture tramite FileHandle) anziché I/O asincrono, poiché il runtime Swift blocca il thread principale con un DispatchSemaphore durante run().