OXYGENHUB
# c7161ec1a607 spectra / src / cli / setup.rs
6.2 KiB · Rust · last changed spectra status, probe and bridge: what the VS Code extension drives (#31) 1 day ago source blame history raw
c7161ec1 Andy McCall ↶ 1
//! Commands a program uses to set Spectra up and to talk to it (spec
2
//! §18.1): `spectra status` says what is set up, `spectra probe` what a
3
//! server is before trusting it, and `spectra bridge` signs for a program
4
//! that cannot (the VS Code extension).
5
6
use super::identity::owner_home;
7
use super::{ui, Ctx};
8
use anyhow::{bail, Context, Result};
9
use serde_json::{json, Value};
10
use spectra_mcp::client::{host_port, Client, Endpoint};
11
use spectra_mcp::tls::Pinned;
12
use std::path::PathBuf;
13
14
/// `spectra status [--json]`: the identity, the server or local Spectra.
15
pub async fn status(ctx: &Ctx, as_json: bool) -> Result<i32> {
16
    let home = owner_home(ctx, None);
17
    let identity = spectra_core::keys::KeyPair::load_home(&home).ok();
18
    let remote = ctx.remote()?;
19
    let initialised = ctx.data.join("spectra.db").exists();
20
    let serving = initialised && Client::alive(&ctx.socket()).await;
21
    let reachable = match &remote {
22
        Some(c) => Client::at(c.endpoint()?, None).send("GET", "/api/v1/health", &[], vec![]).await.is_ok_and(|r| r.status < 500),
23
        None => serving,
24
    };
25
    let v = json!({
26
        "version": env!("SPECTRA_VERSION_LONG"),
27
        "home": home.display().to_string(),
28
        "identity": identity.as_ref().map(|p| json!({ "name": p.identity.name, "key": p.key.to_hex() })),
29
        "server": remote.as_ref().map(|c| json!({ "url": c.server, "instance": c.instance })),
30
        "local": { "data": ctx.data.display().to_string(), "initialised": initialised, "serving": serving },
31
        "reachable": reachable,
32
    });
33
    if as_json {
34
        println!("{v}");
35
        return Ok(0);
36
    }
37
    println!("{}", ui::kv("version", env!("SPECTRA_VERSION_LONG")));
38
    match &identity {
39
        Some(p) => println!("{}", ui::kv("you", format!("{}{}{}", ui::author(&p.identity.name), ui::sep(), ui::hash(p.key.short(12))))),
40
        None => println!("{}", ui::kv("you", ui::warn("no identity yet: spectra identity create --name NAME"))),
41
    }
42
    match &remote {
43
        Some(c) => println!("{}", ui::kv("spectra", format!("{}{}", ui::path(&c.server), if reachable { "" } else { " (not answering)" }))),
44
        None if initialised => println!(
45
            "{}",
46
            ui::kv("spectra", format!("{}{}", ui::path(ctx.data.display()), if serving { " (serving)" } else { " (not serving)" }))
47
        ),
48
        None => println!("{}", ui::kv("spectra", ui::warn("none yet: spectra init here, or spectra connect https://HOST:7350"))),
49
    }
50
    Ok(0)
51
}
52
53
/// `spectra probe URL [--json]`: who a server says it is, checked against
54
/// its certificate, without trusting it.
55
pub async fn probe(url: &str, as_json: bool) -> Result<i32> {
56
    let addr = host_port(url)?;
57
    let seen = Pinned::new(None);
58
    let c = Client::at(Endpoint::Tls { addr: addr.clone(), config: seen.client_config()? }, None);
59
    let r = c.send("GET", "/api/v1/instance", &[], vec![]).await.with_context(|| format!("reaching https://{addr}"))?;
60
    if r.status != 200 {
61
        bail!("https://{addr} answered {} to /api/v1/instance: is it a Spectra?", r.status);
62
    }
63
    let answer = r.json()?;
64
    let (_, pin) = seen.seen().context("the server showed no certificate")?;
65
    let checked = spectra_mcp::instance::check(&answer, &pin)?;
66
    let mine = spectra_core::keys::KeyPair::load_home(&spectra_core::keys::default_home()).ok();
67
    let waiting = answer.get("named_owner").and_then(Value::as_str).map(String::from);
68
    let v = json!({
69
        "url": format!("https://{addr}"),
70
        "name": checked.name,
71
        "instance": checked.instance.hash().to_hex(),
72
        "owner": checked.owner,
73
        "waiting_for": waiting,
74
        "yours": mine.as_ref().is_some_and(|m| {
75
            let me = m.key.to_hex();
76
            checked.owner.as_deref() == Some(me.as_str()) || waiting.as_deref() == Some(me.as_str())
77
                || [checked.owner.as_deref(), waiting.as_deref()].into_iter().flatten()
78
                    .any(|k| spectra_core::identity::KeyId::from_hex(k).is_some_and(|k| spectra_core::keys::identity_in(&spectra_core::keys::default_home(), &k).is_some()))
79
        }),
80
        "tls_spki": hex::encode(pin),
81
    });
82
    if as_json {
83
        println!("{v}");
84
    } else {
85
        println!("{}", ui::kv("server", ui::path(v["url"].as_str().unwrap_or(""))));
86
        println!("{}", ui::kv("name", v["name"].as_str().unwrap_or("")));
87
        println!("{}", ui::kv("instance", ui::hash(v["instance"].as_str().unwrap_or(""))));
88
        let whose = match (v["owner"].as_str(), v["waiting_for"].as_str()) {
89
            (Some(o), _) => format!("owned by {}", &o[..12.min(o.len())]),
90
            (None, Some(w)) => format!("waiting to be claimed by {}", &w[..12.min(w.len())]),
91
            _ => "no owner".into(),
92
        };
93
        println!("{}", ui::kv("owner", whose));
94
    }
95
    Ok(0)
96
}
97
98
/// `spectra bridge [--port 0]`: sign as the owner for the program that
99
/// started it; prints `{ "url", "token" }` and ends with standard input.
100
pub async fn bridge(ctx: &Ctx, port: u16, owner_home_flag: Option<PathBuf>) -> Result<i32> {
101
    let home = owner_home(ctx, owner_home_flag);
102
    let client =
103
        ctx.owner_client(&home).await?.context("no Spectra to bridge to: spectra init here, or spectra connect https://HOST:7350")?;
104
    let listener = tokio::net::TcpListener::bind(("127.0.0.1", port)).await.with_context(|| format!("binding 127.0.0.1:{port}"))?;
105
    let port = listener.local_addr()?.port();
106
    let token = spectra_mcp::bridge::token();
107
    println!("{}", json!({ "url": format!("http://127.0.0.1:{port}"), "token": token }));
108
    use std::io::Write;
109
    std::io::stdout().flush().ok();
110
    let app = spectra_mcp::bridge::router(client, token, port);
111
    axum::serve(listener, app)
112
        .with_graceful_shutdown(async {
113
            // Ends when the program that started it closes our input, or on Ctrl-C.
114
            let stdin = async {
115
                let mut buf = [0u8; 256];
116
                let mut input = tokio::io::stdin();
117
                loop {
118
                    match tokio::io::AsyncReadExt::read(&mut input, &mut buf).await {
119
                        Ok(0) | Err(_) => return,
120
                        Ok(_) => {}
121
                    }
122
                }
123
            };
124
            tokio::select! {
125
                _ = stdin => {}
126
                _ = tokio::signal::ctrl_c() => {}
127
            }
128
        })
129
        .await?;
130
    Ok(0)
131
}