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_plugin_api::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 plan_capacity_ratio: f64,
116 pub organization_type: Box<str>,
117 pub rate_limit_tier: Box<str>,
118 pub seat_tier: Box<str>,
119}
120
121/// Borrowed mirror of [`UpstreamCandidate`].
122#[derive(Archive, Serialize)]
123pub struct UpstreamCandidateRef<'a> {
124 #[rkyv(with = InlineAsBox)]
125 pub upstream_id: &'a str,
126 #[rkyv(with = InlineAsBox)]
127 pub name: &'a str,
128 #[rkyv(with = InlineAsBox)]
129 pub kind: &'a str,
130 pub observed_at_unix_secs: u64,
131 pub predicted_cache_read_tokens: u32,
132 pub plan_capacity_ratio: f64,
133 #[rkyv(with = InlineAsBox)]
134 pub organization_type: &'a str,
135 #[rkyv(with = InlineAsBox)]
136 pub rate_limit_tier: &'a str,
137 #[rkyv(with = InlineAsBox)]
138 pub seat_tier: &'a str,
139}
140
141/// One header on the inbound request.
142#[derive(Archive, Serialize, Deserialize, Clone, Debug)]
143#[rkyv(derive(Debug))]
144pub struct Header {
145 pub name: Box<str>,
146 /// Raw header bytes — never base64'd. The whole point of the rkyv wire is
147 /// to remove that encode/decode hop.
148 pub value: Box<[u8]>,
149}
150
151/// Borrowed mirror of [`Header`].
152#[derive(Archive, Serialize)]
153pub struct HeaderRef<'a> {
154 #[rkyv(with = InlineAsBox)]
155 pub name: &'a str,
156 #[rkyv(with = InlineAsBox)]
157 pub value: &'a [u8],
158}
159
160/// Filter hook input (owned form used by the guest).
161#[derive(Archive, Serialize, Deserialize, Clone, Debug)]
162#[rkyv(derive(Debug))]
163pub struct FilterRequest {
164 pub request_id: Box<str>,
165 pub thread_id: Option<Box<str>>,
166 pub method: Box<str>,
167 pub path: Box<str>,
168 pub query: Option<Box<str>>,
169 pub headers: Box<[Header]>,
170 pub body: Box<[u8]>,
171 pub principal: Principal,
172 pub candidates: Box<[UpstreamCandidate]>,
173}
174
175/// Borrowed mirror of [`FilterRequest`] used by the host encode path.
176/// Serialising this emits the same archived byte layout as
177/// [`FilterRequest`] so the guest reads it via
178/// `rkyv::access::<ArchivedFilterRequest, _>` unchanged.
179#[derive(Archive, Serialize)]
180pub struct FilterRequestRef<'a> {
181 #[rkyv(with = InlineAsBox)]
182 pub request_id: &'a str,
183 pub thread_id: Option<QueryRef<'a>>,
184 #[rkyv(with = InlineAsBox)]
185 pub method: &'a str,
186 #[rkyv(with = InlineAsBox)]
187 pub path: &'a str,
188 pub query: Option<QueryRef<'a>>,
189 #[rkyv(with = InlineAsBox)]
190 pub headers: &'a [HeaderRef<'a>],
191 #[rkyv(with = InlineAsBox)]
192 pub body: &'a [u8],
193 pub principal: PrincipalRef<'a>,
194 #[rkyv(with = InlineAsBox)]
195 pub candidates: &'a [UpstreamCandidateRef<'a>],
196}
197
198/// Borrowed wrapper for the optional `query` field. Necessary because
199/// `Option<InlineAsBox<&'a str>>` does not compose directly at the
200/// derive layer; a named struct pushes the `#[rkyv(with = ...)]`
201/// attribute onto the inner reference.
202#[derive(Archive, Serialize)]
203pub struct QueryRef<'a> {
204 #[rkyv(with = InlineAsBox)]
205 pub value: &'a str,
206}
207
208/// Decision the plugin made for one candidate.
209#[derive(Archive, Serialize, Deserialize, Clone, Debug)]
210#[rkyv(derive(Debug))]
211pub struct PerCandidateReason {
212 pub upstream_id: Box<str>,
213 /// `"accept"` or `"reject"` — kept as string so plugins remain forward-
214 /// compatible with future decision variants without a host re-bump.
215 pub decision: Box<str>,
216 pub reason: Box<str>,
217}
218
219/// Filter hook output.
220///
221/// Guest packs `(out_ptr, out_len)` into a single `u64` (`(ptr << 32) | len`)
222/// for the return value of `cc_lb_filter`. The host reads the bytes via
223/// `Memory::data(&store)` and runs `rkyv::access::<ArchivedFilterResponse, _>`.
224#[derive(Archive, Serialize, Deserialize, Clone, Debug)]
225#[rkyv(derive(Debug))]
226pub struct FilterResponse {
227 pub results: Box<[PerCandidateReason]>,
228}
229
230/// Upstream backend exposed to plugins. Mirrors
231/// `cc_lb_plugin_api::Upstream` (currently a single variant —
232/// extending the host enum requires extending this one in lockstep
233/// and bumping a wire schema tag).
234#[derive(Archive, Serialize, Deserialize, Clone, Debug)]
235#[rkyv(derive(Debug))]
236pub enum Upstream {
237 AnthropicDirect {
238 /// Operator-configured base URL override, if any.
239 base_url: Option<Box<str>>,
240 },
241}
242
243/// Borrowed mirror of [`Upstream`].
244#[derive(Archive, Serialize)]
245pub enum UpstreamRef<'a> {
246 AnthropicDirect { base_url: Option<QueryRef<'a>> },
247}
248
249/// Shape hook input (owned form).
250#[derive(Archive, Serialize, Deserialize, Clone, Debug)]
251#[rkyv(derive(Debug))]
252pub struct ShapeRequest {
253 pub request_id: Box<str>,
254 pub method: Box<str>,
255 pub path: Box<str>,
256 pub query: Option<Box<str>>,
257 pub headers: Box<[Header]>,
258 pub body: Box<[u8]>,
259 pub principal: Principal,
260 pub upstream: Upstream,
261}
262
263/// Borrowed mirror of [`ShapeRequest`] used by the host encode path.
264#[derive(Archive, Serialize)]
265pub struct ShapeRequestRef<'a> {
266 #[rkyv(with = InlineAsBox)]
267 pub request_id: &'a str,
268 #[rkyv(with = InlineAsBox)]
269 pub method: &'a str,
270 #[rkyv(with = InlineAsBox)]
271 pub path: &'a str,
272 pub query: Option<QueryRef<'a>>,
273 #[rkyv(with = InlineAsBox)]
274 pub headers: &'a [HeaderRef<'a>],
275 #[rkyv(with = InlineAsBox)]
276 pub body: &'a [u8],
277 pub principal: PrincipalRef<'a>,
278 pub upstream: UpstreamRef<'a>,
279}
280
281/// Shape hook output — the upstream-bound request the plugin produced.
282#[derive(Archive, Serialize, Deserialize, Clone, Debug)]
283#[rkyv(derive(Debug))]
284pub struct ShapeResponse {
285 pub url: Box<str>,
286 pub method: Box<str>,
287 pub headers: Box<[Header]>,
288 pub body: Box<[u8]>,
289}
290
291/// Lifecycle event delivered to the observe hook. rkyv mirror of
292/// `cc_lb_plugin_api::ObserveEvent`.
293#[derive(Archive, Serialize, Deserialize, Clone, Debug)]
294#[rkyv(derive(Debug))]
295pub enum ObserveEvent {
296 RequestStarted {
297 request_id: Box<str>,
298 downstream_user_agent: Option<Box<str>>,
299 },
300 AuthnComplete {
301 principal_id: Box<str>,
302 principal_kind: Box<str>,
303 },
304 UpstreamChosen {
305 upstream: Upstream,
306 },
307 Chunk {
308 batch_index: u64,
309 event_count: u64,
310 total_bytes: u64,
311 },
312 RequestFinished {
313 status: u16,
314 input_tokens: Option<u64>,
315 output_tokens: Option<u64>,
316 cache_creation_input_tokens: Option<u64>,
317 cache_read_input_tokens: Option<u64>,
318 duration_ms: u64,
319 },
320 Error {
321 code: Box<str>,
322 message: Box<str>,
323 source: Box<str>,
324 },
325}
326
327include!(concat!(env!("OUT_DIR"), "/wire_schema_impls.rs"));