Motebit

Daemon Mode & Approvals

Running motebit as a daemon — scheduled goals, approval queue, and governance enforcement.

The daemon is a long-running process that executes scheduled goals on behalf of your motebit. It reads the signed motebit.md identity file, derives three-band governance thresholds, and runs a tick-based scheduler that fires goals, gates tool calls through the approval queue, and enforces policy at every step.

Starting the daemon

motebit run

Or with an explicit identity file path:

motebit run --identity ./path/to/motebit.md

On startup, the daemon:

  1. Reads and verifies the motebit.md Ed25519 signature (fails if invalid)
  2. Checks that all three governance thresholds are present — max_risk_auto, require_approval_above, deny_above (fails if any are missing)
  3. Derives a GovernancePolicyConfig from the governance block
  4. Starts the GoalScheduler with a 60-second tick interval
  5. Prints a summary:
Daemon running. motebit_id: a1b2c3d4... Goals: 3. Policy: max_risk_auto=R1_DRAFT, deny_above=R4_MONEY

Stop the daemon with SIGINT (Ctrl-C) or SIGTERM. Pending approvals stay pending across a stop (see "Shutdown and restart" below); a run that was mid-turn at a graceful stop is closed as failed and re-fires on schedule.

Managing goals

Goals are scheduled prompts that the daemon executes at a fixed interval.

Add a goal

motebit goal add "Summarize unread emails" --every 1h

The --every flag is required. Valid intervals use a number followed by a unit: m (minutes), h (hours), d (days). Examples: 5m, 30m, 1h, 6h, 1d.

List goals

motebit goal list
  ID        Prompt                                     Interval    Last Run            Enabled
  a1b2c3d4  Summarize unread emails                    1h          2025-01-15T10:00Z   true
  e5f6a7b8  Check deployment status                    30m         2025-01-15T10:30Z   true

Remove, pause, resume

motebit goal remove a1b2    # prefix match on goal ID
motebit goal pause e5f6     # disable without deleting
motebit goal resume e5f6    # re-enable

All goal commands support prefix matching — you only need enough characters to uniquely identify the goal.

How approvals work

Governance in motebit.md defines three risk thresholds that partition all tool calls into three bands:

BandConditionBehavior
Auto-allowrisk_level <= max_risk_autoTool executes immediately
Approval requiredrisk_level > require_approval_above and risk_level <= deny_aboveDaemon suspends the turn and creates a pending approval
Hard denyrisk_level > deny_aboveTool is blocked by PolicyGate.validate() even if visible to the model

When a tool lands in the approval band, the daemon:

  1. Persists a pending approval to approval_queue in SQLite
  2. Suspends the current goal's turn
  3. Stops scheduling any other goals (depth-1 constraint — only one approval can be pending at a time)
  4. Waits for an operator to approve or deny via the CLI

The default governance thresholds (set by motebit export) are:

FieldDefault
max_risk_autoR1_DRAFT
require_approval_aboveR1_DRAFT
deny_aboveR4_MONEY

Risk levels, from lowest to highest: R0_READ, R1_DRAFT, R2_WRITE, R3_EXECUTE, R4_MONEY.

Managing approvals

List all approvals

motebit approvals list

Shows all approvals (pending, approved, denied, expired), most recent first.

Show full detail

motebit approvals show a1b2

Displays the tool name, argument preview (first 500 chars of serialized args), risk level, timestamps, and status. Supports prefix matching on the approval ID.

Approve a pending tool call

motebit approvals approve a1b2

Sets the approval status to approved. The daemon picks it up on its next tick (within 60 seconds) and resumes the suspended turn.

Deny a pending tool call

motebit approvals deny a1b2 --reason "Too risky for production"

Sets the status to denied with an optional reason for governance telemetry. If --reason is omitted, the denial is recorded without a reason.

Attempting to approve or deny a non-pending approval fails:

Error: approval a1b2c3d4 is already denied.

Expiry and restart behavior

Expiry: Pending approvals expire after 1 hour (3,600,000 ms). On each tick, the scheduler runs expireStale() before processing any goals. Expired approvals remain visible in approvals list with status expired.

Shutdown and restart: A pending approval is a human's decision to make, and it survives the daemon. When the daemon stops, pending approvals stay pending and their goal runs stay awaiting_approval in the run ledger (goal_runs); the goal does not fire again while that run is unresolved. When the daemon comes back and you approve, the paused turn itself is gone with the old process, so the daemon executes exactly the one call you approved (full arguments are persisted alongside the preview, and their hash is re-checked) through the same policy gate, records the outcome, and closes the run. It does not re-run the rest of the goal's turn — the tool calls before the pause already happened. Denying closes the run without executing anything. A money action (R4) is never executed from a recovered run.

Interrupted runs: If the daemon dies mid-run, the next start marks that run interrupted and reads the tool audit log for it: how many allowed tool calls recorded a completion, and how many have a decision row but no completion row (the process died between dispatch and recording — their external effect is unknown). If either count is non-zero the goal is held: it will not fire again until you run motebit runs ack <run_id>, because re-firing would repeat the completed calls and nobody can say what the unknown ones did. A run with no allowed tool calls at all resolves itself. motebit ps shows held goals; motebit runs lists the runs holding them. motebit runs ack <run_id> --allow-fresh-run retries nothing — it records that you looked and accepts that the goal's next run starts from scratch, which may repeat effects the interrupted run already caused; without the flag the command prints that consequence and releases nothing. A decision applied after a restart closes its run as partial, never completed: the one approved action ran (or was refused), and the goal's remaining work was not resumed.

What a restart cannot do: it cannot resume the paused turn itself (its stream died with the old process), which is why a post-restart decision applies to the one approved call only and the run closes as partial. The 1-hour TTL bounds the decision, not the sweep: an approval granted after its expiry — for example while the daemon was down — is refused by motebit approvals approve and, if it slipped through, is never executed by the daemon.

Stopping it

A halt withdraws the standing permission to act unattended. It is not the opposite of an approval — an approval authorizes one exact pending call, a halt revokes the permission to act at all, and a halt in force outranks an approval granted before it.

motebit halt --reason "going out"        # stop all unattended execution
motebit halt goal a1b2c3d4               # stop one goal, leave the rest
motebit halt-status                      # what was asked, and which processes acknowledged
motebit resume a1b2c3d4                  # give the permission back
motebit resume all

A halt stops new goal runs, recovered approvals, and idle consolidation, and signals an abort of the run in flight — the daemon checks for halts on its own interval, outside the guard that a long-running goal holds, so a stop reaches work in progress rather than queueing behind it. The signal is observed between steps, so a tool call already dispatched runs to its end; the acknowledgement says exactly that rather than claiming the run was cut off. A run stopped this way is not counted against the goal's retry budget — a stop you asked for is not a failure of the goal. A live conversation you are having at the keyboard is not unattended work and is not what a halt is for.

A goal-scoped halt is genuinely scoped: it stops that goal's next run and that goal's pending approval, and leaves every other goal alone. The id is resolved before anything is recorded — the 8-character prefix motebit goal list prints is enough — and a scope naming a goal that does not exist is refused at the store, not merely at the command that happened to be written first. A halt that matched no goal would report a stop while the goal kept running, so that state cannot be recorded at all.

Where a halt is enforced. Two chokepoints, not a list of call sites: every dispatched task passes through the runtime's task entry, and every idle cycle through the consolidation entry, so a path added later inherits the refusal instead of having to remember it. On top of those the scheduler will not fire a goal, execute a recovered approval, or resume a turn whose approval expired — that resume is a model turn, and under a halt it is unattended work starting.

motebit serve is covered too: a halted worker does not claim relay-dispatched tasks. One limitation is worth stating plainly — the relay's task protocol has no decline verb, so an unclaimed task is re-dispatched and refused again until it times out, and the submitter learns only from that timeout. Answering them properly needs a decline verb, which belongs to the task protocol rather than to halt.

Several processes, several acknowledgements. More than one process can run unattended work for one motebit — motebit run and motebit serve on the same machine, against the same database — and each stops different work. So each acknowledges for itself, and halt-status lists them by name. One process acknowledging never meant that everything had stopped; treating it as if it did let the other keep working under a surface that said "Stopped". So no surface here says a bare "Stopped" — motebit halt names which processes acknowledged and what each of them stopped, and a remote halt answers for the runtime that received it while listing the rest.

Two MACHINES is refused, not guessed between. Everything above is about one machine's processes, which share a database — so whichever receives a remote halt, the record they all read is the same one. Two machines are not that: the halt store is local and nothing replicates it, so delivering to one would stop that machine and answer as though the motebit had stopped, while the other kept working. A remote halt on a motebit with unattended runtimes on two different machines is refused, and says so. Run it on each machine, or stop the runtime you do not want. Reaching every machine at once is the right answer and is tracked in issue #681; it is not built, and a refusal you can act on beats a "Stopped" that is not true.

Acknowledged is not stopped either. A process acknowledging says it answered for itself, not that it had work to stop: the worker answers a goal-scoped halt with "nothing here runs under that goal — dispatched tasks continue", which is true, useful, and not the goal having stopped. So halt-status counts processes that ACKNOWLEDGED and prints what each one stopped underneath, rather than folding both facts into one word.

Requested is not stopped, and neither is one acknowledgement. The request carries only the ask: who asked, what for, and when. Whether anything stopped is a separate record, one row per process, each naming the process and what stopping entailed there (aborted run 1a2b3c4d; no further goal runs will start). It is kept apart because a motebit's unattended work can run in several processes at once, so "has it stopped" is never one fact. A daemon that is offline, wedged, or mid-tool has been asked and has not stopped. motebit halt waits a few seconds, then names the processes that answered, and no surface reports a bare "Stopped" on the strength of one of them.

The halt is durable and in force from the instant it is written — before anyone acknowledges it, and across a restart. A stop a restart forgets is not a stop.

From somewhere else

--remote signs the command with the motebit's identity key and sends it to the running runtime over the relay (signed-request-envelope@1.0, audience agent-command/{motebit_id}). The relay verifies at ingress as defence in depth, forwards the envelope verbatim, and the runtime re-verifies fail-closed before acting.

motebit halt --remote --reason "from the train"
motebit halt-status --remote
motebit resume all --remote

Because that request reaches the process that is doing the work, the response is the acknowledgement: it says what was stopped, not that a message was delivered. If no unattended runtime is connected the command fails, and the honest reading of that failure is "not delivered" — never "stopped". A halt written locally is in force on that machine regardless.

Coming back to it

The phone can already stop the motebit and decide an approval. /runs lets it see what either was about:

/runs                what happened while you were away
/runs abc12345       one run in full

The run ledger, the outcomes and the evidence live where goals actually fire, and no other surface holds them — so this is a question sent to the runtime that has the answer, exactly as a remote halt is. A surface that cannot see the ledger says so, which is not the same sentence as "nothing happened".

Two things it deliberately does not do. The verbatim result does not travel. A signed artifact is read on the machine that signed it, because a copy arriving over a relay cannot be checked against that signature by whatever surface receives it — presenting it as the result would offer proof that is not there. A bounded preview travels instead and says which it is. And every piece of text that does cross is masked at the boundary rather than trusted from the reader — by a wider membrane than an approval's arguments get, because the two are read for different things. An approval's arguments are read to decide, so masking the destination or the amount would leave nothing to consent to; a run's result is read as a report, where losing a field costs legibility and keeping one can cost a card number. An evidence source is a third case again — a URL you are told to re-fetch, so its path is left legible and its query, where an account number would sit, is not. Asked from the terminal that holds the ledger, nothing is masked at all: no wire is being crossed.

What comes back is the same shape the terminal shows: the run's status, what it produced and whether that is signed, what its tools reported, the evidence pointers with their sources and full digests, and anything read and deliberately not kept.

Every surface of a motebit holds an open socket, so the relay routes these verbs specifically to a peer that runs unattended work. Without that, a halt sent from the phone could be answered by the phone — "this surface cannot be halted" — while the daemon kept running, which reads exactly like a refusal.

The phone speaks the same verbs as chat commands in the mobile app — halt, resume and halted, plus pending, approve and deny for the approval queue. (Those are mobile-app commands, not motebit CLI subcommands.) The phone can sign them because after a key-transfer pairing it holds the motebit's identity key; a device paired without key transfer will be told so rather than silently refused.

Deciding an approval from the phone does not execute anything there. It writes the verdict into the same queue a terminal decision writes to; the daemon picks it up on its next tick, through the same policy gate, and a money action is still never executed from a recovered run.

What the phone is shown is redacted the way any egress to a non-sovereign party is: the destination, the path and the amount are visible so the decision is real, while credential-class values are masked, and the full arguments never leave the machine. The preview is accompanied by a hash over the whole argument set. Note precisely what that hash buys: the phone holds neither the full arguments nor the un-redacted preview, so it cannot recompute anything. The hash lets a later check on the machine that holds the arguments detect a preview that diverged from what would execute; the phone carries it forward rather than verifying it.

Governance enforcement

The daemon requires all three governance fields in motebit.md. If any field is missing, the daemon exits with:

Error: motebit.md governance.max_risk_auto is missing or empty.
All three governance thresholds are required for daemon mode.

At runtime, governance is enforced at two layers:

  • PolicyGate.filterTools() controls which tools the AI model can see. Tools above the deny threshold may still be visible but are blocked at execution time.
  • PolicyGate.validate() controls execution. Approval-band tools are gated here — the model can reason about them, but cannot execute without operator approval. Hard-denied tools (above deny_above) are blocked unconditionally.

This is fail-closed: if governance cannot be parsed or a risk level is unknown, the daemon refuses to start.

On this page