Skip to main content

cc_lb_plugin_wire/v1/
mod.rs

1//! Wire schema version 1 (baseline).
2//!
3//! Shared types between host (`cc-lb-runtime-wasmtime`) and guest
4//! (`cc-lb-pdk-wasmtime`) compiled in lockstep.
5//!
6//! Wire types for the three hooks the wasmtime runtime ships:
7//! filter (Phase 1), shape (Phase 2), observe
8//! (Phase 2). Signer extension is intentionally not exposed across
9//! the plugin boundary — host-side built-in `AnthropicKeySigner` /
10//! `AnthropicOAuthSigner` handle credential signing in-process.
11//!
12//! rkyv derives `Archive` + `Serialize` + `Deserialize` for every wire type.
13//! The host calls `rkyv::access::<ArchivedFilterRequest, rkyv::rancor::Error>`
14//! to get a zero-copy `&ArchivedFilterRequest` view straight out of guest
15//! linear memory; the guest mirrors that pattern in reverse for the response.
16//!
17//! ## ABI invariants (review consensus)
18//!
19//! * Every guest-allocated buffer is aligned to `align_of::<Archived<T>>()`
20//!   for its root type. rkyv 0.8's default relative pointers require this;
21//!   the alignment is propagated through `cc_lb_alloc(size, align)`.
22//! * `Arc`/`Rc` MUST NOT appear in any wire type (rkyv 0.8 Issue #670 —
23//!   `ArchivedRc::verify` can be bypassed for DST shared pointers).
24//! * The wire schema is fingerprinted via BLAKE3 of `cc_lb_schema_hash`
25//!   custom section content. Host and guest must agree byte-for-byte.
26//!
27//! ## Borrowed-wire optimisation (RFC-0001 #9)
28//!
29//! Host-to-guest request types (FilterRequest, ShapeRequest) come in
30//! two flavours that produce IDENTICAL archived bytes:
31//! * **Owned** (`FilterRequest`, `ShapeRequest`, ...) — owned `Box<str>`
32//!   / `Box<[u8]>` fields. Used by the guest for `rkyv::deserialize`
33//!   round-trips and for round-trip test fixtures.
34//! * **Borrowed ref** (`FilterRequestRef<'a>`, `ShapeRequestRef<'a>`,
35//!   ...) — `&'a str` / `&'a [u8]` fields with `#[rkyv(with =
36//!   InlineAsBox)]`. Used by the host in `wire_to_host_wire_request`
37//!   so the request body (up to 100 MiB on `/v1/files`) is serialised
38//!   IN PLACE from the request pipeline's `bytes::Bytes` without any
39//!   `.to_vec()` copy.
40//!
41//! Both variants archive to the same `ArchivedBox<ArchivedSlice<u8>>`
42//! / `ArchivedBox<ArchivedStr>` byte layouts — verified by the wire
43//! round-trip tests in `crates/cc-lb-plugin-wire/tests/borrowed_wire_roundtrip.rs`.
44//! When either variant is written to guest memory, the guest reads it
45//! via `rkyv::access::<ArchivedFilterRequest, _>` — the archived
46//! type name is identical because the owned type is the sole `Archive`
47//! source of truth; the ref type carries `#[rkyv(archived = ...)]` to
48//! reuse the owned type's archived form.
49//!
50//! See `docs/rfc/0001-plugin-runtime-vnext.md`.
51
52use alloc::boxed::Box;
53
54use rkyv::{Archive, Deserialize, Serialize, with::InlineAsBox};
55
56/// Principal context as seen by the filter plugin.
57///
58/// Mirrors `cc_lb_domain::Principal` semantically but the rkyv-derived
59/// wire form is the source of truth on the host↔guest boundary.
60#[derive(Archive, Serialize, Deserialize, Clone, Debug)]
61#[rkyv(derive(Debug))]
62pub struct Principal {
63    pub id: Box<str>,
64    pub kind: Box<str>,
65    /// Free-form key/value claims. Bytes intentionally, not JSON.
66    /// Modelled as a named `Claim` struct (not a tuple) so the
67    /// borrowed [`ClaimRef`] and this owned form archive to the same
68    /// byte layout for host/guest wire compatibility.
69    pub claims: Box<[Claim]>,
70}
71
72/// One claim key/value pair — see [`Principal::claims`]. Named
73/// struct (not tuple) so that `ClaimRef<'a>` matches the archived
74/// byte layout without an extra Ref wrapper.
75#[derive(Archive, Serialize, Deserialize, Clone, Debug)]
76#[rkyv(derive(Debug))]
77pub struct Claim {
78    pub key: Box<str>,
79    pub value: Box<[u8]>,
80}
81
82/// Borrowed mirror of [`Principal`] used by the host encode path. Every
83/// reference field carries `#[rkyv(with = InlineAsBox)]` so serialising
84/// this struct emits the same archived byte layout as [`Principal`].
85#[derive(Archive, Serialize)]
86pub struct PrincipalRef<'a> {
87    #[rkyv(with = InlineAsBox)]
88    pub id: &'a str,
89    #[rkyv(with = InlineAsBox)]
90    pub kind: &'a str,
91    #[rkyv(with = InlineAsBox)]
92    pub claims: &'a [ClaimRef<'a>],
93}
94
95/// One claim entry inside [`PrincipalRef`]. The tuple form used by the
96/// owned type does not have a straight borrowed equivalent, so we
97/// promote it to a named struct with two borrowed fields.
98#[derive(Archive, Serialize)]
99pub struct ClaimRef<'a> {
100    #[rkyv(with = InlineAsBox)]
101    pub key: &'a str,
102    #[rkyv(with = InlineAsBox)]
103    pub value: &'a [u8],
104}
105
106/// One upstream candidate the filter plugin can choose to keep or drop.
107#[derive(Archive, Serialize, Deserialize, Clone, Debug)]
108#[rkyv(derive(Debug))]
109pub struct UpstreamCandidate {
110    pub upstream_id: Box<str>,
111    pub name: Box<str>,
112    pub kind: Box<str>,
113    pub observed_at_unix_secs: u64,
114    pub predicted_cache_read_tokens: u32,
115    pub predicted_cache_creation_tokens_5m: u32,
116    pub predicted_cache_creation_tokens_1h: u32,
117    pub predicted_uncached_input_tokens: u32,
118    pub plan_capacity_ratio: f64,
119    pub organization_type: Box<str>,
120    pub rate_limit_tier: Box<str>,
121    pub seat_tier: Box<str>,
122}
123
124/// Borrowed mirror of [`UpstreamCandidate`].
125#[derive(Archive, Serialize)]
126pub struct UpstreamCandidateRef<'a> {
127    #[rkyv(with = InlineAsBox)]
128    pub upstream_id: &'a str,
129    #[rkyv(with = InlineAsBox)]
130    pub name: &'a str,
131    #[rkyv(with = InlineAsBox)]
132    pub kind: &'a str,
133    pub observed_at_unix_secs: u64,
134    pub predicted_cache_read_tokens: u32,
135    pub predicted_cache_creation_tokens_5m: u32,
136    pub predicted_cache_creation_tokens_1h: u32,
137    pub predicted_uncached_input_tokens: u32,
138    pub plan_capacity_ratio: f64,
139    #[rkyv(with = InlineAsBox)]
140    pub organization_type: &'a str,
141    #[rkyv(with = InlineAsBox)]
142    pub rate_limit_tier: &'a str,
143    #[rkyv(with = InlineAsBox)]
144    pub seat_tier: &'a str,
145}
146
147/// Model pricing summary supplied to filter plugins.
148#[derive(Archive, Serialize, Deserialize, Clone, Debug)]
149#[rkyv(derive(Debug))]
150pub struct CachePricingSummary {
151    pub status: Box<str>,
152    pub input_micros_per_million: Option<u64>,
153    pub cache_creation_5m_micros_per_million: Option<u64>,
154    pub cache_creation_1h_micros_per_million: Option<u64>,
155    pub cache_read_micros_per_million: Option<u64>,
156}
157
158/// Borrowed mirror of [`CachePricingSummary`].
159#[derive(Archive, Serialize)]
160pub struct CachePricingSummaryRef<'a> {
161    #[rkyv(with = InlineAsBox)]
162    pub status: &'a str,
163    pub input_micros_per_million: Option<u64>,
164    pub cache_creation_5m_micros_per_million: Option<u64>,
165    pub cache_creation_1h_micros_per_million: Option<u64>,
166    pub cache_read_micros_per_million: Option<u64>,
167}
168
169/// One header on the inbound request.
170#[derive(Archive, Serialize, Deserialize, Clone, Debug)]
171#[rkyv(derive(Debug))]
172pub struct Header {
173    pub name: Box<str>,
174    /// Raw header bytes — never base64'd. The whole point of the rkyv wire is
175    /// to remove that encode/decode hop.
176    pub value: Box<[u8]>,
177}
178
179/// Borrowed mirror of [`Header`].
180#[derive(Archive, Serialize)]
181pub struct HeaderRef<'a> {
182    #[rkyv(with = InlineAsBox)]
183    pub name: &'a str,
184    #[rkyv(with = InlineAsBox)]
185    pub value: &'a [u8],
186}
187
188/// Filter hook input (owned form used by the guest).
189#[derive(Archive, Serialize, Deserialize, Clone, Debug)]
190#[rkyv(derive(Debug))]
191pub struct FilterRequest {
192    pub request_id: Box<str>,
193    pub thread_id: Option<Box<str>>,
194    pub service_tier: Option<Box<str>>,
195    pub canonical_model_id: Box<str>,
196    pub cache_pricing: CachePricingSummary,
197    pub method: Box<str>,
198    pub path: Box<str>,
199    pub query: Option<Box<str>>,
200    pub headers: Box<[Header]>,
201    pub body: Box<[u8]>,
202    pub principal: Principal,
203    pub candidates: Box<[UpstreamCandidate]>,
204}
205
206/// Borrowed mirror of [`FilterRequest`] used by the host encode path.
207/// Serialising this emits the same archived byte layout as
208/// [`FilterRequest`] so the guest reads it via
209/// `rkyv::access::<ArchivedFilterRequest, _>` unchanged.
210#[derive(Archive, Serialize)]
211pub struct FilterRequestRef<'a> {
212    #[rkyv(with = InlineAsBox)]
213    pub request_id: &'a str,
214    pub thread_id: Option<QueryRef<'a>>,
215    pub service_tier: Option<QueryRef<'a>>,
216    #[rkyv(with = InlineAsBox)]
217    pub canonical_model_id: &'a str,
218    pub cache_pricing: CachePricingSummaryRef<'a>,
219    #[rkyv(with = InlineAsBox)]
220    pub method: &'a str,
221    #[rkyv(with = InlineAsBox)]
222    pub path: &'a str,
223    pub query: Option<QueryRef<'a>>,
224    #[rkyv(with = InlineAsBox)]
225    pub headers: &'a [HeaderRef<'a>],
226    #[rkyv(with = InlineAsBox)]
227    pub body: &'a [u8],
228    pub principal: PrincipalRef<'a>,
229    #[rkyv(with = InlineAsBox)]
230    pub candidates: &'a [UpstreamCandidateRef<'a>],
231}
232
233/// Borrowed wrapper for the optional `query` field. Necessary because
234/// `Option<InlineAsBox<&'a str>>` does not compose directly at the
235/// derive layer; a named struct pushes the `#[rkyv(with = ...)]`
236/// attribute onto the inner reference.
237#[derive(Archive, Serialize)]
238pub struct QueryRef<'a> {
239    #[rkyv(with = InlineAsBox)]
240    pub value: &'a str,
241}
242
243/// Decision the plugin made for one candidate.
244#[derive(Archive, Serialize, Deserialize, Clone, Debug)]
245#[rkyv(derive(Debug))]
246pub struct PerCandidateReason {
247    pub upstream_id: Box<str>,
248    /// `"accept"` or `"reject"` — kept as string so plugins remain forward-
249    /// compatible with future decision variants without a host re-bump.
250    pub decision: Box<str>,
251    pub reason: Box<str>,
252}
253
254/// Filter hook output.
255///
256/// Guest packs `(out_ptr, out_len)` into a single `u64` (`(ptr << 32) | len`)
257/// for the return value of `cc_lb_filter`. The host reads the bytes via
258/// `Memory::data(&store)` and runs `rkyv::access::<ArchivedFilterResponse, _>`.
259#[derive(Archive, Serialize, Deserialize, Clone, Debug)]
260#[rkyv(derive(Debug))]
261pub struct FilterResponse {
262    pub results: Box<[PerCandidateReason]>,
263}
264
265/// Upstream backend exposed to plugins. Mirrors
266/// `cc_lb_domain::Upstream` (currently a single variant —
267/// extending the host enum requires extending this one in lockstep
268/// and bumping a wire schema tag).
269#[derive(Archive, Serialize, Deserialize, Clone, Debug)]
270#[rkyv(derive(Debug))]
271pub enum Upstream {
272    AnthropicDirect {
273        /// Operator-configured base URL override, if any.
274        base_url: Option<Box<str>>,
275    },
276}
277
278/// Borrowed mirror of [`Upstream`].
279#[derive(Archive, Serialize)]
280pub enum UpstreamRef<'a> {
281    AnthropicDirect { base_url: Option<QueryRef<'a>> },
282}
283
284/// Shape hook input (owned form).
285#[derive(Archive, Serialize, Deserialize, Clone, Debug)]
286#[rkyv(derive(Debug))]
287pub struct ShapeRequest {
288    pub request_id: Box<str>,
289    pub method: Box<str>,
290    pub path: Box<str>,
291    pub query: Option<Box<str>>,
292    pub headers: Box<[Header]>,
293    pub body: Box<[u8]>,
294    pub principal: Principal,
295    pub upstream: Upstream,
296}
297
298/// Borrowed mirror of [`ShapeRequest`] used by the host encode path.
299#[derive(Archive, Serialize)]
300pub struct ShapeRequestRef<'a> {
301    #[rkyv(with = InlineAsBox)]
302    pub request_id: &'a str,
303    #[rkyv(with = InlineAsBox)]
304    pub method: &'a str,
305    #[rkyv(with = InlineAsBox)]
306    pub path: &'a str,
307    pub query: Option<QueryRef<'a>>,
308    #[rkyv(with = InlineAsBox)]
309    pub headers: &'a [HeaderRef<'a>],
310    #[rkyv(with = InlineAsBox)]
311    pub body: &'a [u8],
312    pub principal: PrincipalRef<'a>,
313    pub upstream: UpstreamRef<'a>,
314}
315
316/// Shape hook output — the upstream-bound request the plugin produced.
317#[derive(Archive, Serialize, Deserialize, Clone, Debug)]
318#[rkyv(derive(Debug))]
319pub struct ShapeResponse {
320    pub url: Box<str>,
321    pub method: Box<str>,
322    pub headers: Box<[Header]>,
323    pub body: Box<[u8]>,
324}
325
326#[derive(Archive, Serialize, Deserialize, Clone, Debug)]
327#[rkyv(derive(Debug))]
328pub struct TransformResponseRequest {
329    pub request_id: Box<str>,
330    pub principal: Principal,
331    pub upstream: Upstream,
332    pub request_method: Box<str>,
333    pub request_path: Box<str>,
334    pub canonical_model_id: Box<str>,
335    pub response_status: u16,
336    pub response_headers: Box<[Header]>,
337    pub body: Box<[u8]>,
338}
339
340#[derive(Archive, Serialize)]
341pub struct TransformResponseRequestRef<'a> {
342    #[rkyv(with = InlineAsBox)]
343    pub request_id: &'a str,
344    pub principal: PrincipalRef<'a>,
345    pub upstream: UpstreamRef<'a>,
346    #[rkyv(with = InlineAsBox)]
347    pub request_method: &'a str,
348    #[rkyv(with = InlineAsBox)]
349    pub request_path: &'a str,
350    #[rkyv(with = InlineAsBox)]
351    pub canonical_model_id: &'a str,
352    pub response_status: u16,
353    #[rkyv(with = InlineAsBox)]
354    pub response_headers: &'a [HeaderRef<'a>],
355    #[rkyv(with = InlineAsBox)]
356    pub body: &'a [u8],
357}
358
359#[derive(Archive, Serialize, Deserialize, Clone, Debug)]
360#[rkyv(derive(Debug))]
361pub enum TransformResponseResult {
362    Unchanged,
363    Replace {
364        status: Option<u16>,
365        headers: Option<Box<[Header]>>,
366        body: Option<Box<[u8]>>,
367    },
368}
369
370#[derive(Archive, Serialize, Deserialize, Clone, Debug)]
371#[rkyv(derive(Debug))]
372pub struct SseEvent {
373    pub event: Box<str>,
374    pub data: Box<[u8]>,
375}
376
377#[derive(Archive, Serialize)]
378pub struct SseEventRef<'a> {
379    #[rkyv(with = InlineAsBox)]
380    pub event: &'a str,
381    #[rkyv(with = InlineAsBox)]
382    pub data: &'a [u8],
383}
384
385#[derive(Archive, Serialize, Deserialize, Clone, Debug)]
386#[rkyv(derive(Debug))]
387pub struct TransformSseEventRequest {
388    pub request_id: Box<str>,
389    pub principal: Principal,
390    pub upstream: Upstream,
391    pub request_method: Box<str>,
392    pub request_path: Box<str>,
393    pub canonical_model_id: Box<str>,
394    pub response_status: u16,
395    pub response_headers: Box<[Header]>,
396    pub event: SseEvent,
397}
398
399#[derive(Archive, Serialize)]
400pub struct TransformSseEventRequestRef<'a> {
401    #[rkyv(with = InlineAsBox)]
402    pub request_id: &'a str,
403    pub principal: PrincipalRef<'a>,
404    pub upstream: UpstreamRef<'a>,
405    #[rkyv(with = InlineAsBox)]
406    pub request_method: &'a str,
407    #[rkyv(with = InlineAsBox)]
408    pub request_path: &'a str,
409    #[rkyv(with = InlineAsBox)]
410    pub canonical_model_id: &'a str,
411    pub response_status: u16,
412    #[rkyv(with = InlineAsBox)]
413    pub response_headers: &'a [HeaderRef<'a>],
414    pub event: SseEventRef<'a>,
415}
416
417#[derive(Archive, Serialize, Deserialize, Clone, Debug)]
418#[rkyv(derive(Debug))]
419pub enum TransformSseEventResult {
420    Unchanged,
421    Replace { events: Box<[SseEvent]> },
422    Drop,
423}
424
425/// Lifecycle event delivered to the observe hook. rkyv mirror of
426/// `cc_lb_observability::ObserveEvent`.
427#[derive(Archive, Serialize, Deserialize, Clone, Debug)]
428#[rkyv(derive(Debug))]
429pub enum ObserveEvent {
430    RequestStarted {
431        request_id: Box<str>,
432        downstream_user_agent: Option<Box<str>>,
433    },
434    AuthnComplete {
435        principal_id: Box<str>,
436        principal_kind: Box<str>,
437    },
438    UpstreamChosen {
439        upstream: Upstream,
440    },
441    Chunk {
442        batch_index: u64,
443        event_count: u64,
444        total_bytes: u64,
445    },
446    RequestFinished {
447        status: u16,
448        input_tokens: Option<u64>,
449        output_tokens: Option<u64>,
450        cache_creation_input_tokens: Option<u64>,
451        cache_read_input_tokens: Option<u64>,
452        duration_ms: u64,
453    },
454    Error {
455        code: Box<str>,
456        message: Box<str>,
457        source: Box<str>,
458    },
459}
460
461include!(concat!(env!("OUT_DIR"), "/wire_schema_impls.rs"));