Watch
0
0
Fork
You've already forked hyperhive
0

Make agent creation swarm-only and refuse a name placed on another hive

swarm-controller's POST /api/agents now refuses (409) a name the swarm
has already placed on a different hive: a non-Destroyed declaration in
that hive's wanted state, or a SetAgentWanted node still queued for it.
The same name on the same hive is that agent being re-created and goes
through. A wanted state that cannot be read refuses (503/500) instead of
reading as "placed nowhere". Creations are serialised from that read to
the graph insert so two concurrent creations of one name cannot both
pass.

Hive-level creation is removed: hivectl `agent create` / `request-create`,
HostRequest::Spawn / RequestSpawn, the dashboard POST /api/request-spawn
route, and ApprovalKind::Spawn with its approve/resolve arms and the
approval-carrying `templates::spawn`. The swarm path (deploy request or
wanted-state sweep -> queue_first_deploy -> templates::first_deploy) used
none of them. Old `spawn` approval rows are skipped by collect_lenient,
as `init_config` rows were in a3b672d1.

policy.rs's comment on agent_object_name stated swarm-wide name
uniqueness as a fact; it now says where it is enforced and what that
check cannot see.

Refs #4396
This commit is contained in:
atlas 2026-09-29 15:11:57 +02:00
commit 5785c0024c
35 changed files with 376 additions and 434 deletions

View file

@ -1,7 +1,7 @@
//! Approval queue. Requests are submitted by an agent
//! (`RequestSchedulePrompt`), the config-PR webhook (`MergeConfigPr`), or
//! the operator (`Spawn`); the user approves/denies via the host admin CLI;
//! on approval the host runs the corresponding action.
//! (`RequestSchedulePrompt`) or the config-PR webhook (`MergeConfigPr`); the
//! user approves/denies via the host admin CLI; on approval the host runs the
//! corresponding action.
//!
//! `UpdateMetaInputs` rows are legacy: the MCP tool that queued them was
//! removed and nothing produces the kind any more. The variant and
@ -78,7 +78,7 @@ impl Approvals {
/// Insert a new pending approval row. `fetched_sha` may be supplied
/// when the sha is already known at submission time (e.g. `MergeConfigPr`
/// fetches the PR head before inserting), making the insert + sha-set
/// atomic. Pass `None` when the kind carries no sha (e.g. `Spawn`).
/// atomic. Pass `None` when the kind carries no sha (e.g. `SchedulePrompt`).
pub fn submit_kind(
&self,
agent: &str,
@ -382,7 +382,6 @@ fn row_to_approval(row: &rusqlite::Row<'_>) -> rusqlite::Result<Approval> {
// Column order: id, agent, kind, commit_ref, requested_at, status, resolved_at, note, fetched_sha, description.
let kind: String = row.get(2)?;
let kind = match kind.as_str() {
"spawn" => ApprovalKind::Spawn,
"update_meta_inputs" => ApprovalKind::UpdateMetaInputs,
"schedule_prompt" => ApprovalKind::SchedulePrompt,
"merge_config_pr" => ApprovalKind::MergeConfigPr,
@ -435,7 +434,6 @@ fn row_to_approval(row: &rusqlite::Row<'_>) -> rusqlite::Result<Approval> {
fn kind_from_str(s: &str) -> Result<ApprovalKind> {
Ok(match s {
"spawn" => ApprovalKind::Spawn,
"update_meta_inputs" => ApprovalKind::UpdateMetaInputs,
"schedule_prompt" => ApprovalKind::SchedulePrompt,
"merge_config_pr" => ApprovalKind::MergeConfigPr,
@ -467,7 +465,7 @@ mod tests {
None,
)
.unwrap();
db.submit_kind("b", ApprovalKind::Spawn, "", None, "b", None)
db.submit_kind("b", ApprovalKind::SchedulePrompt, "", None, "b", None)
.unwrap();
db.submit_kind("c", ApprovalKind::UpdateMetaInputs, "[]", None, "c", None)
.unwrap();
@ -508,7 +506,14 @@ mod tests {
// final — re-cancelling errors instead of silently overwriting.
let (_dir, _path, db) = open_temp();
let id = db
.submit_kind("a", ApprovalKind::Spawn, "deadbeef", None, "a", None)
.submit_kind(
"a",
ApprovalKind::MergeConfigPr,
"deadbeef",
None,
"a",
None,
)
.unwrap();
db.mark_cancelled(id, "manager").expect("first cancel");
let err = db
@ -567,7 +572,7 @@ mod tests {
let raw = Connection::open(&path).unwrap();
raw.execute(
"INSERT INTO approvals (agent, kind, commit_ref, requested_at, status)
VALUES ('old', 'spawn', '', 0, 'pending')",
VALUES ('old', 'merge_config_pr', '', 0, 'pending')",
[],
)
.unwrap();