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
//! Shared HTTP server scaffolding for trusty-* daemons.
//!
//! Why: Every trusty-* daemon wants the same axum middleware stack (permissive
//! CORS for local browser UIs, a tracing layer, gzip compression) and the same
//! fast-fail reqwest client when one daemon calls another. Centralising removes
//! drift between trusty-search, future trusty-memory daemons, etc.
//!
//! What: pure helpers — no global state.
//! - [`with_standard_middleware`] layers CORS/Trace/Compression on a router.
//! - [`with_guarded_middleware`] additionally applies the router-wide
//! same-origin write guard ([`origin_guard`], #3304) so destructive daemon
//! write routes are not exposed to cross-origin CSRF.
//! - [`daemon_http_client`] builds a reqwest client with short timeouts so
//! CLI commands never hang on a missing daemon.
//!
//! Test: `cargo test -p trusty-common --features axum-server` covers router
//! composition (smoke) and client construction (timeouts surfaced through
//! the public `reqwest::Client` API — we just assert no error on build).
use ;
use ;
use json;
use Duration;
use ;
pub use ;
/// Apply the standard trusty-* middleware stack to an axum router.
///
/// Why: Local browser-based UIs (trusty-search SPA, future dashboards) need
/// permissive CORS to talk to `127.0.0.1:<port>`; every daemon benefits from
/// request tracing for debugging; gzip is a cheap wire-size win.
/// What: layers `CorsLayer` (any origin/methods/headers), `TraceLayer` (HTTP
/// span), and `CompressionLayer` (gzip) in that order. The order matters:
/// CORS must run on every response (including 404s from inner routes), and
/// compression should be outermost so the trace span captures the encoded
/// size if needed.
///
/// Compression skips `text/event-stream` (SSE) responses: gzip's trailer is
/// only flushed at stream close, so a fast-completing SSE response leaves the
/// client (reqwest) mid-decode and surfaces as
/// `Transport error: error decoding response body`. tower-http 0.5 ships
/// `NotForContentType::SSE` ("text/event-stream") for exactly this case; we
/// compose it with `DefaultPredicate` so all other heuristics (min size, no
/// already-compressed media) still apply.
/// Test: smoke-tested via the dependent crates' integration tests — any
/// regression breaks `cargo test -p trusty-search-service`.
/// Apply the standard middleware stack PLUS the router-wide same-origin write
/// guard (#3304).
///
/// Why: the sibling trusty-* daemons (search, memory, analyze, mpm) inherit the
/// permissive-CORS [`with_standard_middleware`] stack, which leaves their
/// DESTRUCTIVE write routes (daemon shutdown, index/palace/drawer deletion,
/// session spawn/stop, the `/rpc` JSON-RPC surface) open to cross-origin CSRF
/// from any page the operator visits. This helper composes
/// [`guard_write_origin`] into that stack so every daemon adopts the console's
/// proven guard (#3280) router-wide with a one-line change, instead of each
/// re-implementing it (architecture review tranche 1).
/// What: layers [`guard_write_origin`] (via
/// [`axum::middleware::from_fn_with_state`] carrying `self_origins`) as the
/// INNERMOST middleware — closest to the routes, so it wraps every route
/// including those merged in later — then applies [`with_standard_middleware`]
/// (compression/trace/CORS) on top. The guard is method-gated (only
/// POST/PUT/PATCH/DELETE), so `GET` reads and SSE/WebSocket upgrades pass
/// through untouched; it fails open on a missing `Origin` header, so all
/// server-side callers (the console reverse proxy, `curl`, the MCP stdio
/// bridge) keep working. Pass `SelfOrigins::default()` for a loopback-only bind
/// or `SelfOrigins::from_bind_addrs(&addrs)` to additionally trust the daemon's
/// own non-loopback (e.g. Tailscale) bind address (#3269).
/// Test: `with_guarded_middleware_composes` below; consuming crates' per-daemon
/// guard regression tests.
/// Build a `reqwest::Client` configured for daemon-to-daemon calls.
///
/// Why: every CLI command that talks to the daemon must fail fast when the
/// daemon is not running. Without timeouts, reqwest waits for the OS TCP
/// stack (minutes on some platforms), freezing the terminal.
/// What: 2 s connect timeout, 5 s total request timeout. Returns
/// `anyhow::Result` so callers can `?`-propagate alongside other anyhow
/// errors without conversion boilerplate.
/// Test: `daemon_http_client_builds` — construction succeeds with the
/// configured timeouts; the timeout values themselves are exercised in the
/// dependent CLIs (manual: stop daemon, run `trusty-search status`).
/// Standard health-check handler returning `{"status":"ok","version":"<v>"}`.
///
/// Why: trusty-search and trusty-memory both expose `/health`, but their
/// payload shapes drifted (one returned plain `"ok"`, the other JSON with
/// version). Centralising gives every trusty-* daemon the same JSON contract
/// so monitoring tooling (curl probes, MCP supervisors) can rely on a single
/// shape.
/// What: returns a 200 OK with body `{"status":"ok","version":"<version>"}`.
/// The `version` argument is `&'static str` so callers can pass
/// `env!("CARGO_PKG_VERSION")` without allocation.
/// Usage: `.route("/health", get(|| health_handler(env!("CARGO_PKG_VERSION"))))`
/// Test: `health_handler_returns_expected_json` exercises the handler
/// directly and asserts the JSON body.
pub async