This repository has been archived on 2026-10-08. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
maestro/crates/maestro-cli/src/policy.rs
T
naps62-yoloandClaude Opus 4.8 3b60f67332
ci / rust (push) Successful in 9m49s
fix(maestro): give CLI reqwest clients connect + request timeouts (NAP-262)
The `RegistryClient` and the bare `reqwest::Client::new()` calls in
`policy.rs` built clients with no timeouts, so `maestro list` / `maestro
policy {show,reload}` would hang forever against a registry/daemon that
is TCP-alive but not making forward progress (SIGSTOP, swap thrash,
deadlock behind slow disk).

Centralize construction in `http::build_client()`, which applies a 5s
connect timeout and a 30s request timeout, and route every call site
through it. Covered by a test that points the client at a peer which
accepts the connection but never responds and asserts the request times
out instead of hanging.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 19:22:58 +00:00

128 lines
3.6 KiB
Rust

//! `maestro policy reload | show` — talk to the local daemon's
//! `/api/policy` endpoints (NAP-237).
//!
//! The local daemon's bind port is read from `[daemon].bind_addr` in the
//! unified config; the host is always `127.0.0.1` (these endpoints are
//! same-host by design — the daemon's bind may be `0.0.0.0` for attach,
//! but operators reload policy from the same machine).
use std::io::Write;
use std::path::Path;
use maestro_config::MaestroConfigFile;
use serde::Deserialize;
const DEFAULT_PORT: u16 = 7070;
#[derive(Debug, thiserror::Error)]
pub enum PolicyError {
#[error("config: {0}")]
Config(#[from] maestro_config::ConfigError),
#[error("http: {0}")]
Http(#[from] reqwest::Error),
#[error("daemon returned {status}: {body}")]
DaemonError {
status: reqwest::StatusCode,
body: String,
},
#[error("io: {0}")]
Io(#[from] std::io::Error),
}
#[derive(Debug, Deserialize)]
pub struct PolicyView {
pub allow_inbound: Vec<String>,
}
pub async fn run_reload(config_path: &Path, out: &mut impl Write) -> Result<(), PolicyError> {
let url = format!("{}/api/policy/reload", local_base_url(config_path)?);
let resp = crate::http::build_client().post(&url).send().await?;
let status = resp.status();
if !status.is_success() {
let body = resp.text().await.unwrap_or_default();
return Err(PolicyError::DaemonError { status, body });
}
let view: PolicyView = resp.json().await?;
writeln!(
out,
"reloaded — allow_inbound now has {} entr{}",
view.allow_inbound.len(),
if view.allow_inbound.len() == 1 {
"y"
} else {
"ies"
},
)?;
for entry in &view.allow_inbound {
writeln!(out, " {entry}")?;
}
Ok(())
}
pub async fn run_show(
config_path: &Path,
json: bool,
out: &mut impl Write,
) -> Result<(), PolicyError> {
let url = format!("{}/api/policy", local_base_url(config_path)?);
let resp = crate::http::build_client().get(&url).send().await?;
let status = resp.status();
if !status.is_success() {
let body = resp.text().await.unwrap_or_default();
return Err(PolicyError::DaemonError { status, body });
}
let view: PolicyView = resp.json().await?;
if json {
writeln!(
out,
"{}",
serde_json::to_string(&view.allow_inbound).unwrap()
)?;
} else if view.allow_inbound.is_empty() {
writeln!(
out,
"(allow_inbound is empty — every cross-daemon attach is denied)"
)?;
} else {
for entry in &view.allow_inbound {
writeln!(out, "{entry}")?;
}
}
Ok(())
}
/// `http://127.0.0.1:<port>` where `<port>` is `[daemon].bind_addr`'s port
/// or `7070` if unset / file missing.
fn local_base_url(config_path: &Path) -> Result<String, PolicyError> {
let file = MaestroConfigFile::load_or_default(config_path)?;
let port = file
.daemon
.bind_addr
.map(|a| a.port())
.unwrap_or(DEFAULT_PORT);
Ok(format!("http://127.0.0.1:{port}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn local_base_url_uses_bind_port_or_default() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.toml");
// missing file → default
assert_eq!(local_base_url(&path).unwrap(), "http://127.0.0.1:7070");
std::fs::write(
&path,
r#"
[daemon]
bind_addr = "0.0.0.0:9090"
"#,
)
.unwrap();
assert_eq!(local_base_url(&path).unwrap(), "http://127.0.0.1:9090");
}
}