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:
parent
1d8ec00ddc
commit
5785c0024c
35 changed files with 376 additions and 434 deletions
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue