Skip to main content

brazen/protocol/
wire.rs

1//! The wire REQUEST that flows encode → auth → transport (arch §4.1), and the three
2//! data facts it carries about how to deliver it: the HTTP [`Method`], the optional
3//! subprocess [`ExecSpec`], and the [`Envelope`] saying what that subprocess's pipes
4//! carry. Kept apart from the `Protocol` seam in the parent — the trait is the
5//! dialect's BEHAVIOUR, this is the request it hands over.
6
7use crate::transport::Timeouts;
8
9/// The HTTP verb a `WireRequest` carries (model-discovery §6): every generation
10/// request is a `Post` (the default — `encode` is unchanged), the `list-models` verb's
11/// GET a `Get`. Data on the one struct already crossing the transport seam (mirrors
12/// `timeouts`), not a new `send` parameter — the impure `HttpTransport` reads it to
13/// pick the verb, `MockTransport` records it.
14#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
15pub enum Method {
16    #[default]
17    Post,
18    Get,
19}
20
21/// A subprocess target a [`WireRequest`] may name instead of an HTTP one
22/// (claude-code spec §3.1): the native transport spawns `program args…`, writes
23/// `wire.body` to the child's stdin, and streams the child's stdout as the response
24/// body. Data on the one struct already crossing the transport seam — like
25/// [`Method`]/[`Timeouts`], never a new `send` parameter. [`Envelope`] says what the
26/// child's pipes CARRY, which is the only thing the two subprocess uses differ in.
27#[derive(Clone, Debug, Default, PartialEq)]
28pub struct ExecSpec {
29    pub program: String,
30    pub args: Vec<String>,
31    pub envelope: Envelope,
32}
33
34/// What a spawned child's stdin/stdout carry (transport spec §4.1) — the ONE
35/// discriminator between the two subprocess uses, so `WireRequest` never grows a
36/// second exec field and a row can never be both by construction.
37#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
38pub enum Envelope {
39    /// The child IS the provider: stdin is the dialect's own body, stdout its own
40    /// dialect stream, status 200 at spawn (claude-code spec §3.2).
41    #[default]
42    Body,
43    /// The child IS the transport: stdin is one whole HTTP/1.1 request message,
44    /// stdout one whole HTTP/1.1 response message (transport spec §5). The status,
45    /// and any `retry-after`, are the ones the child reports.
46    Http,
47}
48
49/// The HTTP request that flows encode → auth → transport (arch §4.1). `encode`
50/// builds the body + non-auth headers; `Auth::apply` adds the auth headers in
51/// place; `Transport::send` consumes it. Header names match case-insensitively so
52/// an auth overwrite never duplicates a header. `method` is `Post` for every
53/// generation request (the default — `encode` builds POSTs via `new`) and `Get` for
54/// the `list-models` verb's GET (§6). `timeouts` is the per-request transport policy
55/// (config §4): `encode` leaves it at the `Default` (all unset) and `run` stamps the
56/// resolved config onto it before `send`, so a config-driven bound reaches the
57/// impure transport without a wider `send` signature. `exec` declares a SUBPROCESS
58/// target (claude-code spec §3): `None` = HTTP (every prior dialect, byte-identical);
59/// `Some` routes the native transport to the spawn — `url`/`method`/`headers` are
60/// inert on that path.
61#[derive(Clone, Debug, Default, PartialEq)]
62pub struct WireRequest {
63    pub method: Method,
64    pub url: String,
65    pub headers: Vec<(String, String)>,
66    pub body: Vec<u8>,
67    pub timeouts: Timeouts,
68    pub exec: Option<ExecSpec>,
69}
70
71impl WireRequest {
72    /// A `Post` request targeting `url` with `body`, no headers yet and default
73    /// (unset) timeouts. The one constructor `encode` uses — the method stays `Post`.
74    pub fn new(url: impl Into<String>, body: Vec<u8>) -> Self {
75        WireRequest {
76            method: Method::Post,
77            url: url.into(),
78            headers: Vec::new(),
79            body,
80            timeouts: Timeouts::default(),
81            exec: None,
82        }
83    }
84
85    /// A `Get` request targeting `url` with an empty body — the `list-models` verb's
86    /// GET (§6). No headers yet and default (unset) timeouts.
87    pub fn get(url: impl Into<String>) -> Self {
88        WireRequest {
89            method: Method::Get,
90            url: url.into(),
91            headers: Vec::new(),
92            body: Vec::new(),
93            timeouts: Timeouts::default(),
94            exec: None,
95        }
96    }
97
98    /// Set a header, replacing any existing one of the same (case-insensitive)
99    /// name rather than appending a duplicate.
100    pub fn set_header(&mut self, name: &str, value: &str) {
101        if let Some(slot) = self
102            .headers
103            .iter_mut()
104            .find(|(n, _)| n.eq_ignore_ascii_case(name))
105        {
106            slot.1 = value.to_owned();
107        } else {
108            self.headers.push((name.to_owned(), value.to_owned()));
109        }
110    }
111
112    /// The value of a header by case-insensitive name, if set.
113    pub fn header(&self, name: &str) -> Option<&str> {
114        self.headers
115            .iter()
116            .find(|(n, _)| n.eq_ignore_ascii_case(name))
117            .map(|(_, v)| v.as_str())
118    }
119}