refactor(hive-c0re): drop the request_init_config tool and InitConfig approval
swarm-controller's `InitAgentConfigRepo` node already covers config-repo creation, so this deletes a duplicate rather than a capability; old `init_config` rows are skipped by `collect_lenient` with no migration, by operator decision. Refs #4398
This commit is contained in:
parent
3f086bc659
commit
a3b672d1d5
31 changed files with 134 additions and 601 deletions
|
|
@ -1,6 +1,5 @@
|
|||
//! Config-approval request handlers: `RequestInitConfig` /
|
||||
//! `RequestUpdateMetaInputs`, plus the shared submit helpers
|
||||
//! (`submit_init_config` / `submit_merge_config_pr`).
|
||||
//! Config-approval request handlers: `RequestUpdateMetaInputs`, plus the
|
||||
//! shared submit helper `submit_merge_config_pr`.
|
||||
//!
|
||||
//! `submit_merge_config_pr` is called from the dashboard webhook handler
|
||||
//! (`dashboard::webhook`) — agents no longer need an MCP tool for config
|
||||
|
|
@ -11,70 +10,8 @@ use std::sync::Arc;
|
|||
|
||||
use hive_core_agent_sock::Response;
|
||||
|
||||
use super::require_new_child;
|
||||
use crate::coordinator::Coordinator;
|
||||
|
||||
/// `RequestInitConfig` — queue an `InitConfig` approval for an agent. The
|
||||
/// `name` must be brand-new (absent from the topology) or already in the
|
||||
/// caller's subtree; the requester is recorded as the new agent's parent (the
|
||||
/// root requesting a new agent → a top-level agent, matching reconcile's
|
||||
/// default).
|
||||
pub(super) fn handle_request_init_config(
|
||||
coord: &Arc<Coordinator>,
|
||||
agent: &str,
|
||||
name: &str,
|
||||
description: Option<String>,
|
||||
) -> Response {
|
||||
if let Some(err) = require_new_child(agent, name, "request_init_config for") {
|
||||
return err;
|
||||
}
|
||||
tracing::info!(%agent, %name, "request_init_config");
|
||||
// Warn, do not refuse: an agent already created under a colliding
|
||||
// name must stay re-initialisable, so the refusal comes later, once
|
||||
// the warning has had time to be seen.
|
||||
//
|
||||
// Checked HERE and not only in `swarm-controller::create_agent`:
|
||||
// that daemon is opt-in and off on most hives, while this is the
|
||||
// path the `request_init_config` tool takes on every hive. Guarding
|
||||
// only the rarer one would have left the common flow exactly as
|
||||
// unguarded as before.
|
||||
//
|
||||
// The blacklist itself comes from nix via `HIVE_RESERVED_NAMES`, so it
|
||||
// stays a config change rather than a rebuild. An UNSET variable means
|
||||
// this daemon was never told — which is not the same as "no name is
|
||||
// reserved", and saying nothing there would be a check that reports
|
||||
// clean because it could not run.
|
||||
let raw = hive_types::reserved_names_raw();
|
||||
let warnings = match raw.as_deref().map(hive_types::parse_reserved_names) {
|
||||
None => {
|
||||
tracing::error!(
|
||||
var = hive_types::RESERVED_NAMES_ENV,
|
||||
"request_init_config: reserved-name check could not run — variable not set"
|
||||
);
|
||||
vec![format!(
|
||||
"the reserved-name check did not run: {} is unset, so {name:?} was accepted \
|
||||
without being checked against the protocol literals",
|
||||
hive_types::RESERVED_NAMES_ENV
|
||||
)]
|
||||
}
|
||||
Some(reserved) if hive_types::is_reserved_name(name, &reserved) => {
|
||||
tracing::warn!(%agent, %name, "request_init_config: reserved name");
|
||||
vec![format!(
|
||||
"agent name {name:?} is a reserved protocol name — messages from this agent will \
|
||||
be indistinguishable from hyperhive's own; this will become an error"
|
||||
)]
|
||||
}
|
||||
Some(_) => Vec::new(),
|
||||
};
|
||||
match submit_init_config(coord, name, Some(agent), description) {
|
||||
Ok(_id) if warnings.is_empty() => Response::Ok,
|
||||
Ok(_id) => Response::OkWarn { warnings },
|
||||
Err(e) => Response::Err {
|
||||
message: format!("{e:#}"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// `RequestUpdateMetaInputs` — queue an `UpdateMetaInputs` approval
|
||||
/// carrying the JSON-encoded input list in `commit_ref` (no git commit
|
||||
/// is involved; the field is the payload the approval handler decodes).
|
||||
|
|
@ -214,58 +151,3 @@ pub(crate) async fn submit_merge_config_pr(
|
|||
});
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// Queue an `InitConfig` approval for a brand-new agent whose config repo
|
||||
/// does not yet exist. Shared between the manager and agent sockets.
|
||||
///
|
||||
/// `parent`, when `Some`, is the agent that will own the new child once
|
||||
/// the operator approves: it is stashed in the approval's `commit_ref`
|
||||
/// field (unused for `InitConfig` otherwise — same pattern
|
||||
/// `UpdateMetaInputs` uses to carry its inputs JSON) and consumed in
|
||||
/// `run_approval_init_config` to write the `child -> parent` topology
|
||||
/// edge. Callers pass the requesting agent, so the requester becomes the
|
||||
/// new agent's parent (the root requesting a new agent → a top-level agent,
|
||||
/// matching `topology::reconcile`'s default). `None` writes no explicit
|
||||
/// edge (reconcile-default placement) — retained for that fallback.
|
||||
pub(crate) fn submit_init_config(
|
||||
coord: &Arc<Coordinator>,
|
||||
name: &str,
|
||||
parent: Option<&str>,
|
||||
description: Option<String>,
|
||||
) -> anyhow::Result<i64> {
|
||||
let agent = hive_types::Ident::parse(name)
|
||||
.map_err(|e| anyhow::anyhow!("invalid agent name {name:?}: {e}"))?;
|
||||
let proposed_dir = crate::coordinator::Coordinator::agent_proposed_dir(&agent);
|
||||
if proposed_dir.join(".git").exists() {
|
||||
anyhow::bail!(
|
||||
"proposed config repo for '{name}' already exists at {} - \
|
||||
nothing to init; config changes go through a forge PR on \
|
||||
agent-configs/{name}",
|
||||
proposed_dir.display()
|
||||
);
|
||||
}
|
||||
let id = coord
|
||||
.approvals
|
||||
.submit_kind(
|
||||
name,
|
||||
hive_sh4re::approvals::ApprovalKind::InitConfig,
|
||||
parent.unwrap_or(""),
|
||||
description.as_deref(),
|
||||
// `parent` is the requesting agent (becomes the new child's
|
||||
// parent); it's also the submitter the approval events route
|
||||
// back to. No declared parent = operator-initiated path.
|
||||
parent.unwrap_or("operator"),
|
||||
None, // no sha for InitConfig
|
||||
)
|
||||
.map_err(|e| anyhow::anyhow!("queue approval row: {e:#}"))?;
|
||||
tracing::info!(%id, %name, "init_config approval queued");
|
||||
coord.emit_approval_added(crate::coordinator::ApprovalAdded {
|
||||
id,
|
||||
agent: name,
|
||||
approval_kind: "init_config",
|
||||
sha_short: None,
|
||||
description,
|
||||
pr_number: None,
|
||||
});
|
||||
Ok(id)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ pub(crate) use config_approvals::submit_merge_config_pr;
|
|||
pub(crate) use schedules::filter_ghost_schedule_targets;
|
||||
pub use schedules::schedule_to_wire_public;
|
||||
|
||||
use config_approvals::{handle_request_init_config, handle_request_update_meta_inputs};
|
||||
use config_approvals::handle_request_update_meta_inputs;
|
||||
use lifecycle_handlers::{
|
||||
handle_kill, handle_list_descendants, handle_restart, handle_start, handle_update,
|
||||
};
|
||||
|
|
@ -560,9 +560,6 @@ async fn dispatch(req: &Request, agent: &str, coord: &Arc<Coordinator>) -> Respo
|
|||
Request::Kill { name } => handle_kill(coord, agent, name).await,
|
||||
Request::Update { name } => handle_update(coord, agent, name),
|
||||
Request::ListDescendants => handle_list_descendants(coord, agent).await,
|
||||
Request::RequestInitConfig { name, description } => {
|
||||
handle_request_init_config(coord, agent, name, description.clone())
|
||||
}
|
||||
// Agent-state queries: own subtree is free; other agents + the
|
||||
// hive-wide `"*"` sweep require `QueryAgentState`.
|
||||
Request::GetLooseEnds { agent: target } => {
|
||||
|
|
@ -695,46 +692,6 @@ fn require_group(agent: &str, group: &str, action: &str) -> Option<Response> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Topology guard for `request_init_config`, which may legitimately target a
|
||||
/// child that does not exist *yet* (seeding a brand-new sub-agent's config
|
||||
/// repo). The caller may act on a
|
||||
/// `target` that is EITHER already in its subtree (re-init / config
|
||||
/// update of an agent it owns) OR brand-new (absent from the topology
|
||||
/// tree — the requester becomes its parent). A name that already
|
||||
/// exists outside the caller's subtree is refused so
|
||||
/// one agent can't hijack another's sub-tree.
|
||||
///
|
||||
/// Also re-runs the agent-name format check (a traversal / malformed name
|
||||
/// could never be a descendant): a brand-new name now flows straight to
|
||||
/// `submit_init_config`, which builds filesystem paths from it, so validate
|
||||
/// before that.
|
||||
fn require_new_child(agent: &str, target: &str, action: &str) -> Option<Response> {
|
||||
if let Err(reason) = hive_types::Ident::parse(target) {
|
||||
return Some(Response::Err {
|
||||
message: format!("agent `{agent}` cannot {action} `{target}`: {reason}"),
|
||||
});
|
||||
}
|
||||
// brand-new name (absent from topology) — requester becomes the parent on
|
||||
// approval; allowed for any caller.
|
||||
if !crate::topology::read().contains_key(target) {
|
||||
return None;
|
||||
}
|
||||
// existing agent — allowed only if it's in the caller's subtree
|
||||
// (re-init / config update of an agent the caller owns; the root owns
|
||||
// every existing agent). Refuses an agent outside the caller's subtree
|
||||
// so one agent can't hijack another's config.
|
||||
if crate::topology::is_descendant_of(target, agent) {
|
||||
None
|
||||
} else {
|
||||
Some(Response::Err {
|
||||
message: format!(
|
||||
"agent `{agent}` cannot {action} `{target}`: it already exists \
|
||||
outside its subtree in the topology tree"
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// `GetLooseEnds` — read the target's loose ends. `None` / own / a subtree
|
||||
/// descendant resolve freely (a parent sees its subtree, the root sees all);
|
||||
/// any other named agent needs `QueryAgentState`; `"*"` is a hive-wide sweep
|
||||
|
|
|
|||
Loading…
Reference in a new issue