feat(orchestrator): read ORCHESTRATOR_PORT env var for configurable API port (#1113)

* feat(orchestrator): read ORCHESTRATOR_PORT env var for configurable API port

The orchestrator internal API port was hardcoded to 50051 in two places
(ContainerJobConfig and OrchestratorApi::start call), making it impossible
to run multiple IronClaw instances on the same host — the second instance
fails with "Address already in use".

NETWORK_SECURITY.md already documents ORCHESTRATOR_PORT as configurable,
and ContainerJobConfig.orchestrator_port is propagated to worker containers
via IRONCLAW_ORCHESTRATOR_URL, but the env var was never actually read.

Extract resolve_orchestrator_port() that reads ORCHESTRATOR_PORT and falls
back to 50051. Includes tests for valid, invalid, and out-of-range values.

* test: add ENV_LOCK mutex for env-var test serialization

Address Gemini review: add std::sync::Mutex to serialize env var access
across test threads. Keep unsafe blocks — required in Rust edition 2024
where std::env::set_var/remove_var are unsafe functions.

---------

Co-authored-by: SMKRV <[email protected]>
This commit is contained in:
smkrv
2026-03-16 07:58:48 +00:00
committed by GitHub
co-authored by SMKRV
parent 877f117096
commit 0245c0f9e9
+49 -2
View File
@@ -51,6 +51,15 @@ use crate::db::Database;
use crate::llm::LlmProvider;
use crate::secrets::SecretsStore;
/// Resolve the orchestrator port from the `ORCHESTRATOR_PORT` environment
/// variable, falling back to 50051.
fn resolve_orchestrator_port() -> u16 {
std::env::var("ORCHESTRATOR_PORT")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(50051)
}
/// Result of orchestrator setup, containing all handles needed by the agent.
pub struct OrchestratorSetup {
pub container_job_manager: Option<Arc<ContainerJobManager>>,
@@ -101,11 +110,12 @@ pub async fn setup_orchestrator(
let job_event_tx = Some(tx);
let token_store = TokenStore::new();
let orchestrator_port = resolve_orchestrator_port();
let job_config = ContainerJobConfig {
image: config.sandbox.image.clone(),
memory_limit_mb: config.sandbox.memory_limit_mb,
cpu_shares: config.sandbox.cpu_shares,
orchestrator_port: 50051,
orchestrator_port,
claude_code_api_key: std::env::var("ANTHROPIC_API_KEY").ok(),
claude_code_oauth_token: crate::config::ClaudeCodeConfig::extract_oauth_token(),
claude_code_model: config.claude_code.model.clone(),
@@ -127,7 +137,7 @@ pub async fn setup_orchestrator(
};
tokio::spawn(async move {
if let Err(e) = OrchestratorApi::start(orchestrator_state, 50051).await {
if let Err(e) = OrchestratorApi::start(orchestrator_state, orchestrator_port).await {
tracing::error!("Orchestrator API failed: {}", e);
}
});
@@ -151,3 +161,40 @@ pub async fn setup_orchestrator(
docker_status,
}
}
#[cfg(test)]
mod tests {
use std::sync::Mutex;
use super::*;
/// Serialize access to `ORCHESTRATOR_PORT` env var across test threads.
static ENV_LOCK: Mutex<()> = Mutex::new(());
#[test]
fn resolve_orchestrator_port_from_env() {
let _guard = ENV_LOCK.lock().unwrap();
// Safety: env-var mutation requires unsafe in edition 2024;
// ENV_LOCK serializes concurrent access from other test threads.
// Absent env var → default 50051
unsafe { std::env::remove_var("ORCHESTRATOR_PORT") };
assert_eq!(resolve_orchestrator_port(), 50051);
// Valid custom port
unsafe { std::env::set_var("ORCHESTRATOR_PORT", "50052") };
assert_eq!(resolve_orchestrator_port(), 50052);
// Non-numeric value → fallback to default
unsafe { std::env::set_var("ORCHESTRATOR_PORT", "not_a_port") };
assert_eq!(resolve_orchestrator_port(), 50051);
// Out of u16 range → fallback to default
unsafe { std::env::set_var("ORCHESTRATOR_PORT", "99999") };
assert_eq!(resolve_orchestrator_port(), 50051);
// Cleanup
unsafe { std::env::remove_var("ORCHESTRATOR_PORT") };
}
}