Unattended Approvals

A risk-tiered step waits for a human. But when a run is unattended — a daemon tick, a schedule, a fleet loop — nobody is there to answer inside the next few minutes. MUR parks the request instead of burning a timeout against it.

mur channel approve fleet-builder hitl-01J...       # approve, whenever you get to it
mur channel approve fleet-builder hitl-01J... --deny
mur fleet run builder                               # re-run: it continues from there

What happens when nobody answers

The gate writes a durable request into the run's signed channel and returns immediately:

  • the step is blocked — not failed, so nothing is reported as broken and no on_failure handling fires;
  • steps that do not depend on it still run to completion, so the run gets as far as it legitimately can;
  • the channel stays input-required, so every surface keeps showing that a human still owes it an answer;
  • a --loop run stops rather than paying for another iteration that would arrive at the same unanswered question.

mur job status and mur fleet status report the run as blocked. That is deliberately not a terminal state: an approval resumes it.

Approving later

Approve from Hub's Needs You card or with mur channel approve <channel> <hitl_id>. Then re-run — the next fleet run, loop iteration, or schedule tick picks up where the last one stopped, and steps that already completed are skipped.

This works because an approval is matched on the action's content hash, not on the request id. The id is minted fresh every call, so an id-keyed lookup could never connect your answer to the next run.

Two consequences fall out of that, both intended:

  • Change the action and the approval no longer covers it. Different bytes, different hash — nothing you did not see gets executed on the strength of something you did.
  • A denial sticks. It settles the action for the same window rather than being re-asked every iteration.

Approvals and denials count for 7 days. The hash bounds what was approved; the window bounds how long ago.

Declaring a policy in fleet.yaml

hitl:
  mode: defer                        # defer | wait | deny
  auto_approve_tiers: [read, write]  # standing responsibility, capped at write

mode — what an unanswered gate does. Absent, MUR decides by whether a terminal is attached. State it explicitly when that is a poor proxy for somebody is watching:

modeUse it when
defernobody is watching in the next few minutes (the unattended default)
waitthe run is headless but a person is on Hub — poll for the answer
denythis fleet must never reach for a human; fail immediately and legibly

deny is a floor, not a preference. It refuses before consulting anything, so neither an older approval for the same action nor a stray --yes can lift it.

auto_approve_tiers — risk tiers whose actions this fleet's owner has pre-approved, so they run without asking.

It is an explicit list rather than a ceiling, and it is capped at write. spend, destructive, privileged and network-egress cannot be granted here at any tier: their cost is not something noticing afterwards can undo. mur fleet run refuses a fleet that lists one, rather than ignoring it quietly — a config line you believed took effect is worse than one that errors.

Three things stay true whatever you grant:

  • Your explicit no to an action outranks a standing grant for its tier. A decision someone actually made beats a blanket one made in advance.
  • Auto-approvals are still audited. The gate writes the request and its response to the channel with surface: policy, naming the tier it came from — so what did this run do without asking me? always has an answer.
  • There is no blanket auto-approve. --yes exists for interactive runs and is not reachable from an unattended fleet path.

See also

  • Fleet Loops — what ends a loop, and the guards around an unattended one
  • Agent CLI — approving a tool call mid-conversation, which is a different gate