1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
//! Launch recipes.
//!
//! A launch recipe knows how to run one harness so its LLM traffic is directed
//! through a capture proxy endpoint — parameterized by that endpoint, never
//! specific to one consumer. Ported from a daemon client's per-agent env/config
//! injection and the Go opencode/codex config injection in tapes'
//! `cmd/tapes/start/`.
//!
//! # Recipes are pure
//!
//! Every recipe is a **pure function of its inputs**: it returns a
//! [`LaunchPlan`] describing the argv prefix, the environment overlay, and any
//! config documents the harness needs — but it never spawns a process, writes a
//! file, creates a temporary directory, or reads the user's home. The consumer
//! owns all of that, and therefore owns cleanup.
//!
//! This matters because consumers differ. A daemon client launches through a
//! `tokio::process::Command` and keeps the parent alive so its tracing
//! subscriber stays attached; the Go `tapes start` shelled out and registered a
//! PID with its own daemon. Neither process model belongs in shared harness
//! knowledge, and a recipe that materialised its own temporary directory would
//! have to invent a cleanup contract for both. Purity also makes the interesting
//! part — the exact bytes a harness needs — testable offline with no filesystem
//! at all.
//!
//! # What a recipe owns, and what the consumer owns
//!
//! The dividing line is *harness* knowledge versus *deployment* knowledge:
//!
//! | Recipe (here) | Consumer |
//! | --- | --- |
//! | which env var carries the base URL | what that URL's path prefix is |
//! | codex's `-c` provider-config grammar | which route each auth mode maps to |
//! | opencode's `opencode.json` provider shape | where to materialise it, and cleanup |
//! | that codex treats a blank `env_key` as absent | which credential to supply |
//! | that provider-display and attribution knobs exist | their branding and header names |
//!
//! So a recipe never constructs a route: the consumer hands it a fully
//! qualified [`ProxyEndpoint`] that already names whatever backend or provider
//! segment its proxy expects. A daemon client listening on loopback passes
//! something shaped like
//! `http://127.0.0.1:<port>/v1/anthropic/anthropic-transparent`; a client whose
//! proxy serves a flat root passes `http://127.0.0.1:<port>`. Both are the
//! consumer's to choose — the recipe only knows that Claude will append
//! `/v1/messages` to whatever it is given.
//!
//! # What is deliberately *not* here
//!
//! * **A pi recipe.** pi has no base-URL environment knob, so there is nothing
//! for a recipe to set: capture needs a JavaScript extension running inside
//! the harness. That extension and the environment contract it reads are now
//! [`crate::plugin`], vendor-neutral — but installing it and launching pi
//! against it are different jobs. A recipe here would additionally have to
//! plan the argv that loads an installed extension, and decide whether it
//! loads the globally installed copy or a per-launch one. Until it does,
//! pi is [`crate::harness::LaunchSupport::ConsumerOwned`].
//! * **Credential loading.** Which API key to hand a harness, and where it is
//! stored, is a consumer concern: a daemon client may pass the user's own
//! credential through untouched, while the Go CLI read its own credential
//! store. Recipes accept an already-resolved key and only know *where the
//! harness expects it*.
//! * **Process discovery.** Resolving a harness binary on `PATH` and spawning
//! it stay with the consumer. Name resolution no longer does: mapping a
//! user-typed spelling to a harness is [`crate::harness::find`], and the set
//! of names a consumer should offer is [`crate::harness::supported_agents`].
use PathBuf;
use Snafu;
pub use ;
pub use ;
pub use ;
/// How to launch a specific harness under a capture proxy.
///
/// Implementors carry their inputs as fields — including the
/// [`ProxyEndpoint`] — so this trait stays uniform across harnesses whose inputs
/// differ wildly. Claude needs one endpoint; codex needs an endpoint plus an
/// auth mode and a provider identity; opencode needs one endpoint *per provider*
/// plus a model selection. Threading all of that through a single method
/// signature would mean either a parameter most harnesses ignore or a different
/// signature per harness. Fields, then a nullary [`Self::plan`].
/// Everything a consumer must apply to launch a harness under a capture proxy.
///
/// The three parts are independent, and a recipe may leave any of them empty.
/// A config document a harness reads from disk, and where it must live.
///
/// Content only — no mode bits, no ownership. A consumer materialising one of
/// these should keep it private (`0o600`) whenever it carries a credential,
/// which an [`OpenCodeRecipe`] plan does whenever an API key was supplied.
/// A capture-proxy endpoint a harness should send its LLM traffic to.
///
/// Normalises on construction so downstream URL building never produces a
/// double slash: a bare `host:port` gains an `http://` scheme, and trailing
/// slashes are trimmed. Both matter because harnesses append their own path —
/// Claude appends `/v1/messages`, codex appends `/responses` — and
/// `http://host:1//v1/messages` is a 404 in some setups.
///
/// The path is entirely the consumer's: this type carries whatever route prefix
/// the consumer's proxy expects, and no recipe appends path segments to it.
;
/// Failure modes for [`LaunchRecipe::plan`].