Watch
0
0
Fork
You've already forked hyperhive
0

agent permissions: a destroyed agent's key goes with it

Declaring an agent Destroyed now deletes its `agent-permissions` key,
next to the queue-credential revoke and with the same failure handling:
logged at error, the destroy goes ahead, and declaring it destroyed
again re-runs it. Agent names are reusable after a destroy, and a create
left on the default stores no key, so the old key handed a new agent of
the same name the destroyed agent's grant (argus on #5293).

Also: migrate.rs's create-time-setup note named ruth's tool-group seed
in `ensure_root_agent`, which this PR deletes; it now says she holds the
code-constant default when the swarm sets nothing.

Part of #5170.
This commit is contained in:
atlas 2026-10-15 10:27:35 +02:00 • committed by mara
commit 4f9e9bd680
3 changed files with 73 additions and 7 deletions

View file

@ -206,6 +206,35 @@ pub(crate) async fn delete_agent_permissions(
Ok(StatusCode::NO_CONTENT)
}
/// Delete a destroyed agent's key. A name is free again once its agent is
/// destroyed, and a create left on the default stores nothing, so a key left
/// here would hand the next agent of that name this one's grant.
///
/// Like the credential revoke it runs beside, a failure is logged and does
/// not stop the destroy; declaring the agent destroyed again re-runs it.
pub(crate) async fn clear_on_destroy(state: &AppState, hive: &str, agent: &str) {
let cleared = async {
let client = queue(state).map_err(|(_, why)| anyhow::anyhow!(why))?;
agent_permissions::open_or_create(&client)
.await?
.delete(agent)
.await?;
anyhow::Ok(())
}
.await;
match cleared {
Ok(()) => tracing::info!(hive, agent, "destroyed agent's permissions cleared"),
Err(e) => tracing::error!(
hive,
agent,
error = %format!("{e:#}"),
"clearing this agent's permissions FAILED; it was destroyed anyway, so a new \
agent of the same name created on the default gets its grant. Declaring the \
agent destroyed again re-runs this."
),
}
}
/// Every permission name, and the set an agent the swarm sets nothing for
/// holds — so a client lists them without a copy of its own.
#[derive(Debug, Serialize, ToSchema)]
@ -658,6 +687,41 @@ mod tests {
assert_eq!(published(&client, "damocles").await, None);
}
/// Destroying an agent deletes its key, so a new agent of the same name
/// created on the default gets the default, not the old grant.
#[tokio::test]
async fn a_recreated_agent_does_not_inherit_the_destroyed_ones_grant() {
let nats = TestNats::start();
let client = nats.client().await;
let state = crate::agent_limits::tests::declared(&client).await;
let _ = put(&state, "atlas", granted()).await.expect("set");
let _ = crate::set_agent_state(
State(state.clone()),
Path(("pr1ma".to_owned(), "atlas".to_owned())),
axum::Json(crate::SetAgentStateRequest {
state: swarm_queue_client::wanted::AgentState::Destroyed,
}),
)
.await
.expect("destroyed");
let _ = crate::create_agent(
State(state.clone()),
axum::Json(crate::CreateAgentRequest {
name: "atlas".to_owned(),
hive: "pr1ma".to_owned(),
limits: None,
inference: None,
permissions: None,
}),
)
.await
.expect("recreated");
assert_eq!(published(&client, "atlas").await, None);
assert_eq!(get(&state, "atlas").await, Permissions::default());
}
#[tokio::test]
async fn the_catalogue_lists_every_permission_and_the_default() {
let json = serde_json::to_value(super::get_permission_catalogue().await.0).unwrap();

View file

@ -1410,11 +1410,12 @@ async fn set_agent_state(
// After the declaration and never before it: the write above is the
// destroy order, and until it lands the agent is not being torn down, so
// its credential has to keep working. Sequenced rather than spawned so
// the revocation attempt is finished — and its outcome logged — by the
// time the operator's call returns.
// its credential and permissions have to keep working. Sequenced rather
// than spawned so both attempts are finished — and their outcomes
// logged — by the time the operator's call returns.
if revokes_queue_credential(req.state) {
revoke_or_complain(&hive, &agent).await;
agent_permissions::clear_on_destroy(&state, &hive, &agent).await;
}
Ok(Json(render(&declaration)))
}