diff --git a/TODO.md b/TODO.md index 28ff89ee..da96b5fd 100644 --- a/TODO.md +++ b/TODO.md @@ -44,6 +44,15 @@ Pick anything from here when relevant. Cross-cutting design notes live in ## UI / UX +- **Dashboard: show per-agent applied config.** Surface + `/var/lib/hyperhive/applied//agent.nix` (the file the + container actually builds from) as a collapsible `
` + block on each container row, alongside the journald viewer. + Backend: new `GET /api/agent-config/{name}` returns the file + contents (text/plain). Frontend: lazy-fetch on expand, render + inside a `
` with the same theming as the journal panel.
+  Useful for spot-checking what `request_apply_commit` produced
+  without ssh-ing in.
 - **xterm.js terminal** embedded per-agent, attached to a PTY exposed by
   the harness. Pairs well with the unprivileged-container work — would let
   the operator drop into the container without `nixos-container root-login`.
diff --git a/hive-c0re/assets/app.js b/hive-c0re/assets/app.js
index c367693c..0a7a52ba 100644
--- a/hive-c0re/assets/app.js
+++ b/hive-c0re/assets/app.js
@@ -164,21 +164,6 @@
     if (!(f instanceof HTMLFormElement) || !f.hasAttribute('data-async')) return;
     e.preventDefault();
     if (f.dataset.confirm && !confirm(f.dataset.confirm)) return;
-    if (f.dataset.prompt) {
-      const ans = prompt(f.dataset.prompt, '');
-      if (ans === null) return;  // operator hit Cancel
-      // Drop into a hidden input named after `data-prompt-field` (or
-      // 'note' by default) so the value rides along on the POST.
-      const field = f.dataset.promptField || 'note';
-      let input = f.querySelector(`input[name="${field}"]`);
-      if (!input) {
-        input = document.createElement('input');
-        input.type = 'hidden';
-        input.name = field;
-        f.append(input);
-      }
-      input.value = ans;
-    }
     const btn = f.querySelector('button[type="submit"], button:not([type]), .btn-inline');
     const original = btn ? btn.innerHTML : '';
     if (btn) { btn.disabled = true; btn.innerHTML = ''; }
@@ -305,9 +290,6 @@
       // narrows to the harness service (or empty = full machine).
       const journalUnit = c.is_manager ? 'hive-m1nd.service' : 'hive-ag3nt.service';
       li.append(buildJournalDetails(c.container, journalUnit));
-      // Per-container applied config viewer. Shows the agent.nix
-      // the container is actually built against.
-      li.append(buildConfigDetails(c.name));
 
       ul.append(li);
     }
@@ -366,48 +348,6 @@
     return details;
   }
 
-  // Per-container applied-config viewer. Lazy-fetches on expand;
-  // refresh button re-fetches. Read-only — the file is hive-c0re's
-  // applied repo, mutated only via the approval flow.
-  function buildConfigDetails(agentName) {
-    const details = el('details', {
-      class: 'journal',
-      'data-restore-key': 'agent-config:' + agentName,
-    });
-    const summary = el('summary', {}, '↳ agent.nix · ' + agentName);
-    const body = el('div', { class: 'journal-body' });
-    const controls = el('div', { class: 'journal-controls' });
-    const refresh = el('button', { type: 'button', class: 'btn btn-restart journal-refresh' },
-      '↻ refresh');
-    const pre = el('pre', { class: 'journal-output' }, 'fetching…');
-    let fetching = false;
-    async function fetchConfig() {
-      if (fetching) return;
-      fetching = true;
-      pre.textContent = 'fetching…';
-      try {
-        const resp = await fetch('/api/agent-config/' + agentName);
-        const text = await resp.text();
-        if (!resp.ok) {
-          pre.textContent = 'error: ' + resp.status + '\n' + text;
-        } else {
-          pre.textContent = text || '(empty)';
-          pre.scrollTop = 0;
-        }
-      } catch (err) {
-        pre.textContent = 'fetch failed: ' + err;
-      } finally {
-        fetching = false;
-      }
-    }
-    details.addEventListener('toggle', () => { if (details.open) fetchConfig(); });
-    refresh.addEventListener('click', (e) => { e.preventDefault(); fetchConfig(); });
-    controls.append(refresh);
-    body.append(controls, pre);
-    details.append(summary, body);
-    return details;
-  }
-
   function renderTombstones(s) {
     const root = $('tombstones-section');
     root.innerHTML = '';
@@ -631,21 +571,11 @@
             'new sub-agent — container will be created on approve'),
         );
       }
-      // Deny prompts the operator for an optional reason; the
-      // submit handler stashes it into a hidden `note` input that
-      // rides along on the POST and is surfaced to the manager via
-      // HelperEvent::ApprovalResolved { note }.
-      const denyForm = el('form', {
-        method: 'POST', action: '/deny/' + a.id,
-        class: 'inline', 'data-async': '',
-        'data-prompt': 'reason for denying (optional, sent to manager):',
-      });
-      denyForm.append(el('button', { type: 'submit', class: 'btn btn-deny' }, 'DENY'));
       row.append(
         ' ',
         form('/approve/' + a.id, 'btn-approve', '◆ APPR0VE'),
         ' ',
-        denyForm,
+        form('/deny/' + a.id, 'btn-deny', 'DENY'),
       );
       li.append(row);
       if (a.diff_html) {
diff --git a/hive-c0re/src/actions.rs b/hive-c0re/src/actions.rs
index 00318fd8..1b54efc0 100644
--- a/hive-c0re/src/actions.rs
+++ b/hive-c0re/src/actions.rs
@@ -178,17 +178,17 @@ pub async fn destroy(coord: &Coordinator, name: &str, purge: bool) -> Result<()>
     Ok(())
 }
 
-pub fn deny(coord: &Coordinator, id: i64, note: Option<&str>) -> Result<()> {
+pub fn deny(coord: &Coordinator, id: i64) -> Result<()> {
     let approval = coord.approvals.get(id)?;
-    coord.approvals.mark_denied(id, note)?;
-    tracing::info!(%id, note, "approval denied");
+    coord.approvals.mark_denied(id)?;
+    tracing::info!(%id, "approval denied");
     if let Some(a) = approval {
         coord.notify_manager(&HelperEvent::ApprovalResolved {
             id: a.id,
             agent: a.agent,
             commit_ref: a.commit_ref,
             status: ApprovalStatus::Denied,
-            note: note.map(String::from),
+            note: None,
         });
     }
     Ok(())
diff --git a/hive-c0re/src/approvals.rs b/hive-c0re/src/approvals.rs
index fbc06ab5..d3e659bb 100644
--- a/hive-c0re/src/approvals.rs
+++ b/hive-c0re/src/approvals.rs
@@ -142,12 +142,12 @@ impl Approvals {
         })
     }
 
-    pub fn mark_denied(&self, id: i64, note: Option<&str>) -> Result<()> {
+    pub fn mark_denied(&self, id: i64) -> Result<()> {
         let conn = self.conn.lock().unwrap();
         let affected = conn.execute(
-            "UPDATE approvals SET status = 'denied', resolved_at = ?1, note = ?2
-               WHERE id = ?3 AND status = 'pending'",
-            params![now_unix(), note, id],
+            "UPDATE approvals SET status = 'denied', resolved_at = ?1
+               WHERE id = ?2 AND status = 'pending'",
+            params![now_unix(), id],
         )?;
         if affected == 0 {
             bail!("approval {id} not pending");
diff --git a/hive-c0re/src/dashboard.rs b/hive-c0re/src/dashboard.rs
index ceaafa1d..ddf700d5 100644
--- a/hive-c0re/src/dashboard.rs
+++ b/hive-c0re/src/dashboard.rs
@@ -54,7 +54,6 @@ pub async fn serve(port: u16, coord: Arc) -> Result<()> {
         .route("/cancel-question/{id}", post(post_cancel_question))
         .route("/purge-tombstone/{name}", post(post_purge_tombstone))
         .route("/api/journal/{name}", get(get_journal))
-        .route("/api/agent-config/{name}", get(get_agent_config))
         .route("/request-spawn", post(post_request_spawn))
         .route("/messages/stream", get(messages_stream))
         .with_state(AppState { coord });
@@ -410,23 +409,8 @@ async fn post_approve(State(state): State, AxumPath(id): AxumPath
     }
 }
 
-#[derive(Deserialize, Default)]
-struct DenyForm {
-    #[serde(default)]
-    note: Option,
-}
-
-async fn post_deny(
-    State(state): State,
-    AxumPath(id): AxumPath,
-    Form(form): Form,
-) -> Response {
-    let note = form
-        .note
-        .as_deref()
-        .map(str::trim)
-        .filter(|s| !s.is_empty());
-    match actions::deny(&state.coord, id, note) {
+async fn post_deny(State(state): State, AxumPath(id): AxumPath) -> Response {
+    match actions::deny(&state.coord, id) {
         Ok(()) => Redirect::to("/").into_response(),
         Err(e) => error_response(&format!("deny {id} failed: {e:#}")),
     }
@@ -564,30 +548,6 @@ async fn get_journal(
     }
 }
 
-/// Show the current `agent.nix` from the applied repo — the file
-/// the container actually builds against. Read-only; the manager
-/// can't influence what this returns (that path goes through the
-/// approval queue).
-async fn get_agent_config(AxumPath(name): AxumPath) -> Response {
-    let logical = strip_container_prefix(&name);
-    // Constrain to managed containers — same shape as the journal
-    // endpoint, prevents arbitrary filesystem reads.
-    let live = lifecycle::list().await.unwrap_or_default();
-    let prefixed = if logical == lifecycle::MANAGER_NAME {
-        logical.clone()
-    } else {
-        format!("{}{logical}", lifecycle::AGENT_PREFIX)
-    };
-    if !live.iter().any(|c| c == &prefixed) {
-        return error_response(&format!("agent-config: no managed container {prefixed:?}"));
-    }
-    let path = Coordinator::agent_applied_dir(&logical).join("agent.nix");
-    match std::fs::read_to_string(&path) {
-        Ok(body) => ([("content-type", "text/plain; charset=utf-8")], body).into_response(),
-        Err(e) => error_response(&format!("read {}: {e}", path.display())),
-    }
-}
-
 async fn post_purge_tombstone(
     State(state): State,
     AxumPath(name): AxumPath,
diff --git a/hive-c0re/src/server.rs b/hive-c0re/src/server.rs
index 80f35511..9ce4df1e 100644
--- a/hive-c0re/src/server.rs
+++ b/hive-c0re/src/server.rs
@@ -144,7 +144,7 @@ async fn dispatch(req: &HostRequest, coord: Arc) -> HostResponse {
                 HostResponse::success()
             }
             HostRequest::Deny { id } => {
-                actions::deny(&coord, *id, None)?;
+                actions::deny(&coord, *id)?;
                 HostResponse::success()
             }
         })