two-step agent spawn: request_init_config + request_spawn
This commit is contained in:
parent
42437f9c6a
commit
80dd5bb69e
7 changed files with 165 additions and 14 deletions
|
|
@ -57,6 +57,18 @@ pub async fn approve(coord: Arc<Coordinator>, id: i64) -> Result<()> {
|
|||
}
|
||||
finish_approval(&coord, &approval, result, terminal_tag)
|
||||
}
|
||||
ApprovalKind::InitConfig => {
|
||||
// Seed the proposed config repo. Runs synchronously — it's just
|
||||
// a few git operations with no nixos-container involvement.
|
||||
let result: Result<()> = async {
|
||||
lifecycle::setup_proposed(&proposed_dir, &approval.agent).await?;
|
||||
lifecycle::ensure_claude_dir(&claude_dir)?;
|
||||
lifecycle::ensure_state_dir(¬es_dir)?;
|
||||
Ok(())
|
||||
}
|
||||
.await;
|
||||
finish_approval(&coord, &approval, result, None)
|
||||
}
|
||||
ApprovalKind::Spawn => {
|
||||
// Run the spawn in the background so the approve POST returns
|
||||
// immediately. The dashboard reads `transient` to render a spinner.
|
||||
|
|
@ -144,6 +156,7 @@ fn finish_approval(
|
|||
let approval_kind = match approval.kind {
|
||||
ApprovalKind::Spawn => "spawn",
|
||||
ApprovalKind::ApplyCommit => "apply_commit",
|
||||
ApprovalKind::InitConfig => "init_config",
|
||||
};
|
||||
let sha_short = approval
|
||||
.fetched_sha
|
||||
|
|
@ -159,12 +172,19 @@ fn finish_approval(
|
|||
note.clone(),
|
||||
approval.description.clone(),
|
||||
);
|
||||
// For spawn/rebuild approvals, also surface the underlying action so
|
||||
// the manager knows whether the container actually came up. The
|
||||
// ApprovalResolved event already carries the same `ok` signal but
|
||||
// For spawn/rebuild/init_config approvals, also surface the underlying
|
||||
// action so the manager knows whether the lifecycle step succeeded.
|
||||
// The ApprovalResolved event already carries the same `ok` signal but
|
||||
// separating it lets the manager react to the lifecycle change
|
||||
// without having to special-case approvals.
|
||||
match approval.kind {
|
||||
ApprovalKind::InitConfig => {
|
||||
if ok {
|
||||
coord.notify_manager(&HelperEvent::ConfigReady {
|
||||
agent: approval.agent.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
ApprovalKind::Spawn => coord.notify_manager(&HelperEvent::Spawned {
|
||||
agent: approval.agent.clone(),
|
||||
ok,
|
||||
|
|
@ -439,6 +459,7 @@ pub async fn deny(coord: &Coordinator, id: i64, note: Option<&str>) -> Result<()
|
|||
let approval_kind = match a.kind {
|
||||
ApprovalKind::Spawn => "spawn",
|
||||
ApprovalKind::ApplyCommit => "apply_commit",
|
||||
ApprovalKind::InitConfig => "init_config",
|
||||
};
|
||||
let sha_short = sha.as_deref().map(|s| s[..s.len().min(12)].to_owned());
|
||||
let description = a.description.clone();
|
||||
|
|
|
|||
|
|
@ -307,6 +307,7 @@ fn kind_to_str(kind: ApprovalKind) -> &'static str {
|
|||
match kind {
|
||||
ApprovalKind::ApplyCommit => "apply_commit",
|
||||
ApprovalKind::Spawn => "spawn",
|
||||
ApprovalKind::InitConfig => "init_config",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -314,6 +315,7 @@ fn kind_from_str(s: &str) -> Result<ApprovalKind> {
|
|||
Ok(match s {
|
||||
"apply_commit" => ApprovalKind::ApplyCommit,
|
||||
"spawn" => ApprovalKind::Spawn,
|
||||
"init_config" => ApprovalKind::InitConfig,
|
||||
other => bail!("unknown approval kind '{other}'"),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -563,6 +563,7 @@ fn history_view(a: Approval) -> ApprovalHistoryView {
|
|||
let kind = match a.kind {
|
||||
hive_sh4re::ApprovalKind::ApplyCommit => "apply_commit",
|
||||
hive_sh4re::ApprovalKind::Spawn => "spawn",
|
||||
hive_sh4re::ApprovalKind::InitConfig => "init_config",
|
||||
};
|
||||
ApprovalHistoryView {
|
||||
id: a.id,
|
||||
|
|
@ -603,6 +604,14 @@ async fn build_approval_views(approvals: Vec<Approval>) -> Vec<ApprovalView> {
|
|||
diff: None,
|
||||
description: a.description,
|
||||
},
|
||||
hive_sh4re::ApprovalKind::InitConfig => ApprovalView {
|
||||
id: a.id,
|
||||
agent: a.agent,
|
||||
kind: "init_config",
|
||||
sha_short: None,
|
||||
diff: None,
|
||||
description: a.description,
|
||||
},
|
||||
});
|
||||
}
|
||||
out
|
||||
|
|
@ -1736,9 +1745,12 @@ fn gc_orphans(coord: &Coordinator, approvals: Vec<Approval>) -> Vec<Approval> {
|
|||
approvals
|
||||
.into_iter()
|
||||
.filter(|a| {
|
||||
// Spawn approvals are for not-yet-existent agents; the proposed
|
||||
// dir is supposed to be missing.
|
||||
if matches!(a.kind, hive_sh4re::ApprovalKind::Spawn) {
|
||||
// Spawn and InitConfig approvals are for not-yet-existent agents;
|
||||
// the proposed dir is supposed to be missing.
|
||||
if matches!(
|
||||
a.kind,
|
||||
hive_sh4re::ApprovalKind::Spawn | hive_sh4re::ApprovalKind::InitConfig
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if Coordinator::agent_proposed_dir(&a.agent).exists() {
|
||||
|
|
|
|||
|
|
@ -471,7 +471,9 @@ pub async fn setup_applied(
|
|||
/// Create the per-agent Claude credentials dir if missing. Mode 0700 — only
|
||||
/// root inside the container reads/writes it. Idempotent: existing dirs are
|
||||
/// left untouched (an agent's OAuth tokens survive `destroy`/recreate).
|
||||
fn ensure_claude_dir(claude_dir: &Path) -> Result<()> {
|
||||
/// Public for the `InitConfig` approval path in `actions.rs` which seeds
|
||||
/// dirs without calling the full `spawn`.
|
||||
pub fn ensure_claude_dir(claude_dir: &Path) -> Result<()> {
|
||||
if !claude_dir.exists() {
|
||||
std::fs::create_dir_all(claude_dir)
|
||||
.with_context(|| format!("create {}", claude_dir.display()))?;
|
||||
|
|
@ -485,7 +487,9 @@ fn ensure_claude_dir(claude_dir: &Path) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_state_dir(notes_dir: &Path) -> Result<()> {
|
||||
/// Public for the `InitConfig` approval path in `actions.rs` which seeds
|
||||
/// dirs without calling the full `spawn`.
|
||||
pub fn ensure_state_dir(notes_dir: &Path) -> Result<()> {
|
||||
if !notes_dir.exists() {
|
||||
std::fs::create_dir_all(notes_dir)
|
||||
.with_context(|| format!("create {}", notes_dir.display()))?;
|
||||
|
|
|
|||
|
|
@ -164,8 +164,46 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc<Coordinator>) -> ManagerResp
|
|||
},
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
Err(e) => ManagerResponse::Err {
|
||||
message: format!("{e:#}"),
|
||||
},
|
||||
}
|
||||
}
|
||||
ManagerRequest::RequestSpawn { name, description } => {
|
||||
tracing::info!(%name, "manager: request_spawn");
|
||||
let proposed_dir = crate::coordinator::Coordinator::agent_proposed_dir(name);
|
||||
if !proposed_dir.join(".git").exists() {
|
||||
return ManagerResponse::Err {
|
||||
message: format!(
|
||||
"no proposed config repo found for '{name}' - \
|
||||
call request_init_config first to initialise and customise \
|
||||
the config before spawning"
|
||||
),
|
||||
};
|
||||
}
|
||||
match coord.approvals.submit_kind(
|
||||
name,
|
||||
hive_sh4re::ApprovalKind::Spawn,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue