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

@ -75,8 +75,14 @@ async fn serve(stream: UnixStream, coord: Arc<Coordinator>) -> Result<()> {
}
async fn dispatch(req: &ManagerRequest, coord: &Arc<Coordinator>) -> ManagerResponse {
// Delegate all variants shared with the agent socket to the common handler.
if let Some(resp) = crate::agent_server::dispatch_shared(req, MANAGER_AGENT, coord).await {
// Delegate all variants shared with the agent socket to the common
// handler. `privileged = true`: every request here arrived on the
// manager socket, which is the trust boundary — so the shared guards
// grant manager-level authority from the socket, not from matching the
// `MANAGER_AGENT` name. `MANAGER_AGENT` is still passed as the actor
// name for attribution/routing (notifications, ownership), not authz.
if let Some(resp) = crate::agent_server::dispatch_shared(req, MANAGER_AGENT, true, coord).await
{
return resp;
}
match req {