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
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
//! Starting the `trusty-search` daemon the whole audit stack stands on (#5670).
//!
//! Why: the audit's prerequisite chain is trusty-search → per-repository index →
//! trusty-analyze, and [`super::analyze`] closed only the last link. Starting
//! `trusty-analyze` is not enough on its own, in two different ways.
//!
//! On a cold machine `trusty-analyze serve` exits at its own trusty-search check
//! before it ever binds a socket, so the analyze preflight spawns a process that
//! is already gone by the first poll and refuses the run. The operator's remedy was
//! to run `trusty-search start` by hand, which DOC-67 §2 does not allow: the
//! sweep gets one non-interactive shot and owes no manual prerequisite.
//!
//! The second case is the one a fresh-spawn fix misses. `trusty-analyze` reports
//! itself degraded whenever trusty-search is unreachable, and
//! [`super::analyze`]'s health check counts only `status: "ok"` — so the analyze
//! preflight re-reads trusty-search's LIVE status on every run. An analyze daemon
//! that has been up for days on top of a trusty-search that died an hour ago
//! fails the probe, the spawned replacement exits at its own search check, the
//! original keeps reporting degraded, and the readiness poll refuses. Nothing
//! about that run is a cold start.
//!
//! What: [`ensure_search_daemon`], the socket and binary rules it applies, and
//! [`SearchDaemonUnavailable`]. It runs BEFORE the analyze preflight in
//! `crate::commands::audit::run`, which is the whole point — analyze cannot boot
//! without it.
//!
//! #6285: trusty-search binds one hardened Unix socket and speaks framed
//! JSON-RPC, so the discovery files this used to read describe a listener that
//! is being retired. The daemon and this caller now DERIVE the same path from
//! [`trusty_common::daemon_socket_path`], which is what removes the resolution
//! step entirely — there is no address to discover, no port to fall back to,
//! and nothing for a stale `http_addr` to contradict. This is the same shape
//! [`super::analyze`] took for trusty-analyze in #6287, and it is the crate's
//! only trusty-search dial.
//!
//! Nothing here is fail-open. Both failure arms — the binary would not spawn, and
//! the daemon never answered — return `Err`, for the same reason the analyze
//! preflight refuses: a report with its findings, complexity and health sections
//! empty reads as a clean bill of health rather than as an outage.
//! Test: `super::tests` against stubs; `super::real_binary_tests` (`#[ignore]`d)
//! against the real `trusty-search` binary.
//!
//! # Spec References
//! - [`SPEC-TGAUDIT-06~draft`](../../../../docs/specs/DOC-67-tga-audit-mode.md#SPEC-TGAUDIT-06~draft)
//! - [`SPEC-TGAUDIT-09~draft`](../../../../docs/specs/DOC-67-tga-audit-mode.md#SPEC-TGAUDIT-09~draft)
use ;
use ;
use ;
use ;
/// Wall-clock budget for a freshly-spawned `trusty-search` to report healthy.
///
/// Why: 60s, matching `trusty-search`'s own guard
/// (`crates/trusty-search/src/commands/daemon_guard.rs`'s `READY_TIMEOUT`) rather
/// than `daemon_guard::DEFAULT_STARTUP_TIMEOUT`'s 30s. The socket binds in about
/// a second, but a first run on a machine with no model cache spends 15–30s in
/// ONNX load before it answers, and refusing an audit at 30s would turn a slow
/// cold start into a failed engagement.
pub const SEARCH_STARTUP_TIMEOUT: Duration = from_secs;
/// Environment variable overriding the trusty-search daemon's socket.
///
/// Why: it replaces `TRUSTY_DATA_DIR` as the way a rig points this guard at a
/// daemon it started, without redirecting every other trusty-* client in the
/// same process. Same name trusty-mpm and trusty-audit read, so an operator
/// pins one daemon for the whole stack with one export.
pub const ENV_SEARCH_SOCKET: &str = "TRUSTY_SEARCH_SOCKET";
/// The method `trusty-search` answers a health probe on.
///
/// Duplicated as a literal rather than imported: `tga` has no Cargo edge on
/// `trusty-search`. `trusty_search::service::socket::METHOD_HEALTH` is the
/// definition, and the daemon's own
/// `rpc_router_registers_every_documented_method` is what keeps its router
/// equal to it. A name that drifted answers `method_not_found`, which
/// [`search_is_healthy`] reports as an unhealthy daemon.
const SEARCH_HEALTH_METHOD: &str = "search.health";
/// How long one health dial may take.
const PROBE_TIMEOUT: Duration = from_secs;
/// The audit cannot proceed without the trusty-search daemon.
///
/// Why: this is refused before the sweep rather than reported after it, so the
/// message is the operator's whole remedy. It names trusty-search as the FIRST
/// link rather than describing the analyze symptom, because an operator reading
/// "trusty-analyze is degraded" reaches for the wrong daemon.
/// What: the address probed, the binary tried, and the underlying cause.
/// Test: `super::tests::an_unspawnable_search_binary_refuses_the_audit`.
/// Where and how [`ensure_search_daemon`] looks for the daemon.
///
/// Why: the socket and binary come from the environment and the two budgets are
/// fixed, which makes the whole guard untestable if it reads them itself. Taking
/// them as a value is what lets a test drive the spawn-and-poll path against a
/// stub executable on a temp socket — the same split [`super::AnalyzeGuard`]
/// uses.
/// What: the daemon socket, the binary to spawn, and the readiness budget.
/// Test: `super::tests::a_reachable_search_daemon_is_not_restarted`.
/// The trusty-search daemon's socket: [`ENV_SEARCH_SOCKET`], else the derived
/// default.
///
/// Why/What: see [`SearchGuard::from_env`].
/// Test: `super::tests::the_search_guard_derives_the_socket_the_daemon_binds`.
/// The override rule itself: a non-empty value wins, everything else defaults.
///
/// Split out so the rule is asserted without any test reading or writing the
/// process environment — `set_var` is `unsafe` in edition 2024 and unsound under
/// the parallel harness.
/// Is the trusty-search daemon at `socket` answering?
///
/// Why any result frame counts: `GET /health` on trusty-search answered 200
/// unconditionally — its handler returns a body with no status code of its own —
/// so `probe_once` treated a daemon that was up and warning about one index as
/// reachable. Reading `status` here would newly respawn such a daemon, which is
/// a behaviour change #6285 does not owe. [`super::analyze`]'s probe reads
/// `status` because trusty-analyze's HTTP route really did answer 503.
///
/// What: one `search.health` frame; `true` only for a result frame. A dial
/// failure and an error frame are both `false`, so an RPC error can never read
/// as a healthy daemon and let the audit proceed onto a daemon that is not
/// serving.
/// Test: `super::tests::{a_reachable_search_daemon_is_not_restarted,
/// a_search_daemon_that_refuses_health_is_not_reachable}`.
async
/// The exact argument vector the guard hands `trusty-search`.
///
/// Why: this list IS the tga→trusty-search contract, and `--foreground` is
/// load-bearing rather than decorative — a bare `trusty-search start` re-spawns
/// itself as a background daemon and the parent exits, which is why
/// trusty-search's own guard passes the flag
/// (`crates/trusty-search/src/commands/daemon_guard.rs`'s
/// `spawn_daemon_with_device`). We already detach the child ourselves, so the
/// second fork would only cost the guard its view of the process it started.
/// Building the vector in a pure function is what lets a test assert its contents
/// without spawning anything.
/// What: `start --foreground`.
/// Test: `super::tests::the_search_spawn_arguments_are_start_in_the_foreground`.
pub
/// Ensure the trusty-search daemon is up before anything else in the audit.
///
/// Why/What: see the module docs. Resolves the guard from the environment and
/// delegates to [`ensure_search_daemon_with`], so the environment and the
/// discovery files are read exactly once, at the public entry point.
///
/// # Errors
///
/// [`SearchDaemonUnavailable`] when the daemon is absent and cannot be started.
///
/// Test: `super::tests::a_reachable_search_daemon_is_not_restarted`.
pub async
/// [`ensure_search_daemon`] with the address, binary and budgets already fixed.
///
/// Why: taking them as a value is what lets a test drive the whole spawn-and-poll
/// path against a stub executable on an ephemeral port, without touching the
/// process environment or leaving a daemon behind.
/// What: calls `search.health` on the guard's socket; on a result frame,
/// returns without spawning anything. On anything else, spawns
/// `<binary> start --foreground` detached and polls until it answers. Neither
/// failure is downgraded — a spawn that fails and a daemon that never answers
/// both return `Err`.
///
/// The socket is resolved once, before the spawn, and the poll reuses it, so a
/// daemon this guard starts is found on the same path the daemon's own clients
/// derive.
///
/// # Errors
///
/// [`SearchDaemonUnavailable`] carrying the spawn error, or the readiness timeout.
///
/// Test: `super::tests::{a_reachable_search_daemon_is_not_restarted,
/// an_unspawnable_search_binary_refuses_the_audit,
/// a_search_daemon_that_never_comes_up_refuses_the_audit,
/// the_search_guard_recovers_a_stale_degraded_analyze_daemon}`.
pub async