- 2001
1 - 2002
} - 2003
Ok(vak_flow::PlanOutcome::Failed { node, reason }) => { - 2004
eprintln!("── plan execution failed at '{node}': {reason}"); - 2005
1 - 2006
} - 2007
Ok(vak_flow::PlanOutcome::Aborted) => { - 2008
eprintln!("── aborted"); - 2009
1 - 2010
} - 2011
Err(e) => { - 2012
eprintln!("error: planner crashed: {e}"); - 2013
2 - 2014
} - 2015
} - 2016
} - 2017
- 2018
fn builtin_cases() -> Vec<vak_eval::EvalCase> { - 2019
vak_eval::builtin_suite() - 2020
.into_iter() - 2021
.chain(vak_eval::general_suite()) - 2022
.collect() - 2023
} - 2024
- 2025
async fn run_eval( - 2026
report_path: Option<PathBuf>, - 2027
live: bool, - 2028
provider_flag: Option<String>, - 2029
model_flag: Option<String>, - 2030
) -> i32 { - 2031
let mut reports = Vec::new(); - 2032
let worker_exe = match std::env::current_exe() { - 2033
Ok(executable) => executable, - 2034
Err(error) => { - 2035
eprintln!("error: tool broker unavailable: {error}"); - 2036
return 2; - 2037
} - 2038
}; - 2039
- 2040
if !live { - 2041
for case in builtin_cases() { - 2042
let r = vak_eval::run_case_brokered(&case, worker_exe.clone()).await; - 2043
println!( - 2044
"{:<24} {:>6} in {:>5} / out {:>4} {:>5}ms {}", - 2045
r.task_id, - 2046
if r.passed { "PASS" } else { "FAIL" }, - 2047
r.tokens_in, - 2048
r.tokens_out, - 2049
r.duration_ms, - 2050
r.error.as_deref().unwrap_or("") - 2051
); - 2052
reports.push(r); - 2053
} - 2054
// Deterministic context-engine gate (docs/design/68-context-engine.md - 2055
// "Verification") — no model calls; planner, projection and - 2056
// two-model replay properties over a fixture ledger. - 2057
let card = match vak_eval::run_context_engine_scorecard() { - 2058
Ok(card) => card, - 2059
Err(e) => { - 2060
eprintln!("context scorecard harness error: {e}"); - 2061
return 1; - 2062
} - 2063
}; - 2064
println!("{card}"); - 2065
if !card.passed() { - 2066
eprintln!("context scorecard FAILED"); - 2067
return 1; - 2068
} - 2069
} else { - 2070
let core = match Core::new(std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))) { - 2071
Ok(c) => c, - 2072
Err(e) => { - 2073
eprintln!("error: {e}"); - 2074
return 2; - 2075
} - 2076
}; - 2077
if provider_flag.is_some() || model_flag.is_some() { - 2078
let route = core.effective_route(); - 2079
core.set_route( - 2080
provider_flag.unwrap_or(route.provider), - 2081
model_flag.unwrap_or(route.model), - 2082
); - 2083
} - 2084
let provider = match core.provider() { - 2085
Ok(p) => p, - 2086
Err(e) => { - 2087
eprintln!("error: {e}"); - 2088
return 2; - 2089
} - 2090
}; - 2091
let model = core.effective_model().clone(); - 2092
eprintln!("live eval against {} / {model}", core.effective_provider()); - 2093
for case in &vak_eval::live_suite() { - 2094
let r = vak_eval::run_case_with_provider_brokered( - 2095
case, - 2096
provider.clone(), - 2097
&model, - 2098
worker_exe.clone(), - 2099
) - 2100
.await; - 2101
println!( - 2102
"{:<24} {:>6} in {:>5} / out {:>4} {:>5}ms {}", - 2103
r.task_id, - 2104
if r.passed { "PASS" } else { "FAIL" }, - 2105
r.tokens_in, - 2106
r.tokens_out, - 2107
r.duration_ms, - 2108
r.error.as_deref().unwrap_or("") - 2109
); - 2110
reports.push(r); - 2111
} - 2112
} - 2113
- 2114
let passed = reports.iter().filter(|r| r.passed).count(); - 2115
let total = reports.len(); - 2116
let tokens_in: u64 = reports.iter().map(|r| r.tokens_in).sum(); - 2117
let tokens_out: u64 = reports.iter().map(|r| r.tokens_out).sum(); - 2118
- 2119
if let Some(path) = report_path { - 2120
let json = serde_json::to_string_pretty(&serde_json::json!({ - 2121
"generated_at": chrono::Utc::now(), - 2122
"passed": passed, - 2123
"total": total, - 2124
"tokens_in": tokens_in, - 2125
"tokens_out": tokens_out, - 2126
"cases": reports, - 2127
})) - 2128
.unwrap_or_default(); - 2129
if let Some(parent) = std::path::Path::new(&path).parent() { - 2130
let _ = std::fs::create_dir_all(parent); - 2131
} - 2132
match std::fs::write(&path, json) { - 2133
Ok(_) => eprintln!("report written to {}", path.display()), - 2134
Err(e) => { - 2135
eprintln!("error writing report: {e}"); - 2136
return 2; - 2137
} - 2138
} - 2139
} - 2140
- 2141
println!("\n{passed}/{total} passed · tokens in {tokens_in} / out {tokens_out}"); - 2142
if passed == total { 0 } else { 1 } - 2143
} - 2144
- 2145
/// Open a running server's web surface, already signed in. - 2146
/// - 2147
/// The point is that nobody should have to go and find this machine's - 2148
/// access token to reach a server on this machine. The token is read from - 2149
/// the pinned Shared secret scope and handed over as a one-shot `?token=`, - 2150
/// which the client immediately exchanges for a session cookie and erases - 2151
/// from the address bar. - 2152
/// - 2153
/// LOOPBACK ONLY, and not by convention: the server refuses `?token=` from - 2154
/// any non-loopback host (invariant 34), so this URL authenticates nothing - 2155
/// if it leaves the machine. That is why the convenience is safe to offer - 2156
/// at all. - 2157
fn run_open(surface: cli::OpenSurface, port: Option<u16>, print_only: bool) -> i32 { - 2158
let path = match surface { - 2159
cli::OpenSurface::App => "/app", - 2160
cli::OpenSurface::Admin => "/admin", - 2161
}; - 2162
let port = port.unwrap_or_else(|| vak_ops::OpsConfig::detect().port); - 2163
let base = format!("http://127.0.0.1:{port}{path}"); - 2164
- 2165
// No token pinned is not an error: the server then mints one per boot, - 2166
// and the sign-in screen is the honest answer. - 2167
let url = match vak_config::get_var("VAK_GATEWAY_TOKEN").filter(|t| !t.trim().is_empty()) { - 2168
Some(token) => format!( - 2169
"{base}?token={}", - 2170
percent_encoding::utf8_percent_encode(token.trim(), percent_encoding::NON_ALPHANUMERIC) - 2171
), - 2172
None => { - 2173
eprintln!( - 2174
"note: no VAK_GATEWAY_TOKEN pinned, so this link cannot sign you in.\n\ - 2175
Run `vak self services-sync` to pin one." - 2176
); - 2177
base.clone() - 2178
} - 2179
}; - 2180
- 2181
if print_only { - 2182
println!("{url}"); - 2183
return 0; - 2184
} - 2185
let opener = if cfg!(target_os = "macos") { - 2186
"open" - 2187
} else { - 2188
"xdg-open" - 2189
}; - 2190
match std::process::Command::new(opener).arg(&url).spawn() { - 2191
Ok(_) => { - 2192
// The token is deliberately NOT echoed here; the browser has it. - 2193
println!("opening {base}"); - 2194
0 - 2195
} - 2196
Err(e) => { - 2197
eprintln!("could not launch a browser ({e}); open this yourself:\n{url}"); - 2198
1 - 2199
} - 2200
} - 2201
} - 2202
- 2203
/// Whether this process is running inside a container. - 2204
/// - 2205
/// `/.dockerenv` is Docker's own marker; `VAK_CONTAINER` is set by our - 2206
/// image so the check also holds under runtimes that do not create it - 2207
/// (Podman, containerd). This is a USABILITY guard, not a privilege - 2208
/// boundary — anyone who can set the variable can already pass any flag - 2209
/// they like — so detecting it loosely is fine. - 2210
fn in_container() -> bool { - 2211
std::path::Path::new("/.dockerenv").exists() - 2212
|| std::env::var("VAK_CONTAINER").is_ok_and(|v| v == "1") - 2213
} - 2214
- 2215
async fn run_serve( - 2216
cwd: PathBuf, - 2217
port: u16, - 2218
host: Option<String>, - 2219
gateway: bool, - 2220
trusted: bool, - 2221
) -> i32 { - 2222
// Not `Cli`: this process serves API clients and, with `--gateway`, chat - 2223
// channels. The gateway re-stamps each inbound message with its own - 2224
// channel (`vak-server/src/gateway.rs`); this is the fallback for a - 2225
// plain HTTP caller. - 2226
let core = match Core::new_with_trust(cwd, trusted) - 2227
.map(|c| c.with_surface(vak_core::Surface::Server)) - 2228
{ - 2229
Ok(c) => c, - 2230
Err(e) => { - 2231
eprintln!("error: {e}"); - 2232
return 2; - 2233
} - 2234
}; - 2235
print_config_warnings(&core); - 2236
- 2237
// Where to listen. `--host` wins over `[server] bind`, and both default - 2238
// to loopback — so an existing install keeps behaving exactly as it did. - 2239
let server = core.config().server.clone(); - 2240
let bind = host.unwrap_or_else(|| server.bind.clone()); - 2241
let publicly = !matches!(bind.as_str(), "127.0.0.1" | "::1" | "localhost"); - 2242
- 2243
// Refuse, rather than warn. Binding a shell-capable agent to a - 2244
// reachable interface with no `trusted_hosts` means every request from - 2245
// the network is rejected 421 anyway — so the server would appear to - 2246
// start and then answer nothing, which is the worst of both outcomes. - 2247
// Failing here says exactly which setting is missing while the operator - 2248
// is still looking at the terminal. - 2249
// - 2250
// EXCEPT in a container, where 0.0.0.0 is the only address that can be - 2251
// reached at all and the access control is `docker run -p` — an - 2252
// explicit, deliberate act by the operator, which is exactly what this - 2253
// refusal exists to require. Refusing here would mean every container - 2254
// needs `trusted_hosts` before it can serve its own published port, so - 2255
// the guard would be worked around rather than obeyed. - 2256
// - 2257
// What does NOT relax is the `Host` check itself (invariant 34): DNS - 2258
// rebinding is defended identically inside a container, because that - 2259
// attack does not care where the process runs. - 2260
if publicly && server.trusted_hosts.is_empty() && in_container() { - 2261
eprintln!( - 2262
"note: binding {bind} inside a container; reachability is governed by\n\ - 2263
the published port. Add [server] trusted_hosts to serve a real hostname." - 2264
); - 2265
} else if publicly && server.trusted_hosts.is_empty() { - 2266
eprintln!( - 2267
"error: refusing to bind {bind} with no [server] trusted_hosts.\n\ - 2268
\n\ - 2269
A non-loopback bind exposes this agent — including its tools — to\n\ - 2270
whoever can reach the port. Name the hostnames you will actually\n\ - 2271
use in .vak/config.toml (or the user config):\n\ - 2272
\n\ - 2273
[server]\n\ - 2274
bind = \"{bind}\"\n\ - 2275
trusted_hosts = [\"vak.example.com\"]\n\ - 2276
public_url = \"https://vak.example.com\" # enables Secure cookies\n\ - 2277
\n\ - 2278
If you only need remote access for yourself, an SSH tunnel needs\n\ - 2279
none of this: ssh -N -L {port}:127.0.0.1:{port} <host>" - 2280
); - 2281
return 2; - 2282
} - 2283
- 2284
let ip: std::net::IpAddr = match bind.as_str() { - 2285
"localhost" => std::net::IpAddr::from([127, 0, 0, 1]), - 2286
other => match other.parse() { - 2287
Ok(ip) => ip, - 2288
Err(_) => { - 2289
eprintln!("error: [server] bind is not an IP address: {other}"); - 2290
return 2; - 2291
} - 2292
}, - 2293
}; - 2294
let addr = std::net::SocketAddr::new(ip, port); - 2295
if publicly { - 2296
eprintln!( - 2297
"listening on {addr} — reachable beyond this machine. Trusted hosts: {}", - 2298
server.trusted_hosts.join(", ") - 2299
); - 2300
} - 2301
match vak_server::serve_with(core, addr, gateway).await { - 2302
Ok(()) => 0, - 2303
Err(e) => { - 2304
eprintln!("error: {e}"); - 2305
2 - 2306
} - 2307
} - 2308
} - 2309
- 2310
/// How to tell someone to set a secret, now that there is no file to name - 2311
/// — every bridge's "where do I put this token" hint reads the same. - 2312
/// Resolved through `vak_config::credentials` (OS keychain or the - 2313
/// encrypted-file fallback), so the actionable advice is the Settings UI, - 2314
/// `PUT /config/key`, or an environment variable — never a path to edit - 2315
/// by hand. - 2316
fn env_hint() -> &'static str { - 2317
"the Settings UI, PUT /config/key, or an environment variable" - 2318
} - 2319
- 2320
/// Env-first gateway token so it stays out of `ps`/plist arguments, - 2321
/// shared by every bridge subcommand. - 2322
fn bridge_gateway_token(token_flag: Option<String>) -> Option<String> { - 2323
match token_flag { - 2324
Some(t) if !t.trim().is_empty() => Some(t), - 2325
_ => match vak_config::get_var("VAK_GATEWAY_TOKEN") { - 2326
Some(t) if !t.trim().is_empty() => Some(t), - 2327
_ => { - 2328
eprintln!( - 2329
"error: gateway token missing — set VAK_GATEWAY_TOKEN via {}, or pass --token", - 2330
env_hint() - 2331
); - 2332
None - 2333
} - 2334
}, - 2335
} - 2336
} - 2337
- 2338
/// Credential-store-aware bot-token lookup so a bridge credential never - 2339
/// has to be exported by hand. - 2340
fn bridge_bot_token(env_var: &str) -> Option<String> { - 2341
match vak_config::get_var(env_var) { - 2342
Some(t) if !t.trim().is_empty() => Some(t), - 2343
_ => { - 2344
eprintln!("error: {env_var} is not set — set it via {}", env_hint()); - 2345
None - 2346
} - 2347
} - 2348
} - 2349
- 2350
/// Resolve which env var a bridge should read its token from: the legacy - 2351
/// single per-surface slot by default, or — when `--bot-id` names a bot - 2352
/// created in the admin console's Bots list (multi-bot-per-channel, - 2353
/// docs/design/34) — that bot's own env var, so a second `vak telegram - 2354
/// --bot-id ...` process can run alongside the first with a different - 2355
/// token. `None` only when `--bot-id` was given but no such bot exists. - 2356
fn bridge_token_env_var(legacy_env_var: &str, bot_id: Option<&str>) -> Option<String> { - 2357
match bot_id { - 2358
None => Some(legacy_env_var.to_string()), - 2359
Some(id) => { - 2360
let sessions_home = vak_config::paths::data_home(); - 2361
match vak_server::gateway::bot_token_env_for_id(&sessions_home, id) { - 2362
Some(env_var) => Some(env_var), - 2363
None => { - 2364
eprintln!( - 2365
"error: no bot '{id}' — create it first in the admin console's Bots list" - 2366
); - 2367
None - 2368
} - 2369
} - 2370
} - 2371
} - 2372
} - 2373
- 2374
/// `vak discord` (docs/design/34 Phase 3) — same flag shape as - 2375
/// `vak telegram`, same env-first credential handling. - 2376
async fn run_discord(server: String, token_flag: Option<String>, bot_id: Option<String>) -> i32 { - 2377
let Some(env_var) = bridge_token_env_var("DISCORD_BOT_TOKEN", bot_id.as_deref()) else { - 2378
return 2; - 2379
}; - 2380
let (Some(token), Some(bot_token)) = - 2381
(bridge_gateway_token(token_flag), bridge_bot_token(&env_var)) - 2382
else { - 2383
return 2; - 2384
}; - 2385
let bridge = - 2386
vak_server::surfaces::discord::DiscordBridge::from_env(server, token, bot_token, bot_id); - 2387
println!( - 2388
"discord bridge: {} -> {} ({} channel(s))", - 2389
bridge.api_base, - 2390
bridge.gateway_url, - 2391
bridge.channel_ids.len() - 2392
); - 2393
match bridge.run().await { - 2394
Ok(()) => 0, - 2395
Err(e) => { - 2396
eprintln!("error: {e}"); - 2397
1 - 2398
} - 2399
} - 2400
} - 2401
- 2402
/// `vak slack` (docs/design/34 Phase 3). - 2403
async fn run_slack(server: String, token_flag: Option<String>, bot_id: Option<String>) -> i32 { - 2404
let Some(env_var) = bridge_token_env_var("SLACK_BOT_TOKEN", bot_id.as_deref()) else { - 2405
return 2; - 2406
}; - 2407
let (Some(token), Some(bot_token)) = - 2408
(bridge_gateway_token(token_flag), bridge_bot_token(&env_var)) - 2409
else { - 2410
return 2; - 2411
}; - 2412
let bridge = - 2413
vak_server::surfaces::slack::SlackBridge::from_env(server, token, bot_token, bot_id); - 2414
println!( - 2415
"slack bridge: {} -> {} ({} channel(s))", - 2416
bridge.api_base, - 2417
bridge.gateway_url, - 2418
bridge.channel_ids.len() - 2419
); - 2420
match bridge.run().await { - 2421
Ok(()) => 0, - 2422
Err(e) => { - 2423
eprintln!("error: {e}"); - 2424
1 - 2425
} - 2426
} - 2427
} - 2428
- 2429
async fn run_telegram(server: String, token_flag: Option<String>, bot_id: Option<String>) -> i32 { - 2430
let Some(env_var) = bridge_token_env_var("TELEGRAM_BOT_TOKEN", bot_id.as_deref()) else { - 2431
return 2; - 2432
}; - 2433
let Some(token) = bridge_gateway_token(token_flag) else { - 2434
return 2; - 2435
}; - 2436
let Some(bot_token) = bridge_bot_token(&env_var) else { - 2437
return 2; - 2438
}; - 2439
let api_base = vak_config::get_var("TELEGRAM_API_BASE") - 2440
.unwrap_or_else(|| "https://api.telegram.org".to_string()); - 2441
// Single-instance guard keyed by bot token: a second local bridge - 2442
// fails fast with the holder's identity instead of flapping 409s. - 2443
let locks_dir = Some(vak_config::paths::data_home().join("locks")); - 2444
let bridge = vak_server::surfaces::telegram::TelegramBridge { - 2445
token_env: env_var.clone(), - 2446
api_base, - 2447
bot_token: bot_token.clone(), - 2448
gateway_url: server.trim_end_matches('/').to_string(), - 2449
gateway_token: token, - 2450
locks_dir, - 2451
bot_id, - 2452
}; - 2453
println!( - 2454
"telegram bridge: {} -> {}", - 2455
bridge.api_base, bridge.gateway_url - 2456
); - 2457
match bridge.run().await { - 2458
Ok(()) => 0, - 2459
Err(e) => { - 2460
eprintln!("error: {e}"); - 2461
1 - 2462
} - 2463
} - 2464
} - 2465
- 2466
#[cfg(test)] - 2467
mod reflection_line_tests { - 2468
use super::exec_reflection_line; - 2469
- 2470
#[test] - 2471
fn reflected_outcomes_get_a_footnote_even_at_zero_notes() { - 2472
let line = exec_reflection_line(&vak_core::reflection::ReflectionOutcome::Reflected { - 2473
notes_added: 2, - 2474
skills_proposed: false, - 2475
}); - 2476
assert_eq!(line.as_deref(), Some("· reflected: 2 note(s)")); - 2477
assert_eq!( - 2478
exec_reflection_line(&vak_core::reflection::ReflectionOutcome::Reflected { - 2479
notes_added: 0, - 2480
skills_proposed: true, - 2481
}) - 2482
.as_deref(), - 2483
Some("· reflected: 0 note(s)") - 2484
); - 2485
} - 2486
- 2487
#[test] - 2488
fn budget_skips_are_actionable_and_other_skips_silent() { - 2489
assert_eq!( - 2490
exec_reflection_line(&vak_core::reflection::ReflectionOutcome::Skipped { - 2491
reason: "budget" - 2492
}) - 2493
.as_deref(), - 2494
Some("· reflection skipped: budget cap reached") - 2495
); - 2496
for quiet in [ - 2497
"reflection-disabled", - 2498
"already-in-flight", - 2499
"reflect-call-failed", - 2500
] { - 2501
assert_eq!( - 2502
exec_reflection_line(&vak_core::reflection::ReflectionOutcome::Skipped { - 2503
reason: quiet - 2504
}), - 2505
None, - 2506
"{quiet} must stay silent" - 2507
); - 2508
} - 2509
} - 2510
} - 2511
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.