use std::path::PathBuf;
use std::sync::Arc;
use serde::Deserialize;
use serde_json::{Value, json};
use super::super::{Daemon, PLATFORM_TOOLS, arg_org, audit, authorize_class};
use crate::error::{Error, Result};
use crate::org::OrgId;
use crate::server::http::{Request, Response, TlsConfig};
use crate::server::{Hooks, Listener, Routes};
use crate::servers::upgrade;
use crate::servers::wire::Assertion;
pub struct AgentState {
pub orgs: Arc<crate::servers::store::AgentOrgs>,
pub tls: TlsConfig,
pub tls_dir: PathBuf,
}
pub(super) fn org_bound(tool: &str, scoped: bool) -> bool {
if !scoped && super::super::CROSS_ORG_READS.contains(&tool) {
return false;
}
!(tool == "org_list"
|| PLATFORM_TOOLS.contains(&tool)
&& !matches!(tool, "org_create" | "org_update" | "org_delete"))
}
pub(super) fn agent_hooks(
base: &Hooks,
orgs: Arc<crate::servers::store::AgentOrgs>,
ssh: Option<crate::server::ssh::Ssh>,
) -> Hooks {
use crate::server::Authenticated;
let authn: crate::server::mcp::Authn = Arc::new(|req, _| match req.header("authorization") {
Some(h) => match Assertion::from_header(h) {
Ok(x) => Authenticated::User(Arc::new(x.principal())),
Err(_) => Authenticated::Refused,
},
None => Authenticated::None,
});
let authorize: crate::server::mcp::Authorize = Arc::new(move |c, tool, args, scope| {
let args = authorize_class(
c,
&tool.name,
audit::class_for(tool, &args),
args,
scope,
false,
)?;
if org_bound(&tool.name, scope.is_some()) {
let org = arg_org(&args)?;
if !orgs.contains(&org) {
return Err(Error::Forbidden(format!(
"org {org} is not placed on this server"
)));
}
}
Ok(args)
});
Hooks {
authn: Some(authn),
authorize: Some(authorize),
events: base.events.clone(),
terminal: base.terminal.clone(),
ssh,
audit: base.audit.clone(),
route: None,
listed: base.listed.clone(),
refuse_anonymous: true,
}
}
pub(in crate::daemon) fn agent_listener(
d: Arc<Daemon>,
base: &Hooks,
ac: &super::super::AgentConfig,
webhooks: Routes,
) -> Result<Listener> {
let tls = crate::servers::pki::agent_server_config(&ac.tls_dir)?;
let a = Arc::new(AgentState {
orgs: Arc::new(crate::servers::store::AgentOrgs::open(&d.state_dir)?),
tls: Arc::new(std::sync::RwLock::new(tls)),
tls_dir: ac.tls_dir.clone(),
});
let placed: Vec<String> = a.orgs.list().iter().map(|o| o.to_string()).collect();
eprintln!(
"isb serve: agent for a control plane (build {}); orgs placed here: {}",
upgrade::build_id(),
placed.join(", ")
);
let ssh = super::super::ssh::forwarded(d.clone());
Ok(Listener::mtls(ac.listen.clone(), a.tls.clone())
.hooks(agent_hooks(base, a.orgs.clone(), Some(ssh)))
.routes(internal_routes(d, a))
.public_routes(webhooks))
}
fn bad(status: u16, code: &str, m: impl Into<String>) -> Response {
Response::json(status, &json!({"error": code, "message": m.into()}))
}
fn internal_routes(d: Arc<Daemon>, a: Arc<AgentState>) -> Routes {
Arc::new(move |req: &Request| {
let p = req.path.strip_prefix("/internal/v1/")?;
let cp = req
.header("authorization")
.and_then(|h| Assertion::from_header(h).ok())
.is_some_and(|x| x.via == "control-plane");
if !cp {
return Some(bad(403, "forbidden", "the control plane's own calls only"));
}
Some(match (req.method.as_str(), p) {
("GET", "heartbeat") => Response::json(200, &heartbeat(&d, &a)),
("POST", "orgs") => {
#[derive(Deserialize)]
struct B {
org: OrgId,
placed: bool,
}
match serde_json::from_slice::<B>(&req.body) {
Ok(b) if b.org.is_default() => {
bad(400, "invalid", "the default org is the control plane's")
}
Ok(b) => match a.orgs.set(&b.org, b.placed) {
Ok(()) => {
eprintln!(
"isb serve: org {} {} this server",
b.org,
if b.placed { "placed on" } else { "taken off" }
);
Response::json(200, &json!({"ok": true, "orgs": a.orgs.list()}))
}
Err(e) => bad(500, "error", e.to_string()),
},
Err(e) => bad(400, "invalid", e.to_string()),
}
}
("POST", "cert") => match rotate(&a, &req.body) {
Ok(fp) => Response::json(200, &json!({"ok": true, "fingerprint": fp})),
Err(e) => bad(400, "invalid", e.to_string()),
},
("POST", p) if p.starts_with("upgrade/") => upgrade_route(&d, &p[8..], req),
_ => bad(404, "not_found", "no such internal route"),
})
})
}
fn upgrade_route(d: &Daemon, what: &str, req: &Request) -> Response {
#[derive(Deserialize)]
struct B {
sha256: String,
#[serde(default)]
size: u64,
}
let dir = upgrade::dir(&d.state_dir);
let json = || serde_json::from_slice::<B>(&req.body).map_err(Error::from);
let r = match what {
"chunk" => {
let offset = req
.query
.as_deref()
.and_then(|q| q.strip_prefix("offset="))
.and_then(|o| o.parse::<u64>().ok());
match offset {
Some(o) => upgrade::stage_chunk(&dir, o, &req.body).map(|n| json!({"staged": n})),
None => Err(Error::invalid("offset= is required")),
}
}
"apply" => json().and_then(|b| {
upgrade::stage_apply(&dir, &b.sha256, b.size)?;
eprintln!("isb serve: staged an upgrade to build {}", b.sha256);
Ok(json!({"ok": true}))
}),
"confirm" => json().and_then(|b| {
upgrade::confirm(&dir, &b.sha256)?;
eprintln!("isb serve: the control plane confirmed build {}", b.sha256);
Ok(json!({"ok": true}))
}),
_ => return bad(404, "not_found", "no such internal route"),
};
match r {
Ok(v) => Response::json(200, &v),
Err(e) => bad(400, "invalid", e.to_string()),
}
}
pub(super) fn rotate(a: &AgentState, body: &[u8]) -> Result<String> {
use crate::servers::pki;
#[derive(Deserialize)]
struct B {
cert: String,
key: String,
}
let b: B = serde_json::from_slice(body)?;
let ca = std::fs::read_to_string(a.tls_dir.join(pki::AGENT_CA))?;
let leaf = pki::Leaf {
cert: b.cert,
key: b.key,
};
let cfg = pki::server_config(&ca, &leaf)?;
pki::write_private(&a.tls_dir.join(pki::AGENT_KEY), &leaf.key)?;
pki::write_private(&a.tls_dir.join(pki::AGENT_CERT), &leaf.cert)?;
*a.tls.write().unwrap_or_else(|p| p.into_inner()) = cfg;
let fp = leaf.fingerprint()?;
eprintln!("isb serve: now serving certificate {fp}");
Ok(fp)
}
fn heartbeat(d: &Daemon, a: &AgentState) -> Value {
let mut v = status(d, a);
if let (Some(m), Value::Object(extra)) =
(v.as_object_mut(), upgrade::heartbeat_fields(&d.state_dir))
{
m.extend(extra);
}
v
}
fn status(d: &Daemon, a: &AgentState) -> Value {
let snap = d.ctl.snapshot();
let incus = d
.client
.server_info()
.ok()
.map(|i| i["environment"]["server_version"].clone());
let (_, evs) = d.ctl.events(0, 1000);
let last_error = evs
.iter()
.rev()
.find(|e| e.level == "error")
.map(|e| json!({"at": e.at, "stack": e.stack, "message": e.message}));
json!({
"isb": env!("CARGO_PKG_VERSION"),
"incus": incus,
"host": {
"hostname": snap.host.hostname,
"cpus": snap.host.cpus,
"cpu_pct": snap.host.cpu_pct,
"load1": snap.host.load1,
"mem_used": snap.host.mem_used,
"mem_total": snap.host.mem_total,
"disk_used": snap.host.disk_used,
"disk_total": snap.host.disk_total,
},
"orgs": a.orgs.list(),
"stacks": d.ctl.list().len(),
"last_error": last_error,
})
}