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_CURRENT_INTERVAL",
47    "AGENT_COMPLETIONS_BACKOFF_INITIAL_INTERVAL",
48    "AGENT_COMPLETIONS_BACKOFF_RANDOMIZATION_FACTOR",
49    "AGENT_COMPLETIONS_BACKOFF_MULTIPLIER",
50    "AGENT_COMPLETIONS_BACKOFF_MAX_INTERVAL",
51    "AGENT_COMPLETIONS_BACKOFF_MAX_ELAPSED_TIME",
52    "MCP_BACKOFF_CURRENT_INTERVAL",
53    "MCP_BACKOFF_INITIAL_INTERVAL",
54    "MCP_BACKOFF_RANDOMIZATION_FACTOR",
55    "MCP_BACKOFF_MULTIPLIER",
56    "MCP_BACKOFF_MAX_INTERVAL",
57    "MCP_BACKOFF_MAX_ELAPSED_TIME",
58    "GITHUB_BACKOFF_CURRENT_INTERVAL",
59    "GITHUB_BACKOFF_INITIAL_INTERVAL",
60    "GITHUB_BACKOFF_RANDOMIZATION_FACTOR",
61    "GITHUB_BACKOFF_MULTIPLIER",
62    "GITHUB_BACKOFF_MAX_INTERVAL",
63    "GITHUB_BACKOFF_MAX_ELAPSED_TIME",
64    "AGENT_COMPLETIONS_FIRST_CHUNK_TIMEOUT",
65    "AGENT_COMPLETIONS_OTHER_CHUNK_TIMEOUT",
66    "MCP_CONNECT_TIMEOUT",
67    "MCP_CALL_TIMEOUT",
68    "REVERSE_CHANNEL_TIMEOUT",
69    "MCP_ENCRYPTION_KEY",
70    "OBJECTIVEAI_STATE",
71    "PERSISTENT_CACHE_TRANSIENT_TTL_MS",
72    "MOCK_DELAY_MS",
73    "MOCK_MAX_TOOL_CALLS",
74    "ADDRESS",
75    "PORT",
76];
77
78/// The spawn flow itself, callable in-process (used by
79/// `Context::api_client()` as well as the `api spawn` command).
80/// Idempotent and cheap when the server is already up: a try_read of
81/// the lock returns the published URL without spawning.
82pub async fn spawn(ctx: &Context) -> Result<String, Error> {
83    let bin = if cfg!(windows) {
84        "objectiveai-api.exe"
85    } else {
86        "objectiveai-api"
87    };
88    let exe = ctx.filesystem.bin_dir().join(bin);
89    let lock_dir = ctx.filesystem.bin_dir().join("locks");
90
91    // Project the configured `api.mcp_timeout_ms` onto the spawned api's
92    // env — but ONLY when the user explicitly set it. The single value
93    // fans out to the connect timeout, the per-call timeout, and the
94    // backoff max-elapsed-time (the api forwards these to the proxy it
95    // spawns, so it drives every server-side MCP connection too). The
96    // keys are scrubbed above, so when `mcp_timeout_ms` is unset we leave
97    // them unset and the api resolves them itself (`<OBJECTIVEAI_DIR>/
98    // .env`, then its built-in default) — identical to passing the
99    // canonical default in production, and it preserves the test `.env`'s
100    // `MCP_BACKOFF_*=0` fast-fail on the shared (machine-wide,
101    // first-spawn-wins) api. See `Context::resolve_mcp_timeout_ms_opt`.
102    let timeout_ms = ctx.resolve_mcp_timeout_ms_opt().await?;
103
104    crate::spawn::spawn_until_lock_published(&exe, &lock_dir, "api", |cmd| {
105        for key in API_CONFIG_ENV {
106            cmd.env_remove(key);
107        }
108        cmd.env("OBJECTIVEAI_DIR", ctx.filesystem.dir())
109            .env("SUPPRESS_OUTPUT", "true");
110        if let Some(timeout_ms) = timeout_ms {
111            let v = timeout_ms.to_string();
112            cmd.env("MCP_CONNECT_TIMEOUT", &v)
113                .env("MCP_CALL_TIMEOUT", &v)
114                .env("MCP_BACKOFF_MAX_ELAPSED_TIME", &v);
115        }
116    })
117    .await
118}
119
120pub async fn execute(ctx: &Context, _request: Request) -> Result<Response, Error> {
121    Ok(Response {
122        listening: spawn(ctx).await?,
123    })
124}
125
126pub mod request_schema {
127    use objectiveai_sdk::cli::command::api::spawn as sdk;
128    use objectiveai_sdk::cli::command::api::spawn::request_schema::{Request, Response};
129
130    use crate::context::Context;
131    use crate::error::Error;
132
133    pub async fn execute(_ctx: &Context, _request: Request) -> Result<Response, Error> {
134        Ok(objectiveai_sdk::cli::command::ResponseSchema(schemars::schema_for!(sdk::Request)))
135    }
136}
137
138pub mod response_schema {
139    use objectiveai_sdk::cli::command::api::spawn as sdk;
140    use objectiveai_sdk::cli::command::api::spawn::response_schema::{Request, Response};
141
142    use crate::context::Context;
143    use crate::error::Error;
144
145    pub async fn execute(_ctx: &Context, _request: Request) -> Result<Response, Error> {
146        Ok(objectiveai_sdk::cli::command::ResponseSchema(schemars::schema_for!(sdk::Response)))
147    }
148}