Compare commits

...
Author SHA1 Message Date
iris
ec30277a90 hive-c0re: drop redundant METHOD/path prefixes from OpenAPI summaries
Swagger UI's endpoint-list row already shows the HTTP method badge +
path for every row, so restating `METHOD /path` at the start of a
handler's own summary is pure duplication. Strips that self-referential
prefix from every summary that has it and re-capitalizes what follows
as a standalone sentence.

Left two false positives untouched: misc_api.rs's operator-inbox
summary cross-references a *different* sibling endpoint
(mark-all-read) for context, and topology.rs's SetParentForm struct
doc happens to mention its endpoint's path but isn't a handler summary
line. Both are legitimate, not redundant.
2026-08-02 21:35:17 +02:00
iris
071dbd774c hive-c0re: split OpenAPI summary/description, move param docs to params
utoipa splits a handler's doc comment on the first blank `///` line:
everything before it becomes the OpenAPI `summary` (shown in Swagger
UI's collapsed endpoint-list row), everything after becomes the
`description` (only shown once that row is expanded). With no blank
line, the whole doc comment becomes the summary and the description is
empty — which is what every handler in hive-c0re/src/dashboard/ was
doing, so the all-endpoints list showed full multi-sentence prose next
to every route instead of a short one-liner.

For every `#[utoipa::path(...)]`-annotated handler across the 19 files
in that module:

- Inserted a blank `///` line after the first short sentence/clause so
  utoipa's split produces a real summary + description, where the doc
  comment had more to say. Left already-short single-clause docs alone
  (nothing to split).
- Where a query struct derives `IntoParams`, moved param prose that
  duplicated a field's own doc comment out of the handler doc (the
  field already documents itself in the generated spec), or added a
  field doc where the handler explained a param that had none.

No behavior changes — doc comments and `params()` description text
only. Verified `cargo build -p hive-c0re` (clean) and `nix fmt` (zero
changes) after.

Closes #2969
2026-08-02 21:35:17 +02:00
18 changed files with 236 additions and 176 deletions

View file

@ -16,7 +16,7 @@ use super::{AppState, error_response};
use crate::actions; use crate::actions;
use crate::coordinator::Coordinator; use crate::coordinator::Coordinator;
/// `POST /api/approve/{id}` — approve a pending approval row. /// Approve a pending approval row.
#[utoipa::path( #[utoipa::path(
post, post,
path = "/api/approve/{id}", path = "/api/approve/{id}",
@ -47,7 +47,7 @@ pub(super) struct DenyForm {
note: Option<String>, note: Option<String>,
} }
/// `POST /api/deny/{id}` — deny a pending approval row, with an optional /// Deny a pending approval row, with an optional
/// note (form field `note`). /// note (form field `note`).
#[utoipa::path( #[utoipa::path(
post, post,

View file

@ -29,8 +29,10 @@ pub(super) struct BuildLogsAllQuery {
limit: Option<usize>, limit: Option<usize>,
} }
/// `GET /api/build-logs?limit=N` — most-recent build log headers across /// Most-recent build log headers across
/// all agents, newest first. Same JSON shape as the per-agent endpoint. /// all agents, newest first.
///
/// Same JSON shape as the per-agent endpoint.
#[utoipa::path( #[utoipa::path(
get, get,
path = "/api/build-logs", path = "/api/build-logs",
@ -60,11 +62,11 @@ pub(super) struct BuildLogsQuery {
limit: Option<usize>, limit: Option<usize>,
} }
/// `GET /api/build-logs/{agent}?limit=N` — most-recent build log /// Most-recent build log
/// headers for one agent, newest first. Returns /// headers for one agent, newest first.
/// `Vec<BuildLogHeader>` (JSON). Limit defaults to 10, server-side ///
/// cap at 50. Backs the per-agent log chip in the agent card and /// Returns `Vec<BuildLogHeader>` (JSON). Backs the per-agent log chip
/// the side-panel header list. /// in the agent card and the side-panel header list.
#[utoipa::path( #[utoipa::path(
get, get,
path = "/api/build-logs/{agent}", path = "/api/build-logs/{agent}",
@ -101,10 +103,12 @@ pub(super) async fn get_build_logs_agent(
} }
} }
/// `GET /api/build-logs/id/{id}` — full build log row (stdout + /// Full build log row (stdout +
/// stderr concatenated) by id. Returns `BuildLogFull` (JSON), or /// stderr concatenated) by id.
/// HTTP 404 when the id doesn't exist (vacuum-reaped, or the ///
/// operator passed a stale id from a refresh race). /// Returns `BuildLogFull` (JSON), or HTTP 404 when the id doesn't
/// exist (vacuum-reaped, or the operator passed a stale id from a
/// refresh race).
#[utoipa::path( #[utoipa::path(
get, get,
path = "/api/build-logs/id/{id}", path = "/api/build-logs/id/{id}",
@ -127,14 +131,16 @@ pub(super) async fn get_build_log_full(
} }
} }
/// `GET /api/build-log/{node_id}` — the build log for a **queue node**, /// The build log for a **queue node**,
/// resolved node id → log-row id → full log. Same `BuildLogFull` JSON /// resolved node id → log-row id → full log.
/// (`stdout` / `stderr` + header) as `get_build_log_full`; HTTP 404 when the ///
/// node has no linked log (the client gates the request on /// Same `BuildLogFull` JSON (`stdout` / `stderr` + header) as
/// `NodeView.build_log_id`, but a vacuum race can still 404). This is the /// `get_build_log_full`; HTTP 404 when the node has no linked log (the
/// on-demand live-log-panel fetch, distinct from the `build_log_id` on the /// client gates the request on `NodeView.build_log_id`, but a vacuum
/// wire — that id is for deep-linking to the BUILD L0GS tab's full history /// race can still 404). This is the on-demand live-log-panel fetch,
/// view, not for fetching the log content itself. /// distinct from the `build_log_id` on the wire — that id is for
/// deep-linking to the BUILD L0GS tab's full history view, not for
/// fetching the log content itself.
#[utoipa::path( #[utoipa::path(
get, get,
path = "/api/build-log/{node_id}", path = "/api/build-log/{node_id}",
@ -160,7 +166,7 @@ pub(super) async fn get_build_log_for_node(
} }
} }
/// `GET /api/build-log/{node_id}/raw` — the node's build log as `text/plain` /// The node's build log as `text/plain`
/// for download (delegates to `get_build_log_raw` after resolving the node id). /// for download (delegates to `get_build_log_raw` after resolving the node id).
#[utoipa::path( #[utoipa::path(
get, get,
@ -200,15 +206,16 @@ struct BuildLogFrame {
done: bool, done: bool,
} }
/// `GET /api/build-logs/id/{id}/stream` — SSE stream that delivers /// SSE stream that delivers
/// incremental stdout/stderr as a build runs. The client connects when /// incremental stdout/stderr as a build runs.
/// it opens a running-build panel; the stream closes automatically once
/// the build finishes (or the row disappears due to a vacuum).
/// ///
/// Each frame is a JSON-serialised `BuildLogFrame`. The first frame /// The client connects when it opens a running-build panel; the
/// always carries the full accumulated log so far (cursors start at 0); /// stream closes automatically once the build finishes (or the row
/// subsequent frames carry only new bytes. `done: true` on the final /// disappears due to a vacuum). Each frame is a JSON-serialised
/// frame signals the browser to close the `EventSource`. /// `BuildLogFrame`. The first frame always carries the full
/// accumulated log so far (cursors start at 0); subsequent frames
/// carry only new bytes. `done: true` on the final frame signals the
/// browser to close the `EventSource`.
#[utoipa::path( #[utoipa::path(
get, get,
path = "/api/build-logs/id/{id}/stream", path = "/api/build-logs/id/{id}/stream",
@ -307,8 +314,10 @@ pub(super) async fn get_build_log_stream(
Sse::new(ReceiverStream::new(rx)).keep_alive(KeepAlive::default()) Sse::new(ReceiverStream::new(rx)).keep_alive(KeepAlive::default())
} }
/// `GET /api/build-logs/id/{id}/raw` — full log as `text/plain` for /// Full log as `text/plain` for
/// download. Stdout and stderr are concatenated with a `--- stderr ---` /// download.
///
/// Stdout and stderr are concatenated with a `--- stderr ---`
/// separator (same layout the JS side-panel renders). The /// separator (same layout the JS side-panel renders). The
/// `Content-Disposition` header triggers a browser download with a /// `Content-Disposition` header triggers a browser download with a
/// descriptive filename so the operator can save and share the log. /// descriptive filename so the operator can save and share the log.

View file

@ -58,11 +58,13 @@ pub(super) struct ExtraForgesQuery {
agent: String, agent: String,
} }
/// `GET /api/extra-forges?agent=<name>` — list the external forge accounts /// List the external forge
/// currently provisioned for `agent`, derived from every `forge-<label>- /// accounts currently provisioned for `agent`.
/// token` file in its state dir (mirrors `matrix_accounts.rs`'s filename-scan ///
/// listing). `base_url` is backfilled from the matching `forge-<label>.json` /// Derived from every `forge-<label>-token` file in its state dir
/// sidecar when present. Never returns a token. /// (mirrors `matrix_accounts.rs`'s filename-scan listing). `base_url`
/// is backfilled from the matching `forge-<label>.json` sidecar when
/// present. Never returns a token.
#[utoipa::path( #[utoipa::path(
get, get,
path = "/api/extra-forges", path = "/api/extra-forges",
@ -133,10 +135,12 @@ struct ExtraForgeAccountResult {
ok: bool, ok: bool,
} }
/// `POST /api/extra-forge-account` — add persists the operator-pasted /// Add persists the operator-pasted
/// label/base-URL/token to the agent's state dir via hive-priv; remove /// label/base-URL/token to the agent's state dir via hive-priv; remove
/// deletes both files. Purely local — no remote account creation or /// deletes both files.
/// revocation, there is no admin access assumed on the external forge. ///
/// Purely local — no remote account creation or revocation, there is
/// no admin access assumed on the external forge.
/// Operator-authenticated (dashboard). Never echoes the token back. /// Operator-authenticated (dashboard). Never echoes the token back.
#[utoipa::path( #[utoipa::path(
post, post,

View file

@ -28,7 +28,7 @@ use utoipa::ToSchema;
use crate::host_stats::ServerWarning; use crate::host_stats::ServerWarning;
/// `GET /health/live` — liveness. Always `200`; no further checks. /// Liveness. Always `200`; no further checks.
#[utoipa::path( #[utoipa::path(
get, get,
path = "/health/live", path = "/health/live",
@ -49,12 +49,13 @@ struct ReadyBody {
warnings: Vec<ServerWarning>, warnings: Vec<ServerWarning>,
} }
/// `GET /health/ready` — readiness. `200` with `{"status":"ok", "warnings": /// Readiness.
/// [...]}` unless a `crit`-level warning is currently set in ///
/// [`crate::warnings::snapshot`], in which case `503` with /// `200` with `{"status":"ok", "warnings": [...]}` unless a `crit`-level
/// `{"status":"degraded", ...}`. `warnings` always carries the full /// warning is currently set in [`crate::warnings::snapshot`], in which
/// current list (including `warn`-level entries not affecting the /// case `503` with `{"status":"degraded", ...}`. `warnings` always
/// status) so a poller gets detail either way. /// carries the full current list (including `warn`-level entries not
/// affecting the status) so a poller gets detail either way.
#[utoipa::path( #[utoipa::path(
get, get,
path = "/health/ready", path = "/health/ready",

View file

@ -14,14 +14,15 @@ use hive_priv_sock::{InfraAction, InfraContainer};
use super::{AppState, error_response}; use super::{AppState, error_response};
/// `POST /api/infra-container/{name}/{action}` — start / stop / restart a /// Start / stop / restart a
/// hive infrastructure container from the dashboard. `name` parses into /// hive infrastructure container from the dashboard.
/// [`InfraContainer`] (the allowlist; unrecognised names 400), `action` ///
/// into `start` / `stop` / `restart`. Every attempt lands in the audit log /// `name` parses into [`InfraContainer`] (the allowlist; unrecognised
/// (actor `"operator"`, action `start_infra` / `stop_infra` / /// names 400), `action` into `start` / `stop` / `restart`. Every attempt
/// `restart_infra`) and streams as an `AuditEntryAdded` event, so /// lands in the audit log (actor `"operator"`, action `start_infra` /
/// operator-driven and agent-driven (`infra_admin`) infra actions show up /// `stop_infra` / `restart_infra`) and streams as an `AuditEntryAdded`
/// in the same AUDIT view. /// event, so operator-driven and agent-driven (`infra_admin`) infra
/// actions show up in the same AUDIT view.
#[utoipa::path( #[utoipa::path(
post, post,
path = "/api/infra-container/{name}/{action}", path = "/api/infra-container/{name}/{action}",

View file

@ -34,6 +34,7 @@ pub(super) struct JournalQuery {
} }
/// Read `journalctl -M <container> -b` and return its text output. /// Read `journalctl -M <container> -b` and return its text output.
///
/// Operator-only by virtue of the dashboard being host-bound. hive-c0re /// Operator-only by virtue of the dashboard being host-bound. hive-c0re
/// runs unprivileged (privsep), so the `-M` read — which enters the /// runs unprivileged (privsep), so the `-M` read — which enters the
/// container namespace and needs root — is delegated to hive-priv. /// container namespace and needs root — is delegated to hive-priv.
@ -159,10 +160,12 @@ pub(super) struct JournalHostQuery {
lines: Option<u32>, lines: Option<u32>,
} }
/// `GET /api/journal-host?unit=<unit>&lines=N` — host-side journald (no /// Host-side journald (no
/// `-M` container flag). Restricted to an allow-list of known host services /// `-M` container flag).
/// so arbitrary unit names can't be probed. Operator-only by virtue of the ///
/// dashboard binding to a host-only port. /// Restricted to an allow-list of known host services so arbitrary unit
/// names can't be probed. Operator-only by virtue of the dashboard binding
/// to a host-only port.
#[utoipa::path( #[utoipa::path(
get, get,
path = "/api/journal-host", path = "/api/journal-host",

View file

@ -30,7 +30,7 @@ use super::{AppState, Ident, error_response, guard_agent_name, strip_container_p
use crate::job_queue::{Source, submit}; use crate::job_queue::{Source, submit};
use crate::{actions, lifecycle}; use crate::{actions, lifecycle};
/// `POST /api/rebuild/{name}` — queue a rebuild DAG for `name`. /// Queue a rebuild DAG for `name`.
#[utoipa::path( #[utoipa::path(
post, post,
path = "/api/rebuild/{name}", path = "/api/rebuild/{name}",
@ -59,8 +59,10 @@ pub(super) async fn post_rebuild(
(StatusCode::OK, "ok").into_response() (StatusCode::OK, "ok").into_response()
} }
/// `POST /api/kill/{name}?graceful=1` — stop `name`, hard by default or /// Stop `name`, hard by default or
/// gracefully (quiesce → drain → stop) when `graceful=1`. /// gracefully when `graceful=1`.
///
/// Graceful mode: quiesce → drain → stop.
#[utoipa::path( #[utoipa::path(
post, post,
path = "/api/kill/{name}", path = "/api/kill/{name}",
@ -119,8 +121,10 @@ pub(super) async fn post_kill(
(StatusCode::OK, "ok").into_response() (StatusCode::OK, "ok").into_response()
} }
/// `POST /api/restart/{name}?graceful=1` — restart `name`, hard by default /// Restart `name`, hard by default
/// or gracefully (quiesce → drain → restart) when `graceful=1`. /// or gracefully when `graceful=1`.
///
/// Graceful mode: quiesce → drain → restart.
#[utoipa::path( #[utoipa::path(
post, post,
path = "/api/restart/{name}", path = "/api/restart/{name}",
@ -172,7 +176,7 @@ pub(super) struct StartParams {
paused: bool, paused: bool,
} }
/// `POST /api/start/{name}?paused=1` — start `name`, optionally paused. /// Start `name`, optionally paused.
/// ///
/// Plain `?paused=1` mirrors `hivectl agent <name> start --paused`: if /// Plain `?paused=1` mirrors `hivectl agent <name> start --paused`: if
/// `name` is already running, this just writes the pause marker in place /// `name` is already running, this just writes the pause marker in place
@ -232,7 +236,7 @@ pub(super) async fn post_start(
(StatusCode::OK, "ok").into_response() (StatusCode::OK, "ok").into_response()
} }
/// `POST /api/pause/{name}` — write the pause marker for `name`. /// Write the pause marker for `name`.
/// ///
/// Unlike the lifecycle ops above this is not a DAG: it writes a single /// Unlike the lifecycle ops above this is not a DAG: it writes a single
/// marker file, which the harness stats at the top of its serve loop. /// marker file, which the harness stats at the top of its serve loop.
@ -271,7 +275,7 @@ pub(super) async fn post_pause(
(StatusCode::OK, "ok").into_response() (StatusCode::OK, "ok").into_response()
} }
/// `POST /api/resume/{name}` — remove the pause marker for `name`. /// Remove the pause marker for `name`.
/// ///
/// The inverse of `post_pause`. Removing a non-existent marker is a no-op /// The inverse of `post_pause`. Removing a non-existent marker is a no-op
/// (idempotent). Triggers an immediate rescan so the paused badge clears. /// (idempotent). Triggers an immediate rescan so the paused badge clears.
@ -317,7 +321,7 @@ pub(super) struct ResourceLimitsForm {
memory_max: String, memory_max: String,
} }
/// `POST /api/resource-limits/{name}` — write per-agent CPU/memory limit /// Write per-agent CPU/memory limit
/// overrides for `name`. /// overrides for `name`.
/// ///
/// An empty `cpu_quota` or `memory_max` field clears that field's override, /// An empty `cpu_quota` or `memory_max` field clears that field's override,
@ -390,7 +394,7 @@ pub(super) async fn post_resource_limits(
(StatusCode::OK, "ok").into_response() (StatusCode::OK, "ok").into_response()
} }
/// `POST /api/update-all` — queue a rebuild DAG for every live agent /// Queue a rebuild DAG for every live agent
/// container. /// container.
#[utoipa::path( #[utoipa::path(
post, post,
@ -423,9 +427,10 @@ pub(super) struct DestroyForm {
purge: Option<String>, purge: Option<String>,
} }
/// `POST /api/destroy/{name}` — destroy `name`'s container. Form field /// Destroy `name`'s container.
/// `purge` (any non-empty value, e.g. `"on"`) also wipes the retained ///
/// state dir instead of leaving a tombstone. /// Form field `purge` (any non-empty value, e.g. `"on"`) also wipes the
/// retained state dir instead of leaving a tombstone.
#[utoipa::path( #[utoipa::path(
post, post,
path = "/api/destroy/{name}", path = "/api/destroy/{name}",

View file

@ -103,8 +103,10 @@ fn account_name_from_filename(fname: &str) -> Option<String> {
Some(suffix.to_owned()) Some(suffix.to_owned())
} }
/// `GET /api/matrix-accounts?agent=<name>` — matrix accounts provisioned /// Matrix accounts provisioned
/// for `agent`, backfilled with `homeserver`/`live`/`user_id` from the daemon's /// for `agent`.
///
/// Backfilled with `homeserver`/`live`/`user_id` from the daemon's
/// snapshot. /// snapshot.
#[utoipa::path( #[utoipa::path(
get, get,
@ -188,6 +190,7 @@ struct MatrixLoginResult {
} }
/// Provision (or refresh) the token for an agent's extra matrix account. /// Provision (or refresh) the token for an agent's extra matrix account.
///
/// password mode → `m.login.password`; token mode → validate via `whoami`. /// password mode → `m.login.password`; token mode → validate via `whoami`.
/// On success writes the token to `matrix-token-<account>` via hive-priv and /// On success writes the token to `matrix-token-<account>` via hive-priv and
/// kicks the daemon. Operator-authenticated (dashboard). Never echoes the /// kicks the daemon. Operator-authenticated (dashboard). Never echoes the
@ -293,7 +296,9 @@ struct GithubAccountResult {
} }
/// Provision (or refresh) an agent's GitHub PAT from the dashboard /// Provision (or refresh) an agent's GitHub PAT from the dashboard
/// credentials tab. Validates the agent name, then writes the PAT to /// credentials tab.
///
/// Validates the agent name, then writes the PAT to
/// `<state>/github-token` (`0600`, agent-owned) via hive-priv. No account /// `<state>/github-token` (`0600`, agent-owned) via hive-priv. No account
/// creation and no daemon to kick — the agent's `gh` wrapper / git credential /// creation and no daemon to kick — the agent's `gh` wrapper / git credential
/// helper read the file live, so the new token takes effect immediately. /// helper read the file live, so the new token takes effect immediately.
@ -338,10 +343,11 @@ struct GithubAccountStatus {
present: bool, present: bool,
} }
/// `GET /api/github-account?agent=<name>` — whether the agent has a GitHub /// Whether the agent has a GitHub
/// PAT provisioned (its `github-token` file exists). Lets the credentials tab /// PAT provisioned (its `github-token` file exists).
/// show "token stored" vs "not set" instead of a black-hole paste field. ///
/// Never returns the token itself. /// Lets the credentials tab show "token stored" vs "not set" instead of a
/// black-hole paste field. Never returns the token itself.
#[utoipa::path( #[utoipa::path(
get, get,
path = "/api/github-account", path = "/api/github-account",

View file

@ -179,11 +179,12 @@ pub(super) struct MetaUpdateForm {
} }
/// Bulk-update selected meta flake inputs, then rebuild the affected /// Bulk-update selected meta flake inputs, then rebuild the affected
/// agents in the background. Idempotent w.r.t. selection — choosing /// agents in the background.
/// an input that's already at the latest sha is a no-op (no commit, ///
/// no rebuild ripple). Returns immediately after queueing the work; /// Idempotent w.r.t. selection — choosing an input that's already at
/// dashboard polls for progress via container `pending` spinners + /// the latest sha is a no-op (no commit, no rebuild ripple). Returns
/// the meta-inputs row sha update. /// immediately after queueing the work; dashboard polls for progress
/// via container `pending` spinners + the meta-inputs row sha update.
#[utoipa::path( #[utoipa::path(
post, post,
path = "/api/meta-update", path = "/api/meta-update",

View file

@ -16,6 +16,7 @@ use crate::container_stats::ContainerResource;
use crate::hive_stats::HiveStats; use crate::hive_stats::HiveStats;
/// Unread operator-directed messages for the dashboard's Y3R C4LL inbox. /// Unread operator-directed messages for the dashboard's Y3R C4LL inbox.
///
/// Returns messages addressed to `"operator"` that haven't been /// Returns messages addressed to `"operator"` that haven't been
/// acked yet (the operator clears them via the existing /// acked yet (the operator clears them via the existing
/// `POST /api/agent/operator/mark-all-read`). Newest-first; path-shaped /// `POST /api/agent/operator/mark-all-read`). Newest-first; path-shaped
@ -72,12 +73,14 @@ pub(super) async fn api_operator_inbox(State(state): State<AppState>) -> Respons
#[derive(Deserialize, IntoParams)] #[derive(Deserialize, IntoParams)]
pub(super) struct StatsHiveQuery { pub(super) struct StatsHiveQuery {
/// Stats window; defaults to `24h`.
window: Option<String>, window: Option<String>,
} }
/// Hive-wide turn-stats rollup for the dashboard swarm-stats view. /// Hive-wide turn-stats rollup for the dashboard swarm-stats view.
///
/// Aggregates every agent's `hyperhive-turn-stats.sqlite` read-only /// Aggregates every agent's `hyperhive-turn-stats.sqlite` read-only
/// (skips missing/unreadable ones). Window defaults to `24h`. /// (skips missing/unreadable ones).
#[utoipa::path( #[utoipa::path(
get, get,
path = "/api/stats-hive", path = "/api/stats-hive",
@ -97,8 +100,10 @@ pub(super) async fn api_stats_hive(
.into_response() .into_response()
} }
/// Live per-agent-container CPU + memory load from cgroup v2. Samples /// Live per-agent-container CPU + memory load from cgroup v2.
/// CPU over a short interval (~200 ms), so this call briefly awaits. ///
/// Samples CPU over a short interval (~200 ms), so this call briefly
/// awaits.
#[utoipa::path( #[utoipa::path(
get, get,
path = "/api/container-resources", path = "/api/container-resources",
@ -109,9 +114,10 @@ pub(super) async fn api_container_resources() -> Response {
axum::Json(crate::container_stats::gather().await).into_response() axum::Json(crate::container_stats::gather().await).into_response()
} }
/// `GET /api/audit-log` — most-recent agent-initiated privileged-action /// Most-recent agent-initiated privileged-action
/// audit entries, newest first (server-clamped to 500). Backs the /// audit entries, newest first (server-clamped to 500).
/// operator dashboard's audit view. Returns ///
/// Backs the operator dashboard's audit view. Returns
/// `{ "entries": [AuditEntry…], "total": N }` so the UI can show /// `{ "entries": [AuditEntry…], "total": N }` so the UI can show
/// "latest 500 of N" rather than silently capping. `ts_unix` is in /// "latest 500 of N" rather than silently capping. `ts_unix` is in
/// **seconds**. /// **seconds**.
@ -138,11 +144,12 @@ pub(super) async fn api_audit_log(State(state): State<AppState>) -> Response {
} }
/// Operator-driven "clear this agent's inbox" — backs the side-panel /// Operator-driven "clear this agent's inbox" — backs the side-panel
/// "mark all read" button. Marks every message addressed to the /// "mark all read" button.
/// agent as acked (backfilling `delivered_at` for any still-pending ///
/// rows so vacuum can collect them). Returns `{ "marked": N }` so the /// Marks every message addressed to the agent as acked (backfilling
/// frontend can show "cleared N messages" feedback without an extra /// `delivered_at` for any still-pending rows so vacuum can collect
/// fetch. /// them). Returns `{ "marked": N }` so the frontend can show "cleared
/// N messages" feedback without an extra fetch.
#[utoipa::path( #[utoipa::path(
post, post,
path = "/api/agent/{name}/mark-all-read", path = "/api/agent/{name}/mark-all-read",
@ -186,7 +193,7 @@ pub(super) struct OpSendForm {
body: String, body: String,
} }
/// `POST /api/op-send` — operator compose: drop a message into the /// Operator compose: drop a message into the
/// broker addressed to `to` (or `*` to broadcast). /// broker addressed to `to` (or `*` to broadcast).
#[utoipa::path( #[utoipa::path(
post, post,
@ -242,7 +249,7 @@ pub(super) struct RequestSpawnForm {
name: String, name: String,
} }
/// `POST /api/request-spawn` — queue a spawn approval for `name`. /// Queue a spawn approval for `name`.
#[utoipa::path( #[utoipa::path(
post, post,
path = "/api/request-spawn", path = "/api/request-spawn",

View file

@ -37,7 +37,7 @@ pub(super) struct ToolGroupsSnapshot {
effective: std::collections::BTreeMap<String, Vec<String>>, effective: std::collections::BTreeMap<String, Vec<String>>,
} }
/// `GET /api/tool-groups` — every known tool-group name + description, /// Every known tool-group name + description,
/// plus the per-agent explicit/effective assignment maps. /// plus the per-agent explicit/effective assignment maps.
#[utoipa::path( #[utoipa::path(
get, get,
@ -119,7 +119,7 @@ pub(super) struct SetToolGroupsBody {
groups: Vec<String>, groups: Vec<String>,
} }
/// `POST /api/tool-groups/{agent}` — replace `agent`'s explicit tool-group /// Replace `agent`'s explicit tool-group
/// assignment (JSON body `{"groups": [...]}`). /// assignment (JSON body `{"groups": [...]}`).
#[utoipa::path( #[utoipa::path(
post, post,
@ -187,7 +187,7 @@ pub(super) struct CapabilitiesSnapshot {
effective: std::collections::BTreeMap<String, Vec<String>>, effective: std::collections::BTreeMap<String, Vec<String>>,
} }
/// `GET /api/capabilities` — every known capability name + description, /// Every known capability name + description,
/// plus the per-agent explicit/effective grant maps. /// plus the per-agent explicit/effective grant maps.
#[utoipa::path( #[utoipa::path(
get, get,
@ -227,7 +227,7 @@ pub(super) struct SetCapabilitiesBody {
caps: Vec<String>, caps: Vec<String>,
} }
/// `POST /api/capabilities/{agent}` — replace `agent`'s explicit capability /// Replace `agent`'s explicit capability
/// grant set (JSON body `{"caps": [...]}`). /// grant set (JSON body `{"caps": [...]}`).
#[utoipa::path( #[utoipa::path(
post, post,
@ -299,13 +299,14 @@ pub(super) struct BatchPermsBody {
/// `(logical agent, new groups?, new caps?)`. /// `(logical agent, new groups?, new caps?)`.
type StagedPerm = (String, Option<Vec<String>>, Option<Vec<String>>); type StagedPerm = (String, Option<Vec<String>>, Option<Vec<String>>);
/// Batch permission apply — `POST /api/permissions`. The save-all /// Batch permission apply — `POST /api/permissions`.
/// permissions UI sends only the perm-types that actually changed per ///
/// agent; each affected agent gets ONE combined `PermChange`, so the /// The save-all permissions UI sends only the perm-types that actually
/// dedup key collapses to `(kind, agent)` and an agent whose caps AND /// changed per agent; each affected agent gets ONE combined
/// groups both changed rebuilds once, not twice. The whole batch is /// `PermChange`, so the dedup key collapses to `(kind, agent)` and an
/// atomic: every change is validated up front and on any validation /// agent whose caps AND groups both changed rebuilds once, not twice.
/// error nothing is written or enqueued. /// The whole batch is atomic: every change is validated up front and
/// on any validation error nothing is written or enqueued.
#[utoipa::path( #[utoipa::path(
post, post,
path = "/api/permissions", path = "/api/permissions",
@ -382,7 +383,7 @@ pub(super) struct StalePermsResponse {
stale: Vec<String>, stale: Vec<String>,
} }
/// `GET /api/permissions/stale` — agent names with explicit permission /// Agent names with explicit permission
/// entries but no matching live container or kept-state dir. /// entries but no matching live container or kept-state dir.
#[utoipa::path( #[utoipa::path(
get, get,
@ -428,11 +429,12 @@ pub(super) async fn get_stale_permissions(
} }
/// Clear all explicit permission entries for a named agent without /// Clear all explicit permission entries for a named agent without
/// requiring it to exist in the live roster. Used by the P3RM1SS10NS /// requiring it to exist in the live roster.
/// tab's "remove" button for agents that have stale explicit entries ///
/// in `tool-groups.json` / `capabilities.json` but are no longer /// Used by the P3RM1SS10NS tab's "remove" button for agents that have
/// running (e.g. an agent that was renamed or destroyed while its /// stale explicit entries in `tool-groups.json` / `capabilities.json`
/// JSON entries persisted). /// but are no longer running (e.g. an agent that was renamed or
/// destroyed while its JSON entries persisted).
/// ///
/// Bypasses `guard_agent_name`'s live-roster check intentionally — /// Bypasses `guard_agent_name`'s live-roster check intentionally —
/// the whole point is to remove entries for non-roster agents. Only /// the whole point is to remove entries for non-roster agents. Only

View file

@ -38,7 +38,7 @@ fn with_cors(resp: impl IntoResponse) -> Response {
resp resp
} }
/// `POST /answer-question/{id}` — record the operator's answer and /// Record the operator's answer and
/// notify the asker. /// notify the asker.
#[utoipa::path( #[utoipa::path(
post, post,
@ -94,14 +94,14 @@ pub(super) async fn post_answer_question(
with_cors(resp) with_cors(resp)
} }
/// Resolve a pending operator question with a sentinel answer when /// Resolve a pending question with the
/// the operator decides not to / can't answer. The asker harness
/// receives a `QuestionAnswered` event with `answer = "[cancelled]"`
/// so it can fall back on whatever default it had. Same code path as
/// a real answer — just lets the operator close the loop instead of
/// letting the question dangle forever.
/// `POST /cancel-question/{id}` — resolve a pending question with the
/// `[cancelled]` sentinel answer. /// `[cancelled]` sentinel answer.
///
/// Used when the operator decides not to / can't answer. The asker
/// harness receives a `QuestionAnswered` event with
/// `answer = "[cancelled]"` so it can fall back on whatever default
/// it had. Same code path as a real answer — just lets the operator
/// close the loop instead of letting the question dangle forever.
#[utoipa::path( #[utoipa::path(
post, post,
path = "/api/cancel-question/{id}", path = "/api/cancel-question/{id}",

View file

@ -18,9 +18,11 @@ use crate::scheduled_prompts_worker::FireNowReport;
use super::{AppState, error_problem, error_response}; use super::{AppState, error_problem, error_response};
/// `GET /api/schedules` — snapshot of every schedule for the /// Snapshot of every schedule for the
/// scheduled-prompts tab. Returns the wire shape directly /// scheduled-prompts tab.
/// so the frontend can render without an extra translation layer. ///
/// Returns the wire shape directly so the frontend can render
/// without an extra translation layer.
// `hive_sh4re::WireSchedule` (the actual body) has no `ToSchema` — adding // `hive_sh4re::WireSchedule` (the actual body) has no `ToSchema` — adding
// one would pull `utoipa` into the wire-types crate for a single dashboard // one would pull `utoipa` into the wire-types crate for a single dashboard
// endpoint. `serde_json::Value` placeholder; see the batch report. // endpoint. `serde_json::Value` placeholder; see the batch report.
@ -57,11 +59,12 @@ pub(super) async fn api_schedules(State(state): State<AppState>) -> Response {
} }
} }
/// `POST /api/schedules` — operator-direct schedule creation /// Operator-direct schedule creation
/// (mara: "user can add them manually"). Accepts the same /// (mara: "user can add them manually").
/// `SchedulePromptPayload` shape as the manager request flow but ///
/// skips the approval gate — the operator click *is* the /// Accepts the same `SchedulePromptPayload` shape as the manager
/// approval. The schedule lands directly with /// request flow but skips the approval gate — the operator click
/// *is* the approval. The schedule lands directly with
/// `source = Operator` and the worker picks it up at fire time. /// `source = Operator` and the worker picks it up at fire time.
#[utoipa::path( #[utoipa::path(
post, post,
@ -119,14 +122,16 @@ pub(super) struct FireNowBody {
reset_timer: bool, reset_timer: bool,
} }
/// `POST /api/schedules/{id}/fire-now` — operator-initiated /// Operator-initiated
/// out-of-band fire of a scheduled prompt. Runs the per-target /// out-of-band fire of a scheduled prompt.
/// fan-out once immediately and reports per-target outcome counts. ///
/// One-shot schedules are consumed (cancelled) by a manual fire — /// Runs the per-target fan-out once immediately and reports
/// the operator's intent is "send this now, the scheduled time was /// per-target outcome counts. One-shot schedules are consumed
/// wrong." For recurring schedules the cadence stays intact unless /// (cancelled) by a manual fire — the operator's intent is "send
/// the body carries `{"reset_timer": true}`, in which case the /// this now, the scheduled time was wrong." For recurring schedules
/// countdown is re-armed from now (`next_fire_at = now + interval`). /// the cadence stays intact unless the body carries
/// `{"reset_timer": true}`, in which case the countdown is re-armed
/// from now (`next_fire_at = now + interval`).
#[utoipa::path( #[utoipa::path(
post, post,
path = "/api/schedules/{id}/fire-now", path = "/api/schedules/{id}/fire-now",
@ -153,7 +158,7 @@ pub(super) async fn post_schedule_fire_now(
} }
} }
/// `POST /api/rebuild-queue/{id}/cancel` — drop still-queued work from the job /// Drop still-queued work from the job
/// queue. /// queue.
/// ///
/// `id` is a **node** id. A DAG's root cancels the whole group (the scheduler /// `id` is a **node** id. A DAG's root cancels the whole group (the scheduler
@ -243,18 +248,20 @@ where
T::deserialize(deserializer).map(Some) T::deserialize(deserializer).map(Some)
} }
/// `PATCH /api/schedules/{id}` — partial update of an existing /// Partial update of an existing
/// schedule. Mutable fields: `body`, `description`, /// schedule.
/// `interval_seconds`, `next_fire_at_unix`, plus the target set ///
/// via `targets_add` / `targets_remove`. Both target lists /// Mutable fields: `body`, `description`, `interval_seconds`,
/// are applied in the same transaction as the scalar fields with /// `next_fire_at_unix`, plus the target set via `targets_add` /
/// removes-before-adds; re-adding a previously-removed target /// `targets_remove`. Both target lists are applied in the same
/// resets per-target history (fresh start); draining all targets /// transaction as the scalar fields with removes-before-adds;
/// auto-cancels the parent schedule. JSON body uses missing-key /// re-adding a previously-removed target resets per-target history
/// = "leave alone", explicit null = "clear" for `description` + /// (fresh start); draining all targets auto-cancels the parent
/// `interval_seconds`. Cancelled schedules are refused — submit /// schedule. JSON body uses missing-key = "leave alone", explicit
/// a new one instead. Returns the updated `WireSchedule` so the /// null = "clear" for `description` + `interval_seconds`. Cancelled
/// caller's post-edit refresh has the new state inline. /// schedules are refused — submit a new one instead. Returns the
/// updated `WireSchedule` so the caller's post-edit refresh has the
/// new state inline.
#[utoipa::path( #[utoipa::path(
patch, patch,
path = "/api/schedules/{id}", path = "/api/schedules/{id}",
@ -292,9 +299,11 @@ pub(super) async fn patch_schedule(
} }
} }
/// `POST /api/schedules/{id}/pause` — pause a schedule so the worker /// Pause a schedule so the worker
/// skips it until explicitly resumed. Idempotent; no-op on an already- /// skips it until explicitly resumed.
/// paused row. Returns 404 when the schedule is cancelled or not found. ///
/// Idempotent; no-op on an already-paused row. Returns 404 when the
/// schedule is cancelled or not found.
#[utoipa::path( #[utoipa::path(
post, post,
path = "/api/schedules/{id}/pause", path = "/api/schedules/{id}/pause",
@ -325,7 +334,8 @@ pub(super) async fn post_schedule_pause(
} }
} }
/// `POST /api/schedules/{id}/resume` — resume a paused schedule. /// Resume a paused schedule.
///
/// Idempotent; no-op on an already-active row. Returns 404 when the /// Idempotent; no-op on an already-active row. Returns 404 when the
/// schedule is cancelled or not found. /// schedule is cancelled or not found.
#[utoipa::path( #[utoipa::path(
@ -358,10 +368,12 @@ pub(super) async fn post_schedule_resume(
} }
} }
/// `POST /api/schedules/{id}/cancel` — operator-side cancel /// Operator-side cancel
/// (whole schedule when no `targets` field, partial when one is /// (whole schedule when no `targets` field, partial when one is
/// provided). Operator bypasses the topology check; the manager /// provided).
/// surface enforces it for agent callers. ///
/// Operator bypasses the topology check; the manager surface
/// enforces it for agent callers.
#[utoipa::path( #[utoipa::path(
post, post,
path = "/api/schedules/{id}/cancel", path = "/api/schedules/{id}/cancel",

View file

@ -18,6 +18,9 @@ use crate::paths::{AGENTS_ROOT, SHARED_ROOT};
#[derive(Deserialize, IntoParams)] #[derive(Deserialize, IntoParams)]
pub(super) struct StateFileQuery { pub(super) struct StateFileQuery {
/// Absolute path under an agent's `state/` dir or under `shared/`;
/// checked against the allow-list (`docs/security.md::State-file
/// endpoint`).
path: String, path: String,
} }
@ -164,10 +167,10 @@ pub fn scan_validated_paths(body: &str) -> Vec<String> {
out out
} }
/// `GET /api/state-file?path=…` — serve an allow-listed per-agent /// Serve an allow-listed file.
/// `state/` or `shared/` file. Raster images get their real ///
/// content-type; everything else is served as (possibly truncated) /// Raster images get their real content-type; everything else is
/// text. /// served as (possibly truncated) text.
#[utoipa::path( #[utoipa::path(
get, get,
path = "/api/state-file", path = "/api/state-file",

View file

@ -293,10 +293,12 @@ where
/// minutes. /// minutes.
const CRASH_WARNING_WINDOW: std::time::Duration = std::time::Duration::from_mins(10); const CRASH_WARNING_WINDOW: std::time::Duration = std::time::Duration::from_mins(10);
/// `GET /api/state` — cold-load snapshot of the whole dashboard: roster, /// Cold-load snapshot of the whole dashboard.
/// approvals (+ history), questions (+ history), tombstones, job queue, ///
/// meta inputs, and more. Live clients then follow `/api/dashboard/stream` /// Includes the roster, approvals (+ history), questions (+ history),
/// (SSE) for incremental updates keyed off `seq`. /// tombstones, job queue, meta inputs, and more. Live clients then
/// follow `/api/dashboard/stream` (SSE) for incremental updates keyed
/// off `seq`.
// `StateSnapshot` is a large tree of nested view types (`ContainerView`, // `StateSnapshot` is a large tree of nested view types (`ContainerView`,
// `ApprovalView`, `QuestionView`, ...) with no `ToSchema` anywhere in that // `ApprovalView`, `QuestionView`, ...) with no `ToSchema` anywhere in that
// graph; wiring it up is a schema-modelling project of its own, well past // graph; wiring it up is a schema-modelling project of its own, well past

View file

@ -104,7 +104,7 @@ pub(crate) async fn emit_tombstones_snapshot(coord: &Arc<Coordinator>) {
}); });
} }
/// `POST /api/purge-tombstone/{name}` — wipe a tombstoned agent's /// Wipe a tombstoned agent's
/// retained state dir + applied config dir entirely. /// retained state dir + applied config dir entirely.
#[utoipa::path( #[utoipa::path(
post, post,

View file

@ -47,7 +47,8 @@ pub(super) struct SetParentBulkEntry {
new_parent: Option<String>, new_parent: Option<String>,
} }
/// `POST /api/topology/set-parent` — operator-driven parent move. /// Operator-driven parent move.
///
/// Form fields: `child` (required, agent name), `new_parent` /// Form fields: `child` (required, agent name), `new_parent`
/// (optional — empty / absent string ⇒ promote to root). Refuses /// (optional — empty / absent string ⇒ promote to root). Refuses
/// cycles and unknown agents (surfaced async on the job view — this /// cycles and unknown agents (surfaced async on the job view — this
@ -102,12 +103,13 @@ pub(super) async fn post_set_parent(
Ok((StatusCode::OK, "ok").into_response()) Ok((StatusCode::OK, "ok").into_response())
} }
/// `POST /api/topology/set-parent-bulk` — move multiple agents in a single /// Move multiple agents in a
/// request, producing **one** git commit. JSON body: `[{"child":"name", /// single request, producing **one** git commit.
/// "new_parent":"target-or-null"}, ...]`. Empty array is a no-op (200 OK). ///
/// First identifier that fails to parse aborts the whole batch before /// JSON body: `[{"child":"name", "new_parent":"target-or-null"}, ...]`.
/// anything is submitted — a partially-invalid bulk move never reaches /// Empty array is a no-op (200 OK). First identifier that fails to
/// the queue. /// parse aborts the whole batch before anything is submitted — a
/// partially-invalid bulk move never reaches the queue.
#[utoipa::path( #[utoipa::path(
post, post,
path = "/api/topology/set-parent-bulk", path = "/api/topology/set-parent-bulk",

View file

@ -58,8 +58,10 @@ pub(super) struct PushWebhookRepo {
} }
/// POST `/webhook/knowledge` — Forgejo push webhook for /// POST `/webhook/knowledge` — Forgejo push webhook for
/// `internal/knowledge`. Runs `git pull` on the local clone so /// `internal/knowledge`.
/// agents see up-to-date documents on their next turn. ///
/// Runs `git pull` on the local clone so agents see up-to-date documents
/// on their next turn.
/// ///
/// Expected Forgejo webhook configuration: /// Expected Forgejo webhook configuration:
/// - URL: `https://<HYPERHIVE_HIVE_DOMAIN>/webhook/knowledge` /// - URL: `https://<HYPERHIVE_HIVE_DOMAIN>/webhook/knowledge`