Skip to main content

objectiveai_cli/command/api/
spawn.rs

1//! `api spawn` — start the `objectiveai-api` server in the background.
2//!
3//! The api is machine-wide (one per `OBJECTIVEAI_DIR`): its lock lives
4//! at `<dir>/bin/locks` key `api`, and the lock contents are the
5//! server's client-connect URL. If the lock is already held the server
6//! is already up and its published URL is returned as-is.
7
8use objectiveai_sdk::cli::command::api::spawn::{Request, Response};
9
10use crate::context::Context;
11use crate::error::Error;
12
13/// Every env key the api's config reads (`EnvConfigBuilder` in
14/// `objectiveai-api/src/run.rs`) except `OBJECTIVEAI_DIR`, which the
15/// spawn sets explicitly. The child inherits the cli's environment
16/// (toolchain + machine identity — the dev `bin` entries are cargo-run
17/// shims); scrubbing exactly the api's own config keys keeps the
18/// spawning shell's settings from leaking into a server that other
19/// processes will share. Deliberately scrubbed rather than forwarded:
20/// OBJECTIVEAI_STATE (the api is global; it resolves its own state
21/// default rather than inheriting whichever state the spawning cli
22/// happened to run in) and ADDRESS/PORT (the api defaults to
23/// 127.0.0.1 on an ephemeral port and publishes the bound URL in its
24/// lock). The api's config then comes from `<OBJECTIVEAI_DIR>/.env`
25/// (its main loads it non-overridingly) and its own defaults.
26const API_CONFIG_ENV: &[&str] = &[
27    "OBJECTIVEAI_ADDRESS",
28    "OBJECTIVEAI_AUTHORIZATION",
29    "OPENROUTER_ADDRESS",
30    "OPENROUTER_AUTHORIZATION",
31    "GITHUB_AUTHORIZATION",
32    "MCP_AUTHORIZATION",
33    "USER_AGENT",
34    "HTTP_REFERER",
35    "X_TITLE",
36    "COMMIT_AUTHOR_NAME",
37    "COMMIT_AUTHOR_EMAIL",
38    "CLAUDE_AGENT_SDK_ENABLED",
39    "CLAUDE_AGENT_SDK_RATE_LIMIT_MAX_RETRIES",
40    "CLAUDE_AGENT_SDK_RATE_LIMIT_MAX_WAIT_SECS",
41    "CLAUDE_AGENT_SDK_QUERY_LIMIT",
42    "CODEX_SDK_ENABLED",
43    "CODEX_SDK_RATE_LIMIT_MAX_RETRIES",
44    "CODEX_SDK_RATE_LIMIT_MAX_WAIT_SECS",
45    "CODEX_SDK_QUERY_LIMIT",
46    "AGENT_COMPLETIONS_BACKOFF_MAX_ELAPSED_TIME",
47    "MCP_BACKOFF_MAX_ELAPSED_TIME",
48    "GITHUB_BACKOFF_MAX_ELAPSED_TIME",
49    "AGENT_COMPLETIONS_FIRST_CHUNK_TIMEOUT",
50    "AGENT_COMPLETIONS_OTHER_CHUNK_TIMEOUT",
51    "MCP_CONNECT_TIMEOUT",
52    "MCP_CALL_TIMEOUT",
53    "REVERSE_CHANNEL_TIMEOUT",
54    "MCP_ENCRYPTION_KEY",
55    "OBJECTIVEAI_STATE",
56    "MOCK_DELAY_MS",
57    "MOCK_MAX_TOOL_CALLS",
58    "ADDRESS",
59    "PORT",
60];
61
62/// The spawn flow itself, callable in-process (used by
63/// `Context::api_client()` as well as the `api spawn` command).
64/// Idempotent and cheap when the server is already up: a try_read of
65/// the lock returns the published URL without spawning.
66pub async fn spawn(ctx: &Context) -> Result<String, Error> {
67    let bin = if cfg!(windows) {
68        "objectiveai-api.exe"
69    } else {
70        "objectiveai-api"
71    };
72    let exe = ctx.filesystem.bin_dir().join(bin);
73    let lock_dir = ctx.filesystem.bin_dir().join("locks");
74
75    // Project the two configured api knobs onto the spawned api's env —
76    // each ONLY when the user explicitly set it (the keys are scrubbed
77    // above, so when unset the api resolves them itself from
78    // `<OBJECTIVEAI_DIR>/.env` then its built-in default):
79    //   • `api.mcp_timeout_ms` → MCP_CONNECT_TIMEOUT + MCP_CALL_TIMEOUT
80    //     (the api forwards both to the proxy it spawns, so they drive
81    //     every server-side MCP connection too).
82    //   • `api.backoff_max_elapsed_time_ms` → EVERY backoff max-elapsed
83    //     env the api has (agent-completions, mcp, github).
84    // See `Context::resolve_mcp_timeout_ms_opt` /
85    // `resolve_backoff_max_elapsed_time_ms_opt`.
86    let timeout_ms = ctx.resolve_mcp_timeout_ms_opt().await?;
87    let backoff_ms = ctx.resolve_backoff_max_elapsed_time_ms_opt().await?;
88
89    crate::spawn::spawn_until_lock_published(&exe, &lock_dir, "api", |cmd| {
90        for key in API_CONFIG_ENV {
91            cmd.env_remove(key);
92        }
93        cmd.env("OBJECTIVEAI_DIR", ctx.filesystem.dir())
94            .env("SUPPRESS_OUTPUT", "true");
95        if let Some(timeout_ms) = timeout_ms {
96            let v = timeout_ms.to_string();
97            cmd.env("MCP_CONNECT_TIMEOUT", &v)
98                .env("MCP_CALL_TIMEOUT", &v);
99        }
100        if let Some(backoff_ms) = backoff_ms {
101            let v = backoff_ms.to_string();
102            cmd.env("AGENT_COMPLETIONS_BACKOFF_MAX_ELAPSED_TIME", &v)
103                .env("MCP_BACKOFF_MAX_ELAPSED_TIME", &v)
104                .env("GITHUB_BACKOFF_MAX_ELAPSED_TIME", &v);
105        }
106    })
107    .await
108}
109
110pub async fn execute(ctx: &Context, _request: Request) -> Result<Response, Error> {
111    Ok(Response {
112        listening: spawn(ctx).await?,
113    })
114}
115
116pub mod request_schema {
117    use objectiveai_sdk::cli::command::api::spawn as sdk;
118    use objectiveai_sdk::cli::command::api::spawn::request_schema::{Request, Response};
119
120    use crate::context::Context;
121    use crate::error::Error;
122
123    pub async fn execute(_ctx: &Context, _request: Request) -> Result<Response, Error> {
124        Ok(objectiveai_sdk::cli::command::ResponseSchema(schemars::schema_for!(sdk::Request)))
125    }
126}
127
128pub mod response_schema {
129    use objectiveai_sdk::cli::command::api::spawn as sdk;
130    use objectiveai_sdk::cli::command::api::spawn::response_schema::{Request, Response};
131
132    use crate::context::Context;
133    use crate::error::Error;
134
135    pub async fn execute(_ctx: &Context, _request: Request) -> Result<Response, Error> {
136        Ok(objectiveai_sdk::cli::command::ResponseSchema(schemars::schema_for!(sdk::Response)))
137    }
138}