1use std::sync::Arc;
19use std::time::Duration;
20
21use axum::routing::post;
22use axum::{Json, Router};
23use bytes::Bytes;
24use firstpass_core::{GENESIS_HASH, verify_chain};
25use serde_json::{Value, json};
26
27use crate::provider::ProviderRegistry;
28use crate::proxy::AppState;
29use crate::{ProxyConfig, app, store};
30
31type Fail = Box<dyn std::error::Error>;
34
35pub async fn run() -> Result<(), Fail> {
40 let upstream = spawn_upstream().await?;
42
43 let db = std::env::temp_dir().join(format!("firstpass-demo-{}.db", uuid::Uuid::now_v7()));
45 let (proxy, _writer) = spawn_proxy(&upstream, &db).await?;
46
47 println!("\n\x1b[1mFirstpass demo\x1b[0m — routing one request through a real enforce ladder");
48 println!("no API keys, no config: the upstream is local, everything else is the real proxy\n");
49
50 let client = reqwest::Client::new();
52 let served: Value = client
53 .post(format!("{proxy}/v1/messages"))
54 .header("x-api-key", "byok-demo")
55 .json(&json!({
56 "model": "claude-haiku-4-5",
57 "max_tokens": 256,
58 "messages": [{ "role": "user", "content": "write a hello world in rust" }],
59 }))
60 .send()
61 .await?
62 .json()
63 .await?;
64
65 println!("served output : {}", served["content"][0]["text"]);
66 println!("served model : {}\n", served["model"]);
67
68 let trace = wait_for_trace(&db).await.ok_or("no trace recorded")?;
70 println!("\x1b[1m── audit receipt ──────────────────────────────────\x1b[0m");
71 for a in &trace.attempts {
72 let verdict = match a.verdict {
73 firstpass_core::Verdict::Pass => "\x1b[32mPASS\x1b[0m",
74 firstpass_core::Verdict::Fail => "\x1b[31mFAIL\x1b[0m",
75 firstpass_core::Verdict::Abstain => "\x1b[33mABSTAIN\x1b[0m",
76 };
77 println!(
78 " rung {} · {:<28} · {verdict} · ${:.4}",
79 a.rung, a.model, a.cost_usd
80 );
81 }
82 let f = &trace.final_;
83 println!(" ─────────────────────────────────────────────────");
84 println!(" total ${:.4}", f.total_cost_usd);
85 println!(
86 " baseline ${:.4} (always top-tier)",
87 f.counterfactual_baseline_usd
88 );
89 let pct = if f.counterfactual_baseline_usd > 0.0 {
90 f.savings_usd / f.counterfactual_baseline_usd * 100.0
91 } else {
92 0.0
93 };
94 println!(
95 " \x1b[32mSAVED ${:.4} ({pct:.0}% cheaper at proven quality)\x1b[0m",
96 f.savings_usd
97 );
98 println!(" trace_id {}", trace.trace_id);
99 println!(
100 " chain {}\n",
101 if verify_chain(std::slice::from_ref(&trace), &trace.prev_hash).is_ok() {
102 "verified ✓"
103 } else {
104 "BROKEN"
105 }
106 );
107
108 let trace_id = trace.trace_id.to_string();
110 let fb = client
111 .post(format!("{proxy}/v1/feedback"))
112 .json(&json!({
113 "trace_id": trace_id, "gate_id": "ci-tests",
114 "verdict": "pass", "reporter": "github-actions",
115 }))
116 .send()
117 .await?;
118 println!(
119 "feedback POST /v1/feedback → {} (downstream outcome recorded)",
120 fb.status()
121 );
122
123 let view = store::load_trace_view(&db, "default", &trace_id)?.ok_or("trace view missing")?;
124 let reporter = view
125 .deferred
126 .first()
127 .map_or("(none)", |d| d.reporter.as_str());
128 println!(
129 "deferred verdicts on trace: {} ({reporter} reported it)",
130 view.deferred.len()
131 );
132 let all = store::load_all_traces(&db)?;
133 println!(
134 "audit chain after feedback : {}\n",
135 if verify_chain(&all, GENESIS_HASH).is_ok() {
136 "\x1b[32mstill verified ✓\x1b[0m — the sealed record never changed"
137 } else {
138 "BROKEN"
139 }
140 );
141 println!("next: `firstpass onboard` wires your own agent through this in observe mode.\n");
142
143 let _ = std::fs::remove_file(&db);
144 Ok(())
145}
146
147async fn spawn_upstream() -> Result<String, Fail> {
150 async fn messages(body: Bytes) -> Json<Value> {
151 let model = serde_json::from_slice::<Value>(&body)
152 .ok()
153 .and_then(|v| v.get("model").and_then(Value::as_str).map(str::to_owned))
154 .unwrap_or_default();
155 let text = if model.contains("haiku") {
156 ""
157 } else {
158 "fn main() { println!(\"hello world\"); }"
159 };
160 Json(json!({
161 "id": "msg_demo", "type": "message", "role": "assistant", "model": model,
162 "content": [{ "type": "text", "text": text }],
163 "usage": { "input_tokens": 1200, "output_tokens": 220 },
164 }))
165 }
166 let router = Router::new().route("/v1/messages", post(messages));
167 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
168 let addr = listener.local_addr()?;
169 tokio::spawn(async move {
170 let _ = axum::serve(listener, router).await;
171 });
172 Ok(format!("http://{addr}"))
173}
174
175async fn spawn_proxy(
178 upstream: &str,
179 db: &std::path::Path,
180) -> Result<(String, tokio::task::JoinHandle<()>), Fail> {
181 let routing = "[[route]]\nmatch = {}\nmode = \"enforce\"\nladder = [\"anthropic/claude-haiku-4-5\", \"anthropic/claude-sonnet-5\", \"anthropic/claude-opus-4-8\"]\ngates = [\"non-empty\"]\n";
182 let (up, dbs) = (upstream.to_owned(), db.to_string_lossy().into_owned());
183 let config = ProxyConfig::from_lookup(move |k| match k {
184 "FIRSTPASS_UPSTREAM_ANTHROPIC" => Some(up.clone()),
185 "FIRSTPASS_MODE" => Some("enforce".to_owned()),
186 "FIRSTPASS_CONFIG_TOML" => Some(routing.to_owned()),
187 "FIRSTPASS_DB" => Some(dbs.clone()),
188 _ => None,
189 })?;
190 let providers = ProviderRegistry::new(&config.upstream_anthropic, &config.upstream_openai);
191 let (traces, writer) = store::open(db)?;
192 let state = AppState {
193 config: Arc::new(config),
194 http: reqwest::Client::new(),
195 providers,
196 gate_health: Arc::new(crate::gate::GateHealthRegistry::new()),
197 shadow_ledger: Arc::new(crate::shadow::ShadowLedger::new()),
198 guardrails: Arc::new(crate::guard::GuardrailRegistry::new()),
199 traces,
200 adaptive: None,
201 bandit: None,
202 predictor: None,
203 tenant_rate_limiter: None,
204 spill: None,
205 };
206 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
207 let addr = listener.local_addr()?;
208 let router = app(state)?;
209 tokio::spawn(async move {
210 let _ = axum::serve(listener, router).await;
211 });
212 Ok((format!("http://{addr}"), writer))
213}
214
215async fn wait_for_trace(db: &std::path::Path) -> Option<firstpass_core::Trace> {
217 for _ in 0..150 {
218 if let Ok(t) = store::load_all_traces(db)
219 && let Some(first) = t.into_iter().next()
220 {
221 return Some(first);
222 }
223 tokio::time::sleep(Duration::from_millis(20)).await;
224 }
225 None
226}