feat(#1086): serialize perm changes through rebuild queue

add QueueKind::PermChange — dashboard tool-group and capability
handlers no longer write the shared JSON files inline. instead they
enqueue a PermChange entry; the FIFO worker applies the file write
then calls rebuild_agent so the updated env var takes effect.

concurrent batch-apply actions for different agents previously raced
on tool-groups.json / capabilities.json (last write wins, earlier
change silently dropped). serialising through the queue prevents this.

dedup check extended with perm-type discriminant so tool-groups and
capabilities changes for the same agent are kept as distinct entries
and never collapse into one slot.
This commit is contained in:
damocles 2026-06-02 16:01:43 +02:00
commit eae0e875cf
5 changed files with 127 additions and 27 deletions

View file

@ -2550,16 +2550,21 @@ async fn post_tool_groups(
if let Some(reject) = guard_agent_name(&state, &logical).await {
return reject;
}
if let Err(e) = crate::tool_groups::set_groups(&logical, &body.groups) {
return error_response(&format!("set tool-groups for {logical}: {e}"));
// Validate group names before queuing — fail fast so the operator
// sees the error immediately rather than waiting for the worker.
if let Err(e) = crate::tool_groups::validate_groups(&body.groups) {
return error_response(&format!("invalid tool-groups for {logical}: {e}"));
}
// Trigger a rebuild so the new HIVE_TOOL_GROUPS env var takes effect.
state.coord.rebuild_queue.enqueue(
crate::rebuild_queue::QueueKind::Rebuild,
// Enqueue a PermChange so the JSON file write is serialised through
// the FIFO worker. Prevents concurrent batch-apply actions for
// different agents from racing on the shared tool-groups.json.
state.coord.rebuild_queue.enqueue_with_perm(
logical.clone(),
crate::rebuild_queue::QueueSource::Manual,
"tool-group change via permissions UI".to_owned(),
None,
crate::rebuild_queue::PermPayload::ToolGroups {
groups: body.groups.clone(),
},
);
state.coord.emit_rebuild_queue_snapshot();
tracing::info!(agent = %logical, groups = ?body.groups, "operator: set tool-groups via dashboard");
@ -2614,16 +2619,16 @@ async fn post_capabilities(
return error_response(&format!("unknown capability: {cap}"));
}
}
if let Err(e) = crate::capabilities::set_caps(&logical, &body.caps) {
return error_response(&format!("set capabilities for {logical}: {e}"));
}
// Trigger a rebuild so the new HIVE_CAPABILITIES env var takes effect.
state.coord.rebuild_queue.enqueue(
crate::rebuild_queue::QueueKind::Rebuild,
// Enqueue a PermChange so the JSON file write is serialised through
// the FIFO worker. Prevents concurrent batch-apply actions for
// different agents from racing on the shared capabilities.json.
state.coord.rebuild_queue.enqueue_with_perm(
logical.clone(),
crate::rebuild_queue::QueueSource::Manual,
"capability change via dashboard".to_owned(),
None,
crate::rebuild_queue::PermPayload::Capabilities {
caps: body.caps.clone(),
},
);
state.coord.emit_rebuild_queue_snapshot();
tracing::info!(agent = %logical, caps = ?body.caps, "operator: set capabilities via dashboard");