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}