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:
- REQ: cap =
CAP_IDENTITY, con XID e seq assegnati. - STREAM_START: stream_id =
"identity-verify", media_urn ="media:". - CHUNK: byte del nonce codificati in CBOR, con checksum.
- STREAM_END: chunk_count = 1.
- 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().