refactor(#1834): derive cancel-loose-end privilege from the socket, not the MANAGER_AGENT name

The cancel-loose-end guards (cancel approval / question / reminder)
inferred manager-level privilege by string-matching the canceller
against the hardcoded `hive_sh4re::MANAGER_AGENT` ("ruth"). That laundered
privilege through a name: a request arrives on the privileged manager
socket, gets stamped with the bootstrap agent's name, and the guards
re-derive privilege from that name. Renaming or replacing the bootstrap
agent would then silently move privilege.

Privilege is a property of the SOCKET the request arrived on (the manager
socket is the trust boundary), so thread an explicit `privileged: bool`
through `dispatch_shared` → `handle_cancel_loose_end` → the three guards:

- `Broker::cancel_reminder_as` and `OperatorQuestions::cancel`: the
  `== MANAGER_AGENT` leg becomes `privileged` (owner/asker + operator name
  legs unchanged).
- `check_approval_canceller_is_manager(canceller)` →
  `check_can_cancel_approval(privileged)` (manager-socket-only); unit tests
  updated to assert on the flag.

The manager socket passes `privileged = true`; the agent socket passes
`false`. `MANAGER_AGENT` is still passed as the actor NAME for legitimate
attribution/routing (notifications, schedule ownership, bootstrap
destroy-protection) — those are not privilege checks and are left intact.
Scope is the privilege guards only.
This commit is contained in:
atlas 2026-06-22 02:07:14 +02:00
commit 53f49615fa
5 changed files with 65 additions and 49 deletions

View file

@ -142,14 +142,17 @@ pub fn handle_answer(
}
/// Handle `CancelLooseEnd` from either surface. Dispatches by kind to
/// either `OperatorQuestions::cancel` or `Broker::cancel_reminder_as`,
/// both of which do their own auth check (canceller == owner /
/// asker, or `operator`, or `manager`). On question cancel, fires
/// the per-kind cancel, each of which does its own auth check
/// (canceller == owner / asker, or `operator`, or `privileged`).
/// `privileged` is `true` when the request arrived on the manager
/// socket — privilege derives from the socket (the trust boundary),
/// not from matching a hardcoded agent name. On question cancel, fires
/// the `QuestionAnswered` event back to the asker so the harness
/// loop can react (mirrors the operator-cancel dashboard path).
pub fn handle_cancel_loose_end(
coord: &Arc<Coordinator>,
canceller: &str,
privileged: bool,
kind: hive_sh4re::CancelLooseEndKind,
id: i64,
) -> Result<(), String> {
@ -157,7 +160,7 @@ pub fn handle_cancel_loose_end(
hive_sh4re::CancelLooseEndKind::Question => {
let (question, asker, target) = coord
.questions
.cancel(id, canceller)
.cancel(id, canceller, privileged)
.map_err(|e| format!("{e:#}"))?;
let sentinel = format!("[cancelled by {canceller}]");
tracing::info!(%id, %canceller, %asker, "question cancelled");
@ -183,17 +186,17 @@ pub fn handle_cancel_loose_end(
hive_sh4re::CancelLooseEndKind::Reminder => {
let owner = coord
.broker
.cancel_reminder_as(id, canceller)
.cancel_reminder_as(id, canceller, privileged)
.map_err(|e| format!("{e:#}"))?;
tracing::info!(%id, %canceller, %owner, "reminder cancelled");
coord.emit_reminders_snapshot();
Ok(())
}
hive_sh4re::CancelLooseEndKind::Approval => {
// Manager-only: only the agent that can submit approvals
// is allowed to withdraw them. Sub-agents would have no
// pending approvals of their own to cancel anyway.
check_approval_canceller_is_manager(canceller)?;
// Privileged-only: only a caller on the manager socket (which
// is the sole approval submitter) may withdraw approvals.
// Sub-agents have no pending approvals of their own anyway.
check_can_cancel_approval(privileged)?;
let approval = coord
.approvals
.mark_cancelled(id, canceller)
@ -217,15 +220,19 @@ pub fn handle_cancel_loose_end(
}
}
/// Manager-only guard on the `Approval` cancel arm. Pulled out so
/// Privileged-only guard on the `Approval` cancel arm. Pulled out so
/// the auth check has its own focused unit test — testing the full
/// `handle_cancel_loose_end` flow would need a `Coordinator` fixture
/// (broker + sqlite + in-memory questions), which we don't have
/// today. The check is a single string compare, so a function-level
/// test gives the same coverage with no harness.
fn check_approval_canceller_is_manager(canceller: &str) -> Result<(), String> {
if canceller != hive_sh4re::MANAGER_AGENT {
return Err("cancel_loose_end: only the manager can cancel approval rows".to_owned());
/// today. Privilege is a property of the socket the request arrived
/// on (the manager socket), threaded in as `privileged` — not a match
/// against a hardcoded agent name.
fn check_can_cancel_approval(privileged: bool) -> Result<(), String> {
if !privileged {
return Err(
"cancel_loose_end: only a privileged (manager-socket) caller can cancel approval rows"
.to_owned(),
);
}
Ok(())
}
@ -235,25 +242,18 @@ mod tests {
use super::*;
#[test]
fn approval_cancel_rejects_sub_agent_callers() {
// Sub-agents must not be able to cancel approval rows even
// if they invent an id. The guard is server-side so client
// cooperation is irrelevant.
let err = check_approval_canceller_is_manager("bitburner").unwrap_err();
assert!(err.contains("only the manager"), "{err}");
// Bonus: empty / operator strings also rejected (only the
// exact MANAGER_AGENT constant passes).
assert!(check_approval_canceller_is_manager("").is_err());
assert!(
check_approval_canceller_is_manager(hive_sh4re::OPERATOR_RECIPIENT).is_err(),
"operator surface uses the dashboard cancel path, not this dispatcher",
);
fn approval_cancel_rejects_unprivileged_callers() {
// A non-privileged caller (any regular agent socket) must not be
// able to cancel approval rows even if it invents an id. The guard
// is server-side so client cooperation is irrelevant — and it keys
// on the socket-derived `privileged` flag, not on any agent name.
let err = check_can_cancel_approval(false).unwrap_err();
assert!(err.contains("only a privileged"), "{err}");
}
#[test]
fn approval_cancel_allows_manager() {
check_approval_canceller_is_manager(hive_sh4re::MANAGER_AGENT)
.expect("MANAGER_AGENT must pass the guard");
fn approval_cancel_allows_privileged() {
check_can_cancel_approval(true).expect("a privileged caller must pass the guard");
}
}