Skip to main content

objectiveai_cli/command/viewer/
spawn.rs

1//! `viewer spawn` — start the `objectiveai-viewer` Tauri shell in the
2//! background.
3//!
4//! The viewer is per-state: its lock lives at
5//! `<dir>/state/<state>/locks` key `viewer`. The viewer is a WebSocket
6//! CLIENT of the daemon's broadcast (not a server), so the lock content
7//! is a plain readiness marker, not a URL. If the lock is already held
8//! the viewer is already up.
9
10use objectiveai_sdk::cli::command::viewer::spawn::{Request, Response};
11
12use crate::context::Context;
13use crate::error::Error;
14
15/// The spawn flow itself. Idempotent and cheap when the viewer is
16/// already up: a try_read of the lock returns without spawning.
17pub async fn spawn(ctx: &Context) -> Result<String, Error> {
18    // The viewer requires the daemon's ws:// connect URL. `run`'s
19    // producer tee ensured the daemon and recorded the address on the
20    // ctx before this handler ran; if it's absent the daemon couldn't
21    // be spawned, and a viewer without a daemon is useless — error out.
22    let daemon_address = ctx
23        .daemon_address()
24        .ok_or(Error::DaemonAddressUnavailable)?
25        .to_string();
26
27    let bin = if cfg!(windows) {
28        "objectiveai-viewer.exe"
29    } else {
30        "objectiveai-viewer"
31    };
32    let exe = ctx.filesystem.bin_dir().join(bin);
33    let lock_dir = ctx.filesystem.state_dir().join("locks");
34
35    // Derive the daemon WS auth signature from the cli's DAEMON_SECRET
36    // (env-sourced): `sha256=<hex(SHA256(secret))>` — the same one-way
37    // math as `generate_viewer_secret_signature_pair`. Clients send it
38    // verbatim in the first-message auth preamble (the SDK
39    // `AuthEnvelope`) on every daemon WebSocket connection.
40    let daemon_signature = ctx.config.daemon_secret.as_deref().map(|secret| {
41        use sha2::{Digest, Sha256};
42        let hash = Sha256::digest(secret.as_bytes());
43        format!(
44            "sha256={}",
45            hash.iter().map(|b| format!("{:02x}", b)).collect::<String>()
46        )
47    });
48
49    // The child inherits the cli's environment; every env key the
50    // viewer's config reads (`EnvConfigBuilder` in
51    // `objectiveai-viewer/src-tauri/src/run.rs`: DAEMON_ADDRESS,
52    // DAEMON_SIGNATURE, SUPPRESS_OUTPUT, OBJECTIVEAI_DIR,
53    // OBJECTIVEAI_STATE) is set explicitly here when known.
54    // DAEMON_ADDRESS is the daemon's full ws:// connect URL (always
55    // set — required above; note it shadows any inherited value, which
56    // in the cli's namespace would be a bind address, a different
57    // semantic). DAEMON_SIGNATURE is derived from DAEMON_SECRET when
58    // the cli has one; otherwise any inherited DAEMON_SIGNATURE from
59    // the invoking shell is left as-is (the spawner may know the
60    // signature without the secret).
61    crate::spawn::spawn_until_lock_published(&exe, &lock_dir, "viewer", |cmd| {
62        cmd.env("OBJECTIVEAI_DIR", ctx.filesystem.dir())
63            .env("OBJECTIVEAI_STATE", ctx.filesystem.state())
64            .env("SUPPRESS_OUTPUT", "true")
65            .env("DAEMON_ADDRESS", &daemon_address);
66        if let Some(signature) = daemon_signature {
67            cmd.env("DAEMON_SIGNATURE", signature);
68        }
69    })
70    .await
71}
72
73pub async fn execute(ctx: &Context, _request: Request) -> Result<Response, Error> {
74    Ok(Response {
75        listening: spawn(ctx).await?,
76    })
77}
78
79pub mod request_schema {
80    use objectiveai_sdk::cli::command::viewer::spawn as sdk;
81    use objectiveai_sdk::cli::command::viewer::spawn::request_schema::{Request, Response};
82
83    use crate::context::Context;
84    use crate::error::Error;
85
86    pub async fn execute(_ctx: &Context, _request: Request) -> Result<Response, Error> {
87        Ok(objectiveai_sdk::cli::command::ResponseSchema(schemars::schema_for!(sdk::Request)))
88    }
89}
90
91pub mod response_schema {
92    use objectiveai_sdk::cli::command::viewer::spawn as sdk;
93    use objectiveai_sdk::cli::command::viewer::spawn::response_schema::{Request, Response};
94
95    use crate::context::Context;
96    use crate::error::Error;
97
98    pub async fn execute(_ctx: &Context, _request: Request) -> Result<Response, Error> {
99        Ok(objectiveai_sdk::cli::command::ResponseSchema(schemars::schema_for!(sdk::Response)))
100    }
101}