feat(#791): add request_init_config + request_apply_commit to AgentServer (topology-scoped)

This commit is contained in:
damocles 2026-06-03 22:39:45 +02:00 committed by mara
commit 3fa31a414f
4 changed files with 161 additions and 30 deletions

View file

@ -425,6 +425,60 @@ async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc<Coordinator>) ->
coord.emit_rebuild_queue_snapshot();
AgentResponse::Ok
}
AgentRequest::RequestInitConfig { name, description } => {
if !crate::topology::children_of(agent)
.iter()
.any(|c| c == name)
{
return AgentResponse::Err {
message: format!(
"agent `{agent}` cannot request_init_config for `{name}`: \
not a direct child in the topology tree"
),
};
}
tracing::info!(%agent, %name, "agent: request_init_config for child");
match crate::manager_server::submit_init_config(coord, name, description.clone()).await {
Ok(_id) => AgentResponse::Ok,
Err(e) => AgentResponse::Err {
message: format!("{e:#}"),
},
}
}
AgentRequest::RequestApplyCommit {
agent: target_agent,
commit_ref,
description,
} => {
if !crate::topology::children_of(agent)
.iter()
.any(|c| c == target_agent)
{
return AgentResponse::Err {
message: format!(
"agent `{agent}` cannot request_apply_commit for `{target_agent}`: \
not a direct child in the topology tree"
),
};
}
tracing::info!(%agent, %target_agent, %commit_ref, "agent: request_apply_commit for child");
match crate::manager_server::submit_apply_commit(
coord,
target_agent,
commit_ref,
description.as_deref(),
)
.await
{
Ok((id, sha)) => {
tracing::info!(%id, %target_agent, %sha, "agent: apply_commit approval queued");
AgentResponse::Ok
}
Err(e) => AgentResponse::Err {
message: format!("{e:#}"),
},
}
}
// Manager-only variants are not valid on the agent socket.
_ => AgentResponse::Err {
message: "request not supported on agent socket".to_owned(),

View file

@ -83,34 +83,8 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc<Coordinator>) -> ManagerResp
match req {
ManagerRequest::RequestInitConfig { name, description } => {
tracing::info!(%name, "manager: request_init_config");
let proposed_dir = crate::coordinator::Coordinator::agent_proposed_dir(name);
if proposed_dir.join(".git").exists() {
return ManagerResponse::Err {
message: format!(
"proposed config repo for '{name}' already exists at {} - \
use request_apply_commit to update an existing agent's config",
proposed_dir.display()
),
};
}
match coord.approvals.submit_kind(
name,
hive_sh4re::ApprovalKind::InitConfig,
"",
description.as_deref(),
) {
Ok(id) => {
tracing::info!(%id, %name, "init_config approval queued");
coord.emit_approval_added(
id,
name,
"init_config",
None,
None,
description.clone(),
);
ManagerResponse::Ok
}
match submit_init_config(coord, name, description.clone()).await {
Ok(_id) => ManagerResponse::Ok,
Err(e) => ManagerResponse::Err {
message: format!("{e:#}"),
},
@ -352,7 +326,7 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc<Coordinator>) -> ManagerResp
/// Accepts a 7..=40 char hex string (short or full sha); the exact
/// commit is resolved + existence-checked against the proposed repo
/// later in `lifecycle::git_fetch_to_tag`.
fn validate_commit_ref(commit_ref: &str) -> Result<()> {
pub(crate) fn validate_commit_ref(commit_ref: &str) -> Result<()> {
let n = commit_ref.len();
let hex = commit_ref.chars().all(|c| c.is_ascii_hexdigit());
if !(7..=40).contains(&n) || !hex {
@ -364,6 +338,30 @@ fn validate_commit_ref(commit_ref: &str) -> Result<()> {
Ok(())
}
/// Queue an `InitConfig` approval for a brand-new agent whose config repo
/// does not yet exist. Shared between the manager and agent sockets.
pub(crate) async fn submit_init_config(
coord: &Arc<Coordinator>,
name: &str,
description: Option<String>,
) -> anyhow::Result<i64> {
let proposed_dir = crate::coordinator::Coordinator::agent_proposed_dir(name);
if proposed_dir.join(".git").exists() {
anyhow::bail!(
"proposed config repo for '{name}' already exists at {} - \
use request_apply_commit to update an existing agent's config",
proposed_dir.display()
);
}
let id = coord
.approvals
.submit_kind(name, hive_sh4re::ApprovalKind::InitConfig, "", description.as_deref())
.map_err(|e| anyhow::anyhow!("queue approval row: {e:#}"))?;
tracing::info!(%id, %name, "init_config approval queued");
coord.emit_approval_added(id, name, "init_config", None, None, description);
Ok(id)
}
/// Submit-time half of the apply flow: queue the approval row, then
/// fetch the manager's commit from the proposed repo into applied and
/// pin it as `refs/tags/proposal/<id>`. From this point on the manager
@ -375,7 +373,7 @@ fn validate_commit_ref(commit_ref: &str) -> Result<()> {
/// proposed, fs error, git plumbing crash) we mark the row failed and
/// surface the error to the manager. We don't try to roll the row
/// back — the failure is part of the audit trail.
async fn submit_apply_commit(
pub(crate) async fn submit_apply_commit(
coord: &Arc<Coordinator>,
agent: &str,
commit_ref: &str,