destroy verb: CLI + admin socket + dashboard button; purges state + approvals
This commit is contained in:
parent
c7b50aa5b7
commit
b711296460
8 changed files with 92 additions and 4 deletions
|
|
@ -3,11 +3,11 @@
|
||||||
//! `&Coordinator` and the request parameters; callers stitch the response
|
//! `&Coordinator` and the request parameters; callers stitch the response
|
||||||
//! shape they want (HTTP redirect vs JSON).
|
//! shape they want (HTTP redirect vs JSON).
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::{Result, bail};
|
||||||
use hive_sh4re::{ApprovalStatus, HelperEvent, MANAGER_AGENT, Message, SYSTEM_SENDER};
|
use hive_sh4re::{ApprovalStatus, HelperEvent, MANAGER_AGENT, Message, SYSTEM_SENDER};
|
||||||
|
|
||||||
use crate::coordinator::Coordinator;
|
use crate::coordinator::Coordinator;
|
||||||
use crate::lifecycle;
|
use crate::lifecycle::{self, MANAGER_NAME};
|
||||||
|
|
||||||
/// Approve a pending request: read the agent.nix at the approval's commit from
|
/// Approve a pending request: read the agent.nix at the approval's commit from
|
||||||
/// the proposed repo, copy into the applied repo, commit there, and rebuild
|
/// the proposed repo, copy into the applied repo, commit there, and rebuild
|
||||||
|
|
@ -64,6 +64,33 @@ pub async fn approve(coord: &Coordinator, id: i64) -> Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fully tear down a sub-agent. Refuses the manager (declarative; would fight
|
||||||
|
/// with the host's nixos config).
|
||||||
|
pub async fn destroy(coord: &Coordinator, name: &str) -> Result<()> {
|
||||||
|
if name == MANAGER_NAME || name == MANAGER_AGENT {
|
||||||
|
bail!("refusing to destroy the manager ({name})");
|
||||||
|
}
|
||||||
|
tracing::info!(%name, "destroy");
|
||||||
|
lifecycle::destroy(name).await?;
|
||||||
|
coord.unregister_agent(name);
|
||||||
|
let runtime = Coordinator::agent_dir(name);
|
||||||
|
if runtime.exists() {
|
||||||
|
let _ = std::fs::remove_dir_all(&runtime);
|
||||||
|
}
|
||||||
|
let state = Coordinator::agent_state_root(name);
|
||||||
|
if state.exists() {
|
||||||
|
let _ = std::fs::remove_dir_all(&state);
|
||||||
|
}
|
||||||
|
let applied = Coordinator::agent_applied_dir(name);
|
||||||
|
if applied.exists() {
|
||||||
|
let _ = std::fs::remove_dir_all(&applied);
|
||||||
|
}
|
||||||
|
let _ = coord
|
||||||
|
.approvals
|
||||||
|
.fail_pending_for_agent(name, "agent destroyed");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn deny(coord: &Coordinator, id: i64) -> Result<()> {
|
pub fn deny(coord: &Coordinator, id: i64) -> Result<()> {
|
||||||
let approval = coord.approvals.get(id)?;
|
let approval = coord.approvals.get(id)?;
|
||||||
coord.approvals.mark_denied(id)?;
|
coord.approvals.mark_denied(id)?;
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,18 @@ impl Approvals {
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Mark every pending approval for `agent` as failed (returns rows affected).
|
||||||
|
/// Used by `destroy` to clear the queue of an agent that no longer exists.
|
||||||
|
pub fn fail_pending_for_agent(&self, agent: &str, note: &str) -> Result<usize> {
|
||||||
|
let conn = self.conn.lock().unwrap();
|
||||||
|
let n = conn.execute(
|
||||||
|
"UPDATE approvals SET status = 'failed', resolved_at = ?1, note = ?2
|
||||||
|
WHERE agent = ?3 AND status = 'pending'",
|
||||||
|
params![now_unix(), note, agent],
|
||||||
|
)?;
|
||||||
|
Ok(n)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn row_to_approval(row: &rusqlite::Row<'_>) -> rusqlite::Result<Approval> {
|
fn row_to_approval(row: &rusqlite::Row<'_>) -> rusqlite::Result<Approval> {
|
||||||
|
|
|
||||||
|
|
@ -80,10 +80,15 @@ impl Coordinator {
|
||||||
Self::manager_dir().join("mcp.sock")
|
Self::manager_dir().join("mcp.sock")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Per-agent state root (parent of `config/`, future `prompts/`, etc.).
|
||||||
|
pub fn agent_state_root(name: &str) -> PathBuf {
|
||||||
|
PathBuf::from(format!("{AGENT_STATE_ROOT}/{name}"))
|
||||||
|
}
|
||||||
|
|
||||||
/// Manager-editable proposed config repo. Bind-mounted into the manager
|
/// Manager-editable proposed config repo. Bind-mounted into the manager
|
||||||
/// container as `/agents/<name>/config/`.
|
/// container as `/agents/<name>/config/`.
|
||||||
pub fn agent_proposed_dir(name: &str) -> PathBuf {
|
pub fn agent_proposed_dir(name: &str) -> PathBuf {
|
||||||
PathBuf::from(format!("{AGENT_STATE_ROOT}/{name}/config"))
|
Self::agent_state_root(name).join("config")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Authoritative applied config repo. Hive-c0re-only.
|
/// Authoritative applied config repo. Hive-c0re-only.
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ pub async fn serve(port: u16, coord: Arc<Coordinator>) -> Result<()> {
|
||||||
.route("/", get(index))
|
.route("/", get(index))
|
||||||
.route("/approve/{id}", post(post_approve))
|
.route("/approve/{id}", post(post_approve))
|
||||||
.route("/deny/{id}", post(post_deny))
|
.route("/deny/{id}", post(post_deny))
|
||||||
|
.route("/destroy/{name}", post(post_destroy))
|
||||||
.route("/send", post(post_send))
|
.route("/send", post(post_send))
|
||||||
.route("/messages/stream", get(messages_stream))
|
.route("/messages/stream", get(messages_stream))
|
||||||
.with_state(AppState { coord });
|
.with_state(AppState { coord });
|
||||||
|
|
@ -124,6 +125,16 @@ async fn post_deny(State(state): State<AppState>, AxumPath(id): AxumPath<i64>) -
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn post_destroy(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
AxumPath(name): AxumPath<String>,
|
||||||
|
) -> Response {
|
||||||
|
match actions::destroy(&state.coord, &name).await {
|
||||||
|
Ok(()) => Redirect::to("/").into_response(),
|
||||||
|
Err(e) => error_response(&format!("destroy {name} failed: {e:#}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn error_response(message: &str) -> Response {
|
fn error_response(message: &str) -> Response {
|
||||||
(
|
(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
|
@ -154,7 +165,7 @@ fn render_containers(containers: &[String], hostname: &str) -> String {
|
||||||
let port = lifecycle::agent_web_port(name);
|
let port = lifecycle::agent_web_port(name);
|
||||||
let _ = writeln!(
|
let _ = writeln!(
|
||||||
out,
|
out,
|
||||||
"<li><span class=\"glyph\">▒░▒░░</span> <a href=\"http://{hostname}:{port}/\">{name}</a> <span class=\"role role-ag3nt\">ag3nt</span> <span class=\"meta\">{container} :{port}</span></li>",
|
"<li><span class=\"glyph\">▒░▒░░</span> <a href=\"http://{hostname}:{port}/\">{name}</a> <span class=\"role role-ag3nt\">ag3nt</span> <span class=\"meta\">{container} :{port}</span>\n <form method=\"POST\" action=\"/destroy/{name}\" class=\"inline\" onsubmit=\"return confirm('destroy {name}? this wipes the agent\\'s state.');\"><button class=\"btn btn-destroy\" type=\"submit\">DESTR0Y</button></form>\n</li>",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -410,6 +421,7 @@ const STYLE: &str = r#"
|
||||||
}
|
}
|
||||||
.approvals .row { display: flex; align-items: center; flex-wrap: wrap; gap: 0.4em; }
|
.approvals .row { display: flex; align-items: center; flex-wrap: wrap; gap: 0.4em; }
|
||||||
.approvals form.inline { display: inline; margin-left: 0.4em; }
|
.approvals form.inline { display: inline; margin-left: 0.4em; }
|
||||||
|
ul form.inline { display: inline-block; }
|
||||||
.btn {
|
.btn {
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
@ -424,6 +436,7 @@ const STYLE: &str = r#"
|
||||||
.btn:hover { background: rgba(255,255,255,0.05); text-shadow: 0 0 12px currentColor; }
|
.btn:hover { background: rgba(255,255,255,0.05); text-shadow: 0 0 12px currentColor; }
|
||||||
.btn-approve { color: var(--green); border-color: var(--green); }
|
.btn-approve { color: var(--green); border-color: var(--green); }
|
||||||
.btn-deny { color: var(--red); border-color: var(--red); }
|
.btn-deny { color: var(--red); border-color: var(--red); }
|
||||||
|
.btn-destroy { color: var(--red); border-color: var(--red); font-size: 0.75em; padding: 0.15em 0.5em; margin-left: 0.6em; }
|
||||||
.btn-talk { color: var(--cyan); border-color: var(--cyan); }
|
.btn-talk { color: var(--cyan); border-color: var(--cyan); }
|
||||||
.talkform {
|
.talkform {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,25 @@ pub async fn kill(name: &str) -> Result<()> {
|
||||||
run(&["stop", &container]).await
|
run(&["stop", &container]).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fully tear down a sub-agent's container: stop + remove via `nixos-container
|
||||||
|
/// destroy`, then clean our own systemd drop-in. Leaves it to the caller to
|
||||||
|
/// wipe `/var/lib/hyperhive/...` state and the per-agent runtime dir.
|
||||||
|
pub async fn destroy(name: &str) -> Result<()> {
|
||||||
|
validate(name)?;
|
||||||
|
let container = container_name(name);
|
||||||
|
// nixos-container destroy handles stop + removal of /var/lib/nixos-containers/<C>
|
||||||
|
// and /etc/nixos-containers/<C>.conf. Tolerate "no such container".
|
||||||
|
if let Err(e) = run(&["destroy", &container]).await {
|
||||||
|
tracing::warn!(error = ?e, "nixos-container destroy returned an error; continuing cleanup");
|
||||||
|
}
|
||||||
|
let dropin_dir = format!("/run/systemd/system/container@{container}.service.d");
|
||||||
|
if std::path::Path::new(&dropin_dir).exists() {
|
||||||
|
std::fs::remove_dir_all(&dropin_dir)
|
||||||
|
.with_context(|| format!("remove {dropin_dir}"))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn rebuild(
|
pub async fn rebuild(
|
||||||
name: &str,
|
name: &str,
|
||||||
hyperhive_flake: &str,
|
hyperhive_flake: &str,
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,8 @@ enum Cmd {
|
||||||
Spawn { name: String },
|
Spawn { name: String },
|
||||||
/// Stop a managed container (graceful).
|
/// Stop a managed container (graceful).
|
||||||
Kill { name: String },
|
Kill { name: String },
|
||||||
|
/// Fully tear down a sub-agent (state + applied repo + drop-in wiped).
|
||||||
|
Destroy { name: String },
|
||||||
/// Apply pending config to a managed container.
|
/// Apply pending config to a managed container.
|
||||||
Rebuild { name: String },
|
Rebuild { name: String },
|
||||||
/// List managed containers.
|
/// List managed containers.
|
||||||
|
|
@ -92,6 +94,9 @@ async fn main() -> Result<()> {
|
||||||
Cmd::Kill { name } => {
|
Cmd::Kill { name } => {
|
||||||
render(client::request(&cli.socket, HostRequest::Kill { name }).await?)
|
render(client::request(&cli.socket, HostRequest::Kill { name }).await?)
|
||||||
}
|
}
|
||||||
|
Cmd::Destroy { name } => {
|
||||||
|
render(client::request(&cli.socket, HostRequest::Destroy { name }).await?)
|
||||||
|
}
|
||||||
Cmd::Rebuild { name } => {
|
Cmd::Rebuild { name } => {
|
||||||
render(client::request(&cli.socket, HostRequest::Rebuild { name }).await?)
|
render(client::request(&cli.socket, HostRequest::Rebuild { name }).await?)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,10 @@ async fn dispatch(req: &HostRequest, coord: &Coordinator) -> HostResponse {
|
||||||
coord.unregister_agent(name);
|
coord.unregister_agent(name);
|
||||||
HostResponse::success()
|
HostResponse::success()
|
||||||
}
|
}
|
||||||
|
HostRequest::Destroy { name } => {
|
||||||
|
actions::destroy(coord, name).await?;
|
||||||
|
HostResponse::success()
|
||||||
|
}
|
||||||
HostRequest::Rebuild { name } => {
|
HostRequest::Rebuild { name } => {
|
||||||
tracing::info!(%name, "rebuild");
|
tracing::info!(%name, "rebuild");
|
||||||
let agent_dir = coord.register_agent(name)?;
|
let agent_dir = coord.register_agent(name)?;
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,9 @@ pub enum HostRequest {
|
||||||
Spawn { name: String },
|
Spawn { name: String },
|
||||||
/// Stop a managed container (graceful).
|
/// Stop a managed container (graceful).
|
||||||
Kill { name: String },
|
Kill { name: String },
|
||||||
|
/// Fully tear down a sub-agent: stop, wipe state + applied repo, drop the
|
||||||
|
/// systemd drop-in, purge pending approvals. Manager not destroyable.
|
||||||
|
Destroy { name: String },
|
||||||
/// Apply pending config to a managed container.
|
/// Apply pending config to a managed container.
|
||||||
Rebuild { name: String },
|
Rebuild { name: String },
|
||||||
/// List managed containers.
|
/// List managed containers.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue