jobs are now DAGs of primitive nodes (prebuild, stop-for-update, swap, reconcile, signal, drain, ...) driven by one scheduler with N build slots + per-agent lifecycle leases. per-agent power intent (wanted up/offline) is durable in agent_power.sqlite; Reconcile nodes converge observed state to it. kills the graceful-stop watcher thread, the deferred-start follow-up, and the cascade pre-enqueue (fan-out on MetaLock completion instead). tracker: #2166
166 lines
5.6 KiB
Rust
166 lines
5.6 KiB
Rust
//! Container lifecycle endpoints for the dashboard.
|
|
//!
|
|
//! Rebuild / restart / start / stop (hard + graceful) / update-all all
|
|
//! submit DAGs to the job queue (`job_queue::submit`), so each shows a
|
|
//! visible queued→running transient on the dashboard — a direct
|
|
//! sub-second start/stop only flashed the badge. Start/stop also
|
|
//! persist the agent's `wanted` power intent before submitting; the
|
|
//! DAG's `Reconcile` converges to it. Destroy delegates to
|
|
//! `actions::destroy` (optionally purging).
|
|
|
|
use axum::{
|
|
extract::{Form, Path as AxumPath, Query, State},
|
|
http::StatusCode,
|
|
response::{IntoResponse, Response},
|
|
};
|
|
use serde::Deserialize;
|
|
|
|
/// Query params for `post_kill`. `?graceful=1` routes to the graceful-stop
|
|
/// orchestration (quiesce the harness, flush `/state`, then container stop)
|
|
/// instead of an immediate hard stop. Defaults false → today's hard kill.
|
|
#[derive(Deserialize)]
|
|
pub(super) struct KillParams {
|
|
#[serde(default)]
|
|
graceful: bool,
|
|
}
|
|
|
|
use super::{AppState, error_response, guard_agent_name, strip_container_prefix};
|
|
use crate::job_queue::{Source, submit};
|
|
use crate::{actions, lifecycle};
|
|
|
|
pub(super) async fn post_rebuild(
|
|
State(state): State<AppState>,
|
|
AxumPath(name): AxumPath<String>,
|
|
) -> Response {
|
|
let logical = strip_container_prefix(&name);
|
|
if let Some(reject) = guard_agent_name(&state, &logical).await {
|
|
return reject;
|
|
}
|
|
submit::rebuild(
|
|
&state.coord,
|
|
&logical,
|
|
Source::Manual,
|
|
"manual via dashboard ↻ R3BU1LD button".to_owned(),
|
|
);
|
|
(StatusCode::OK, "ok").into_response()
|
|
}
|
|
|
|
pub(super) async fn post_kill(
|
|
State(state): State<AppState>,
|
|
AxumPath(name): AxumPath<String>,
|
|
Query(params): Query<KillParams>,
|
|
) -> Response {
|
|
let logical = strip_container_prefix(&name);
|
|
if let Some(reject) = guard_agent_name(&state, &logical).await {
|
|
return reject;
|
|
}
|
|
if params.graceful {
|
|
// Graceful stop: submit the quiesce DAG (signal the harness →
|
|
// one stop-checkpoint turn → drain → container stop, with a
|
|
// timeout fallback to a hard stop). The agent's lifecycle
|
|
// lease keeps it from racing an in-flight rebuild for the same
|
|
// agent, and per-node progress surfaces on the queue snapshot.
|
|
submit::graceful_stop(
|
|
&state.coord,
|
|
&logical,
|
|
Source::Manual,
|
|
"manual via dashboard graceful stop".to_owned(),
|
|
);
|
|
return (StatusCode::OK, "ok").into_response();
|
|
}
|
|
// Manager is stoppable from the dashboard like any other
|
|
// agent. The host's dashboard server keeps running (it's
|
|
// hive-c0re, not the manager container), per-agent approvals
|
|
// submitted by other sub-agents still process through the
|
|
// host-side approval queue without the manager up, and
|
|
// operator-driven meta-input updates work from the dashboard
|
|
// either way. The MCP-surface self-kill guard in
|
|
// `socket_server.rs::ManagerRequest::Kill` stays in place: a
|
|
// manager calling Kill on its own container is self-suicide
|
|
// mid-call, not a legitimate operator action.
|
|
submit::stop(
|
|
&state.coord,
|
|
&logical,
|
|
Source::Manual,
|
|
"manual via dashboard stop".to_owned(),
|
|
);
|
|
(StatusCode::OK, "ok").into_response()
|
|
}
|
|
|
|
pub(super) async fn post_restart(
|
|
State(state): State<AppState>,
|
|
AxumPath(name): AxumPath<String>,
|
|
) -> Response {
|
|
let logical = strip_container_prefix(&name);
|
|
if let Some(reject) = guard_agent_name(&state, &logical).await {
|
|
return reject;
|
|
}
|
|
submit::restart(
|
|
&state.coord,
|
|
&logical,
|
|
Source::Manual,
|
|
"manual via dashboard ↺ R3START button".to_owned(),
|
|
);
|
|
(StatusCode::OK, "ok").into_response()
|
|
}
|
|
|
|
pub(super) async fn post_start(
|
|
State(state): State<AppState>,
|
|
AxumPath(name): AxumPath<String>,
|
|
) -> Response {
|
|
let logical = strip_container_prefix(&name);
|
|
if let Some(reject) = guard_agent_name(&state, &logical).await {
|
|
return reject;
|
|
}
|
|
submit::start(
|
|
&state.coord,
|
|
&logical,
|
|
Source::Manual,
|
|
"manual via dashboard start".to_owned(),
|
|
);
|
|
(StatusCode::OK, "ok").into_response()
|
|
}
|
|
|
|
pub(super) async fn post_update_all(State(state): State<AppState>) -> Response {
|
|
let containers = lifecycle::list().await.unwrap_or_default();
|
|
for container in containers {
|
|
let Some(logical) = container
|
|
.strip_prefix(lifecycle::AGENT_PREFIX)
|
|
.map(str::to_owned)
|
|
else {
|
|
continue;
|
|
};
|
|
submit::rebuild(
|
|
&state.coord,
|
|
&logical,
|
|
Source::Manual,
|
|
"manual via dashboard 🌀 UPDATE ALL".to_owned(),
|
|
);
|
|
}
|
|
(StatusCode::OK, "ok").into_response()
|
|
}
|
|
|
|
#[derive(Deserialize, Default)]
|
|
pub(super) struct DestroyForm {
|
|
#[serde(default)]
|
|
purge: Option<String>,
|
|
}
|
|
|
|
pub(super) async fn post_destroy(
|
|
State(state): State<AppState>,
|
|
AxumPath(name): AxumPath<String>,
|
|
Form(form): Form<DestroyForm>,
|
|
) -> Response {
|
|
if let Some(reject) = guard_agent_name(&state, &name).await {
|
|
return reject;
|
|
}
|
|
// Checkbox semantics: any non-empty value (axum sends "on") = purge.
|
|
let purge = form.purge.as_deref().is_some_and(|v| !v.is_empty());
|
|
// `actions::destroy` rescans the container list on success, so the
|
|
// `ContainerRemoved` event lands before we return 200. The matching
|
|
// form carries `data-no-refresh`.
|
|
match actions::destroy(&state.coord, &name, purge).await {
|
|
Ok(()) => (StatusCode::OK, "ok").into_response(),
|
|
Err(e) => error_response(&format!("destroy {name} failed: {e:#}")),
|
|
}
|
|
}
|