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}