Watch
0
0
Fork
You've already forked hyperhive
0

swarm-controller: state route refuses an agent the swarm has not declared on that hive

A placing state is accepted only for an agent declared or queued on the hive; anything else gets 404 telling the caller to create it at swarm level. swarm-ui's wanted badge no longer offers a first declaration for an undeclared row.
This commit is contained in:
atlas 2026-10-25 15:58:24 +01:00
commit 716290e4d2
3 changed files with 131 additions and 40 deletions

View file

@ -148,6 +148,7 @@ fn declaration_target(
responses(
(status = 200, description = "the declaration as now published", body = Vec<AgentDeclaration>),
(status = 400, description = "a name is not an identifier, the hive is not in this swarm, the state is unknown, or `agent` is new and reserved or refused by the forge (problem+json)", body = String),
(status = 404, description = "the swarm has neither declared nor queued `agent` on this hive; create it at swarm level first (problem+json)", body = String),
(status = 409, description = "the swarm has already placed `agent` on a different hive (problem+json)", body = String),
(status = 503, description = "no swarm queue is wired up, or it is not connected, or `agent` is new here and breaks a naming rule but whether it already exists could not be checked (problem+json)", body = String),
(status = 500, description = "the declaration could not be published, or another hive's wanted state could not be read (problem+json)", body = String),
@ -165,17 +166,17 @@ pub(crate) async fn set_agent_state(
.map_err(|reason| error_problem(axum::http::StatusCode::BAD_REQUEST, reason))?
.into_string();
// Only a first declaration of the name on this hive is checked, against
// what `create_agent` refuses: an agent already declared here changes
// state whatever its name. An undeclared name that passes is accepted:
// that is how the swarm adopts an agent that predates swarm-level
// creation. Deliberate, pending the operator's decision on whether a name
// the swarm never placed should be refused here instead.
// A placing state is accepted only for an agent the swarm already declared
// or queued on this hive; anything else is refused, so this route never
// creates an agent.
//
// The gate is held from the checks through the write. A destroy places
// nothing, so it is not checked and does not wait on creations.
let _gate = if places_agent(req.state) {
let gate = state.create_gate.lock().await;
// Before the declaration is read, so a creation finishing in between
// is in the snapshot or in the declaration.
let queued = queued_placements_now(&state);
let here = writer.view(&hive).await.map_err(|e| {
let detail = format!(
"cannot tell whether {agent:?} is already declared on hive {hive:?}, so it was \
@ -189,7 +190,7 @@ pub(crate) async fn set_agent_state(
here.as_ref(),
hive_types::reserved_names_raw().as_deref(),
|| in_roster(state.auth.as_deref(), &agent),
|| queued_placements_now(&state),
queued,
|| declarations_elsewhere(&state, &agent, &hive),
)
.await?;
@ -229,10 +230,11 @@ pub(crate) fn places_agent(state: swarm_wire::wanted::AgentState) -> bool {
}
}
/// Refuse a first placement of `agent` on `hive` that `create_agent`
/// refuses: 400 for a new name that breaks a naming rule, 409 for a name the
/// swarm has placed on another hive. An agent `here`, the hive's current
/// declaration, already declares in a placing state is placed on this hive,
/// Refuse a placement of `agent` on `hive` that the swarm never made there:
/// 400 for a new name that breaks a naming rule, 409 for a name the swarm has
/// placed on another hive, 404 for a name it has neither declared nor queued
/// on this hive. An agent `here`, the hive's current declaration, already
/// declares in a placing state, or one in `queued`, is placed on this hive,
/// and passes.
///
/// The roster is read only for a name that breaks a rule. Unlike in
@ -244,7 +246,7 @@ async fn refuse_placement<R, D>(
here: Option<&swarm_wire::wanted::HiveWanted>,
reserved: Option<&str>,
read_roster: impl FnOnce() -> R,
snapshot_queued: impl FnOnce() -> Vec<(String, String)>,
queued: Vec<(String, String)>,
read_declared: impl FnOnce() -> D,
) -> Result<(), problem_details::ProblemDetails>
where
@ -262,6 +264,7 @@ where
if declared_here {
return Ok(());
}
let queued_here = queued.iter().any(|(h, a)| h == hive && a == agent);
// Both calls log what they push here; this response has nowhere to
// carry warnings.
let mut warnings = Vec::new();
@ -278,7 +281,7 @@ where
name_verdict(agent, &broken, Ok(existing), &mut warnings)
.map_err(|detail| error_problem(axum::http::StatusCode::BAD_REQUEST, &detail))?;
}
let elsewhere = placements_elsewhere(agent, hive, snapshot_queued, read_declared).await?;
let elsewhere = placements_elsewhere(agent, hive, || queued, read_declared).await?;
if !elsewhere.is_empty() {
let detail = format!(
"agent {agent:?} is placed on hive {}, not {hive:?} — agent names are unique across \
@ -287,6 +290,17 @@ where
);
return Err(error_problem(axum::http::StatusCode::CONFLICT, &detail));
}
if !queued_here {
tracing::warn!(
hive,
agent,
"refused a state for an agent the swarm has not declared on this hive"
);
let detail = format!(
"agent {agent:?} is not declared at swarm level on hive {hive:?}; create it there"
);
return Err(error_problem(axum::http::StatusCode::NOT_FOUND, &detail));
}
Ok(())
}
@ -1853,7 +1867,7 @@ mod tests {
here.as_ref(),
Some("admin"),
|| std::future::ready(roster),
Vec::new,
Vec::new(),
|| std::future::ready(Ok(declared)),
)
.await
@ -1935,9 +1949,17 @@ mod tests {
Some(axum::http::StatusCode::SERVICE_UNAVAILABLE),
"{err:?}"
);
refuse_on_pr1ma_with("atlas", Vec::new(), unreadable())
.await
.expect("an ordinary name does not depend on the roster");
refuse_on_pr1ma_with(
"atlas",
vec![declaring(
"pr1ma",
"atlas",
swarm_wire::wanted::AgentState::Up,
)],
unreadable(),
)
.await
.expect("an ordinary name does not depend on the roster");
}
/// A destroyed agent is gone from its hive, so declaring it again is a
@ -1989,13 +2011,77 @@ mod tests {
assert!(format!("{err:?}").contains("sec0nd"), "{err:?}");
}
/// An agent that runs on `pr1ma` but was never declared anywhere, and is
/// not in the roster, is one the swarm adopts by declaring it.
/// A name the swarm never declared or queued on this hive is refused with
/// 404, naming the fix.
#[tokio::test]
async fn adopting_an_undeclared_agent_on_its_own_hive_is_accepted() {
refuse_on_pr1ma("legacy", Vec::new())
async fn an_undeclared_agent_is_refused() {
let err = refuse_on_pr1ma("legacy", Vec::new())
.await
.expect("an undeclared agent must still be adoptable");
.expect_err("an agent the swarm never declared must be refused");
assert_eq!(
err.status,
Some(axum::http::StatusCode::NOT_FOUND),
"{err:?}"
);
assert!(format!("{err:?}").contains("create it there"), "{err:?}");
}
/// An agent declared `Destroyed` here is gone from the hive, so it is
/// undeclared again.
#[tokio::test]
async fn a_destroyed_agent_is_refused_like_an_undeclared_one() {
use swarm_wire::wanted::AgentState;
let err = refuse_on_pr1ma(
"atlas",
vec![declaring("pr1ma", "atlas", AgentState::Destroyed)],
)
.await
.expect_err("a destroyed agent must be created again, not declared");
assert_eq!(
err.status,
Some(axum::http::StatusCode::NOT_FOUND),
"{err:?}"
);
}
/// Presence control for the two refusals above: a creation accepted but
/// not yet written passes.
#[tokio::test]
async fn an_agent_queued_for_this_hive_is_accepted() {
let queued = vec![("pr1ma".to_owned(), "atlas".to_owned())];
super::refuse_placement(
"atlas",
"pr1ma",
None,
Some("admin"),
|| std::future::ready(Ok(false)),
queued,
|| std::future::ready(Ok(Vec::new())),
)
.await
.expect("a queued creation is declared");
}
/// A creation queued for another hive does not declare the agent here.
#[tokio::test]
async fn an_agent_queued_for_another_hive_is_refused() {
let queued = vec![("sec0nd".to_owned(), "atlas".to_owned())];
let err = super::refuse_placement(
"atlas",
"pr1ma",
None,
Some("admin"),
|| std::future::ready(Ok(false)),
queued,
|| std::future::ready(Ok(Vec::new())),
)
.await
.expect_err("queued elsewhere is placed elsewhere");
assert_eq!(
err.status,
Some(axum::http::StatusCode::CONFLICT),
"{err:?}"
);
}
#[tokio::test]