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

@ -114,7 +114,7 @@ export function operatorInboxAppendFromEvent(ev) {
onCountsChanged();
}
// ─── approvals — the operator config-change / spawn approval queue ────────
// ─── approvals — the operator config-change approval queue ────────
const APPROVAL_TAB_KEY = "hyperhive:approvals:tab";
// Derived approval state — cold-loaded from /api/state, then mutated
// live by `approval_added` / `approval_resolved` dashboard events.
@ -244,24 +244,14 @@ export function renderApprovals() {
el(
"span",
{ class: "glyph" },
isMergePr ? "⇒" : isUpdateMeta ? "↻" : isSchedule ? "⏱" : "⊕",
isUpdateMeta ? "↻" : isSchedule ? "⏱" : "⇒",
),
el("span", { class: "id" }, "#" + a.id),
el("span", { class: "agent" }, a.agent),
el(
"span",
{
class:
"kind" +
(isMergePr || isUpdateMeta || isSchedule ? "" : " kind-spawn"),
},
isMergePr
? "merge-pr"
: isUpdateMeta
? "meta-update"
: isSchedule
? "schedule"
: "spawn",
{ class: "kind" },
isUpdateMeta ? "meta-update" : isSchedule ? "schedule" : "merge-pr",
),
);
if (isMergePr && a.sha_short) head.append(el("code", {}, a.sha_short));
@ -431,13 +421,11 @@ function renderApprovalHistory(root, history) {
el(
"span",
{ class: "kind" },
a.kind === "merge_config_pr"
? "merge-pr"
: a.kind === "update_meta_inputs"
? "meta-update"
: a.kind === "schedule_prompt"
? "schedule"
: "spawn",
a.kind === "update_meta_inputs"
? "meta-update"
: a.kind === "schedule_prompt"
? "schedule"
: "merge-pr",
),
" ",
);

View file

@ -671,10 +671,6 @@ ul form.inline {
letter-spacing: 0.1em;
text-transform: uppercase;
}
.kind-spawn {
color: var(--amber);
border-color: var(--amber);
}
details {
margin-top: 0.5em;
}

View file

@ -81,10 +81,9 @@ window.marked = marked;
for (const a of approvals) {
if (seenApprovals.has(a.id)) continue;
seenApprovals.add(a.id);
const verb = a.kind === "spawn" ? "spawn approval" : "config commit";
NOTIF.show(
"◆ approval #" + a.id,
`${verb} for ${a.agent}`,
`config commit for ${a.agent}`,
"hyperhive:approval:" + a.id,
);
}