InitRunner

Human-in-the-Loop Approvals

Since v2026.4.17, any tool configured with approval: required pauses the run whenever the model wants to call it. The pending call surfaces as a structured "paused" state; a human approves or denies out of band, and the run resumes from exactly where it stopped — no re-prompting, no lost message history.

Under the hood this is PydanticAI's native DeferredToolRequests / DeferredToolResults contract — the same surface AG-UI and the Vercel AI SDK speak. Every runner mode (single-shot, REPL, daemon, API, dashboard) handles it.

When to use it

Reach for approvals when the argument pattern can't be decided in advance:

  • Shell commands whose safety depends on the target path.
  • Writes to a production store where the diff matters.
  • Money-moving API calls.
  • Anything you'd want a human to glance at before it goes through.

If the answer is always the same regardless of arguments, tool permissions are a better fit — they evaluate before approval and short-circuit denials without bothering a reviewer.

Configuration

Add approval: required to any tool entry:

spec:
  tools:
    - type: shell
      working_dir: .
      approval: required

approval accepts auto (default, no gating) and required. It composes with permissions: — deny rules short-circuit first, so a reviewer is never asked to approve a call that would have been blocked anyway.

The wrapper order is builder → PolicyToolset (Cedar/InitGuard) → PermissionToolset (fnmatch) → ApprovalToolset. A call rejected earlier in that chain never reaches a human.

How a paused run looks

REPL

Approvals prompt inline and the run resumes in place:

> delete /tmp/scratch

Run abc123 paused — 1 tool call(s) need approval.

  shell  call_01HW9Q
  {'command': 'rm -rf /tmp/scratch'}
  Approve? [y/N]: y

Agent: Deleted /tmp/scratch.

Single-shot

Prints pending calls, exits with code 2, and persists state to the audit SQLite:

$ initrunner run demo.yaml -p "delete /tmp/scratch"

Run abc123 paused — 1 tool call awaiting approval.
  call_01HW9Q  shell  {'command': 'rm -rf /tmp/scratch'}

Resume with: initrunner approve abc123 --all

$ initrunner approve abc123 --all
Resumed.
Deleted /tmp/scratch.

Daemon and conversational triggers

When a cron or webhook-fired run pauses, the daemon persists state and keeps serving other triggers. Slack, Discord, and Telegram triggers send a one-liner reply:

Awaiting approval for 1 tool call(s). Resume: initrunner approve abc123 --all

The --no-audit flag disables persistence; in that mode a paused daemon run reports that it cannot be resumed rather than silently losing state.

API

POST /v1/chat/completions returns HTTP 200 with an extended body when the model pauses:

{
  "id": "chatcmpl-...",
  "choices": [{
    "index": 0,
    "message": {"role": "assistant", "content": ""},
    "finish_reason": "tool_calls_pending_approval"
  }],
  "run_id": "abc123",
  "pending_approvals": [
    {"tool_call_id": "call_01HW9Q", "tool_name": "shell",
     "arguments": {"command": "rm -rf /tmp/scratch"}}
  ]
}

Streaming requests get a final SSE event before [DONE]:

data: {"event":"approval_required","run_id":"abc123","pending_approvals":[...]}
data: {"id":"chatcmpl-...","choices":[{"delta":{},"finish_reason":"tool_calls_pending_approval"}]}
data: [DONE]

Resume with a map of {tool_call_id: bool}:

curl -X POST http://localhost:8000/v1/approvals/abc123 \
  -H 'content-type: application/json' \
  -d '{"call_01HW9Q": true}'

Every pending tool_call_id on that run must carry a decision — false denies. Optional X-Resolved-By header records the operator in the audit trail. The response mirrors a regular chat completion, or the paused shape again if the model re-pauses.

Dashboard

The dashboard has two approval surfaces, both driven by the same /api/approvals/* router:

  • Inline in RunPanel — when a run started from the agent detail page pauses, an Approve/Deny card group replaces the "thinking" state. Each card shows a tool-templated argument preview (e.g. rm -rf /tmp/cache rather than raw JSON) and a left state bar (muted = unset, lime = approved, red = denied). Submit fires once every card has a decision.
  • Queue view (/approvals) — reviewers see every paused run across the daemon, API, and other sessions, grouped by run_id. Single-call runs have inline controls; multi-call runs open a right-side drawer. A sidebar badge under Operate shows the pending count (tabular-nums, polled every 20s and bumped immediately by the approval_required SSE event). Press ? anywhere for the keyboard grammar (j/k navigate, A/D decide, ⇧ A/⇧ D bulk, submit, Esc close).

See Dashboard: Approvals queue for screenshots and keyboard details.

CLI

initrunner pending

Lists unresolved tool-call approvals across all runs in the audit database.

$ initrunner pending
Pending approvals (1)
┏━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┓
┃ run_id     ┃ tool_call… ┃ tool  ┃ agent ┃ created_at                 ┃ arguments           ┃
┡━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━┩
│ abc123     │ call_01HW… │ shell │ demo  │ 2026-04-24T14:21:08.947991 │ {"command":"rm -rf… │
└────────────┴────────────┴───────┴───────┴────────────────────────────┴─────────────────────┘

initrunner approve

Resumes a run by approving or denying its pending calls.

FlagDescription
RUN_IDThe paused run identifier (shown in the pending table and in the CLI resume hint).
--allApprove every pending tool call for the run.
--tool-call-id IDDecide only the named call; any other pending calls for the same run default to denied.
--denyCombine with --all or --tool-call-id to deny instead of approve.
initrunner approve abc123 --all
initrunner approve abc123 --tool-call-id call_01HW9Q
initrunner approve abc123 --all --deny

Audit trail

Resumed runs log with trigger_type="resume" and a synthetic prompt of the form (resume: call_id:approve, call_id:deny, ...) so the audit row is self-describing. The pending_approvals table retains resolved rows with resolved_at, resolved_by, and decision ∈ {approve, deny}, so the approval history survives pruning of the runs themselves.

Limitations

The following are not yet supported:

  • Per-role or per-skill approval defaults — today, approval is declared per tool entry.
  • Expiry sweeper for pending approvals older than N hours.
  • Attribution in "already resolved" toasts on the dashboard race path. The resolver's id is in the audit trail but isn't surfaced inline on the losing client.

See also: Security, Tools: Permissions, Dashboard.

On this page