hive-c0re: scrub manager-surface cookies + SetParent doc fix (#715 batch 7)
This commit is contained in:
parent
dd07ca946e
commit
2bdb8c7d51
5 changed files with 37 additions and 37 deletions
|
|
@ -246,7 +246,7 @@ impl Approvals {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Withdraw a pending approval (closes #250). Returns the now-updated
|
/// Withdraw a pending approval. Returns the now-updated
|
||||||
/// row so the caller can emit `ApprovalResolved` with the right
|
/// row so the caller can emit `ApprovalResolved` with the right
|
||||||
/// kind / agent / sha. Errors if the approval isn't pending — once
|
/// kind / agent / sha. Errors if the approval isn't pending — once
|
||||||
/// it's approved/denied/failed/cancelled, the resolution is final.
|
/// it's approved/denied/failed/cancelled, the resolution is final.
|
||||||
|
|
@ -329,8 +329,7 @@ impl Approvals {
|
||||||
/// deserialize. A single malformed / unknown-kind row must never blank
|
/// deserialize. A single malformed / unknown-kind row must never blank
|
||||||
/// the whole list: `collect::<Result<Vec>>()` is all-or-nothing, so one
|
/// the whole list: `collect::<Result<Vec>>()` is all-or-nothing, so one
|
||||||
/// bad row used to make `pending()` / `recent_resolved()` error out
|
/// bad row used to make `pending()` / `recent_resolved()` error out
|
||||||
/// wholesale — the dashboard then rendered an empty approvals queue
|
/// wholesale — the dashboard then rendered an empty approvals queue.
|
||||||
/// (issue #160, an unhandled `init_config` kind poisoning every read).
|
|
||||||
fn collect_lenient(rows: impl Iterator<Item = rusqlite::Result<Approval>>) -> Vec<Approval> {
|
fn collect_lenient(rows: impl Iterator<Item = rusqlite::Result<Approval>>) -> Vec<Approval> {
|
||||||
rows.filter_map(|r| match r {
|
rows.filter_map(|r| match r {
|
||||||
Ok(a) => Some(a),
|
Ok(a) => Some(a),
|
||||||
|
|
@ -435,7 +434,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn init_config_approval_round_trips() {
|
fn init_config_approval_round_trips() {
|
||||||
// Regression for #160: an `init_config` row used to fail
|
// Regression test: an `init_config` row used to fail
|
||||||
// deserialization (row_to_approval matched only apply_commit +
|
// deserialization (row_to_approval matched only apply_commit +
|
||||||
// spawn), erroring out the whole `pending()` query — every
|
// spawn), erroring out the whole `pending()` query — every
|
||||||
// approval then vanished from the dashboard.
|
// approval then vanished from the dashboard.
|
||||||
|
|
@ -465,9 +464,9 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn mark_cancelled_transitions_pending_row() {
|
fn mark_cancelled_transitions_pending_row() {
|
||||||
// #250: manager withdraws a pending approval. Row leaves
|
// Manager withdraws a pending approval. Row leaves pending(),
|
||||||
// pending(), shows up in recent_resolved() with the cancelled
|
// shows up in recent_resolved() with the cancelled status + a
|
||||||
// status + a "cancelled by <who>" note.
|
// "cancelled by <who>" note.
|
||||||
let (_dir, _path, db) = open_temp();
|
let (_dir, _path, db) = open_temp();
|
||||||
let id = db
|
let id = db
|
||||||
.submit_kind(
|
.submit_kind(
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
//! `hivectl` — operator-facing host CLI for hyperhive (#655).
|
//! `hivectl` — operator-facing host CLI for hyperhive.
|
||||||
//!
|
//!
|
||||||
//! Sibling binary to the `hive-c0re` daemon. Where `hive-c0re`'s
|
//! Sibling binary to the `hive-c0re` daemon. Where `hive-c0re`'s
|
||||||
//! subcommands focus on the broker / approval / topology surface
|
//! subcommands focus on the broker / approval / topology surface
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
//! starting with manual user provisioning on the bundled forge +
|
//! starting with manual user provisioning on the bundled forge +
|
||||||
//! matrix containers when c0re's automatic boot-time sweep is
|
//! matrix containers when c0re's automatic boot-time sweep is
|
||||||
//! inappropriate (recovery, debugging, single-shot reprovisioning,
|
//! inappropriate (recovery, debugging, single-shot reprovisioning,
|
||||||
//! verifying the registration token path post-#644).
|
//! verifying the registration token path).
|
||||||
//!
|
//!
|
||||||
//! Verbs read configuration off the same on-disk paths c0re uses
|
//! Verbs read configuration off the same on-disk paths c0re uses
|
||||||
//! (`/var/lib/hyperhive/forge-core-token`,
|
//! (`/var/lib/hyperhive/forge-core-token`,
|
||||||
|
|
@ -69,11 +69,11 @@ enum ForgeCmd {
|
||||||
/// When `<name>` is **not** an agent (a human or any other
|
/// When `<name>` is **not** an agent (a human or any other
|
||||||
/// non-container account), creates the forgejo user and prints the
|
/// non-container account), creates the forgejo user and prints the
|
||||||
/// freshly-minted token to stdout — no `/var/lib/hyperhive/agents/`
|
/// freshly-minted token to stdout — no `/var/lib/hyperhive/agents/`
|
||||||
/// directory is created for the user (#662).
|
/// directory is created for the user.
|
||||||
///
|
///
|
||||||
/// Without `--password` / `--password-stdin` a random throwaway is
|
/// Without `--password` / `--password-stdin` a random throwaway is
|
||||||
/// used (fine for agents — they auth by token via tea / hive-forge).
|
/// used (fine for agents — they auth by token via tea / hive-forge).
|
||||||
/// Set a password to log into the forge web UI afterwards (#663).
|
/// Set a password to log into the forge web UI afterwards.
|
||||||
/// `--password` is idempotent: re-running with the same value sets
|
/// `--password` is idempotent: re-running with the same value sets
|
||||||
/// the same password (covers password resets on already-created
|
/// the same password (covers password resets on already-created
|
||||||
/// accounts since `forgejo admin user create` silently no-ops once
|
/// accounts since `forgejo admin user create` silently no-ops once
|
||||||
|
|
@ -111,21 +111,20 @@ enum MatrixCmd {
|
||||||
/// When `<name>` is **not** an agent (a human or any other
|
/// When `<name>` is **not** an agent (a human or any other
|
||||||
/// non-container account), registers the matrix user and prints
|
/// non-container account), registers the matrix user and prints
|
||||||
/// the freshly-minted access token to stdout — no
|
/// the freshly-minted access token to stdout — no
|
||||||
/// `/var/lib/hyperhive/agents/` directory is created for the user
|
/// `/var/lib/hyperhive/agents/` directory is created for the user.
|
||||||
/// (#662).
|
|
||||||
///
|
///
|
||||||
/// Without `--password` / `--password-stdin` a random throwaway is
|
/// Without `--password` / `--password-stdin` a random throwaway is
|
||||||
/// used (fine for agents — they auth by `access_token`, never by
|
/// used (fine for agents — they auth by `access_token`, never by
|
||||||
/// password). Set a password to log into a matrix web client
|
/// password). Set a password to log into a matrix web client
|
||||||
/// afterwards (#663).
|
/// afterwards.
|
||||||
CreateUser {
|
CreateUser {
|
||||||
/// Matrix localpart. For agents: the container/agent name.
|
/// Matrix localpart. For agents: the container/agent name.
|
||||||
/// For humans: any matrix localpart — `mara`, `damocles`, etc.
|
/// For humans: any matrix localpart — `mara`, `damocles`, etc.
|
||||||
name: String,
|
name: String,
|
||||||
/// Set the account password to this string instead of a random
|
/// Set the account password to this string instead of a random
|
||||||
/// throwaway. Use this for operator accounts that need to log
|
/// throwaway. Use this for operator accounts that need to log
|
||||||
/// into matrix web clients via `m.login.password` (#663).
|
/// into matrix web clients via `m.login.password`. Mutually
|
||||||
/// Mutually exclusive with `--password-stdin`. WARNING: the
|
/// exclusive with `--password-stdin`. WARNING: the
|
||||||
/// password is visible in shell history + process listings;
|
/// password is visible in shell history + process listings;
|
||||||
/// prefer `--password-stdin` for anything sensitive.
|
/// prefer `--password-stdin` for anything sensitive.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
|
|
|
||||||
|
|
@ -84,8 +84,9 @@ enum Cmd {
|
||||||
Deny { id: i64 },
|
Deny { id: i64 },
|
||||||
/// Move an agent in the topology tree. Set `--parent` to a new
|
/// Move an agent in the topology tree. Set `--parent` to a new
|
||||||
/// parent agent name; pass `--root` to promote the agent to root
|
/// parent agent name; pass `--root` to promote the agent to root
|
||||||
/// (no parent). Refuses cycles, unknown agents, and any attempt
|
/// (no parent). Refuses cycles and unknown agents. The manager
|
||||||
/// to reparent the manager.
|
/// is reparentable like any other agent — its privileges come
|
||||||
|
/// from the privileged MCP socket, not its tree position.
|
||||||
SetParent {
|
SetParent {
|
||||||
child: String,
|
child: String,
|
||||||
/// New parent agent name. Mutually exclusive with `--root`.
|
/// New parent agent name. Mutually exclusive with `--root`.
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ pub fn start(coord: Arc<Coordinator>) -> Result<()> {
|
||||||
}
|
}
|
||||||
let listener = UnixListener::bind(&socket)
|
let listener = UnixListener::bind(&socket)
|
||||||
.with_context(|| format!("bind manager socket {}", socket.display()))?;
|
.with_context(|| format!("bind manager socket {}", socket.display()))?;
|
||||||
// 0666 so the in-container hm1nd user (post-#658) can connect;
|
// 0666 so the in-container hm1nd user (non-root) can connect;
|
||||||
// the bind source dir is manager-only on host. See agent_server.rs.
|
// the bind source dir is manager-only on host. See agent_server.rs.
|
||||||
use std::os::unix::fs::PermissionsExt as _;
|
use std::os::unix::fs::PermissionsExt as _;
|
||||||
std::fs::set_permissions(&socket, std::fs::Permissions::from_mode(0o666))
|
std::fs::set_permissions(&socket, std::fs::Permissions::from_mode(0o666))
|
||||||
|
|
@ -116,8 +116,9 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc<Coordinator>) -> ManagerResp
|
||||||
// Resolve magic-recipient sentinels (currently `<parent>`)
|
// Resolve magic-recipient sentinels (currently `<parent>`)
|
||||||
// against topology.json; no-op for ordinary names. The
|
// against topology.json; no-op for ordinary names. The
|
||||||
// manager has no parent in topology, so `<parent>`
|
// manager has no parent in topology, so `<parent>`
|
||||||
// resolves to OPERATOR_RECIPIENT — matching mara's
|
// resolves to OPERATOR_RECIPIENT — the "no parent → tell
|
||||||
// "no parent → tell the operator" rule (#692).
|
// the operator" fallback. See `docs/conventions.md::
|
||||||
|
// Recipient sentinels`.
|
||||||
let resolved = crate::topology::resolve_recipient(MANAGER_AGENT, to);
|
let resolved = crate::topology::resolve_recipient(MANAGER_AGENT, to);
|
||||||
match coord.broker.send(&Message {
|
match coord.broker.send(&Message {
|
||||||
from: MANAGER_AGENT.to_owned(),
|
from: MANAGER_AGENT.to_owned(),
|
||||||
|
|
@ -502,7 +503,7 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc<Coordinator>) -> ManagerResp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ManagerRequest::SetStatus { text } => {
|
ManagerRequest::SetStatus { text } => {
|
||||||
// #720: cap length + reject multi-line so a confused caller
|
// Cap length + reject multi-line so a confused caller
|
||||||
// can't dump a multi-paragraph session report into the
|
// can't dump a multi-paragraph session report into the
|
||||||
// dashboard chip.
|
// dashboard chip.
|
||||||
if let Err(message) = crate::limits::check_status_text(text) {
|
if let Err(message) = crate::limits::check_status_text(text) {
|
||||||
|
|
@ -533,10 +534,10 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc<Coordinator>) -> ManagerResp
|
||||||
}
|
}
|
||||||
ManagerRequest::GetAgentMeta { name } => {
|
ManagerRequest::GetAgentMeta { name } => {
|
||||||
let target = name.as_deref().unwrap_or(MANAGER_AGENT);
|
let target = name.as_deref().unwrap_or(MANAGER_AGENT);
|
||||||
// #432: gate status on the target's running state so a
|
// Gate status on the target's running state so a stopped
|
||||||
// stopped container's stale on-disk status doesn't leak
|
// container's stale on-disk status doesn't leak through.
|
||||||
// through. Also surface `running` itself so callers can
|
// Also surface `running` itself so callers can tell
|
||||||
// tell (e.g. "iris is down" vs "iris has no status set").
|
// (e.g. "iris is down" vs "iris has no status set").
|
||||||
let (status_text, status_set_at, running) =
|
let (status_text, status_set_at, running) =
|
||||||
crate::container_view::read_agent_status_live(target).await;
|
crate::container_view::read_agent_status_live(target).await;
|
||||||
let role = if target == MANAGER_AGENT {
|
let role = if target == MANAGER_AGENT {
|
||||||
|
|
@ -676,7 +677,7 @@ async fn submit_apply_commit(
|
||||||
.approvals
|
.approvals
|
||||||
.set_fetched_sha(id, &sha)
|
.set_fetched_sha(id, &sha)
|
||||||
.map_err(|e| anyhow::anyhow!("persist fetched_sha: {e:#}"))?;
|
.map_err(|e| anyhow::anyhow!("persist fetched_sha: {e:#}"))?;
|
||||||
// #317 pre-flight gates: both reject the apply before approval if
|
// Pre-flight gates: both reject the apply before approval if
|
||||||
// the agent's flake state would inflate meta's lock with duplicates
|
// the agent's flake state would inflate meta's lock with duplicates
|
||||||
// or lie about what nix will fetch. Both checks independently read
|
// or lie about what nix will fetch. Both checks independently read
|
||||||
// `<tag>:flake.lock` via git — they don't share state. Order matters
|
// `<tag>:flake.lock` via git — they don't share state. Order matters
|
||||||
|
|
@ -889,8 +890,8 @@ async fn handle_fire_schedule_now(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Authorize + dispatch a `EditSchedule` patch (#474). Same
|
/// Authorize + dispatch a `EditSchedule` patch. Same ownership
|
||||||
/// ownership rules as `CancelSchedule` — the manager can edit
|
/// rules as `CancelSchedule` — the manager can edit
|
||||||
/// schedules it owns + any owned by an agent in its subtree.
|
/// schedules it owns + any owned by an agent in its subtree.
|
||||||
/// Forwards the partial payload to
|
/// Forwards the partial payload to
|
||||||
/// `ScheduledPrompts::update` which enforces the cancelled-row /
|
/// `ScheduledPrompts::update` which enforces the cancelled-row /
|
||||||
|
|
|
||||||
|
|
@ -209,11 +209,11 @@ pub fn handle_cancel_loose_end(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Manager-only guard on the `Approval` cancel arm. Pulled out so
|
/// Manager-only guard on the `Approval` cancel arm. Pulled out so
|
||||||
/// the auth check has its own focused unit test (argus nit on #508)
|
/// the auth check has its own focused unit test — testing the full
|
||||||
/// — testing the full `handle_cancel_loose_end` flow would need a
|
/// `handle_cancel_loose_end` flow would need a `Coordinator` fixture
|
||||||
/// `Coordinator` fixture (broker + sqlite + in-memory questions),
|
/// (broker + sqlite + in-memory questions), which we don't have
|
||||||
/// which we don't have today. The check is a single string compare,
|
/// today. The check is a single string compare, so a function-level
|
||||||
/// so a function-level test gives the same coverage with no harness.
|
/// test gives the same coverage with no harness.
|
||||||
fn check_approval_canceller_is_manager(canceller: &str) -> Result<(), String> {
|
fn check_approval_canceller_is_manager(canceller: &str) -> Result<(), String> {
|
||||||
if canceller != hive_sh4re::MANAGER_AGENT {
|
if canceller != hive_sh4re::MANAGER_AGENT {
|
||||||
return Err("cancel_loose_end: only the manager can cancel approval rows".to_owned());
|
return Err("cancel_loose_end: only the manager can cancel approval rows".to_owned());
|
||||||
|
|
@ -227,9 +227,9 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn approval_cancel_rejects_sub_agent_callers() {
|
fn approval_cancel_rejects_sub_agent_callers() {
|
||||||
// Argus nit on #508: sub-agents must not be able to cancel
|
// Sub-agents must not be able to cancel approval rows even
|
||||||
// approval rows even if they invent an id. The guard is
|
// if they invent an id. The guard is server-side so client
|
||||||
// server-side so client cooperation is irrelevant.
|
// cooperation is irrelevant.
|
||||||
let err = check_approval_canceller_is_manager("bitburner").unwrap_err();
|
let err = check_approval_canceller_is_manager("bitburner").unwrap_err();
|
||||||
assert!(err.contains("only the manager"), "{err}");
|
assert!(err.contains("only the manager"), "{err}");
|
||||||
// Bonus: empty / operator strings also rejected (only the
|
// Bonus: empty / operator strings also rejected (only the
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue