---
title: "Cockpit Controls"
description: "Authenticated, audited actions that let an operator drive a governed run — resume it or restore a checkpoint — from the Business Hub."
---

Cockpit controls are the only state-changing actions the Business Hub exposes
beyond approving/rejecting gates and writing a `.env` key: **resume a run** and
**restore a stage checkpoint**, both invoked from the browser. They are **off
by default** and, like every other mutation path in the hub, they never trust
the client — the server re-derives eligibility from ground truth before doing
any work, and every request is recorded to an append-only ledger that is both
the idempotency store and the audit trail. The full threat model lives in
`docs/security/cockpit-controls-threat-model.md`; this page covers what the
feature does and how to turn it on.

## How it works

### Off by default, enabled two ways

`cockpitControlsEnabled(policy, env)` in
`src/core/harness/cockpit-actions.js` checks, in order:

1. the env flag `RSTACK_COCKPIT_CONTROLS` (`1`/`true`/`yes`/`on`) — a global
   kill switch, and
2. `policy.cockpit_controls.enabled === true` in `.rstack/policy.json` — a
   per-project opt-in.

With neither set, the server-owned `cockpit` projection
(`src/observability/dashboard/state/cockpit.js`) returns
`{ enabled: false, runs: [] }` — there is nothing for the client to render —
and `POST /api/action` returns **403** before touching anything.

### Two actions, two risk levels

`COCKPIT_ACTION_TYPES` defines exactly two actions:

| Action | Risk | Requires approval | What it does |
|---|---|---|---|
| `resume-run` | `low` | No | Advances the run up to `RESUME_MAX_STEPS` (5) model-free steps via the same planner as `pipeline run`, stopping at every human gate. Non-destructive — it can never cross an approval or a guardrail override. |
| `restore-checkpoint` | `high` | Yes | Overwrites a stage's current artifacts with its last verified checkpoint. Destructive — gated on a manager approval before it runs. |

The projection (`buildCockpitProjection`) builds one `resume-run` action per
run and one `restore-checkpoint` action per checkpointed stage
(`rollup.checkpoints.stages`), each carrying:

- `enabled` / `disabledReason` — from `evaluateResumeEligibility(rollup)` or
  `evaluateCheckpointEligibility(stage, { stale })`, derived from the **same**
  compact pipeline rollup the CLI's `pipeline status` reads, so the UI can't
  offer something execution wouldn't do. Resume is only offered when
  `next_action.kind` is `active`, `pending`, `retry`, or `failed`; a pending
  approval, an exhausted retry budget, or a stale snapshot disables it with an
  explicit reason. Checkpoint restore is only offered when
  `stage.restorable === true` — a corrupt manifest, hash mismatch, missing/extra
  files, or a pre-manifest legacy checkpoint is surfaced as **disabled with the
  reason**, never silently hidden.
- `idempotencyRequired: true` on both actions.
- a `confirm` block (`title`/`consequence`/`target`) the UI renders in a
  confirmation modal before submitting.

### The route re-verifies from ground truth

`handleCockpitAction` in `src/observability/dashboard/server.js` (behind
`POST /api/action`) never trusts the projection it just sent the client. Per
request it:

1. Runs the shared guarded-POST chain (token auth, CSRF-origin check, rate
   limit, body-size cap — the same chain `/api/env-write` uses).
2. Validates `action` against `isKnownCockpitAction`, requires an operator
   identity (`resolvedBy`), and validates `idempotencyKey` against
   `isValidIdempotencyKey` (8–128 chars of `[A-Za-z0-9._:-]`, no `..`).
3. Resolves the target run across known project roots and re-checks
   `cockpitControlsEnabled` against **that root's own policy** — a client
   can't ride one project's opt-in into another's runs.
4. For `restore-checkpoint`, requires `stageId` and checks it against
   `isCanonicalStageId` — the client-sent stage id is never trusted as-is.
5. Claims the idempotency key (see below), then executes:
   - `executeResumeRun` calls the real planner (`runPipeline`, capped at
     `RESUME_MAX_STEPS`) and returns `409 not_eligible` if nothing advanceable
     was found (`complete`, `no_actionable_work`, `pending_approval`,
     `blocked_retry_policy`, `ask_user`, `dry_run`).
   - `executeRestoreCheckpoint` deep-verifies the checkpoint
     (`verifyStageCheckpoint(runDir, stageId, { deep: true })`) **before**
     touching any approval, refusing with `409 not_eligible` if it isn't
     restorable. If restorable, it looks for a consumed one-shot approval on
     `checkpointRestoreArtifact(runId, stageId)`; if none exists it enqueues a
     pending `checkpoint_restore` approval and returns `409 approval_required`
     — the operator resolves it on the Approvals page, then resubmits the same
     request. Only once approved does `rollbackToCheckpoint` run.
6. Writes a `cockpit_resume_run` or `cockpit_checkpoint_restored` run event on
   success and broadcasts a fresh snapshot to connected clients — **no
   optimistic success**: the response body is the real outcome, and the UI
   reconciles from the next real state.

### The ledger: idempotency store *and* audit trail

Every action carries a client-supplied idempotency key. `claimIdempotencyKey`
appends a `started` line to the append-only
`.rstack/cockpit-actions.jsonl` ledger under the harness file lock
(`withFileLock`), then `completeLedgerEntry` appends the terminal
`completed`/`failed` line. `summarizeLedgerForKey` reads the ledger in
**append order, not by timestamp** — the last entry for a key is authoritative,
which avoids a same-millisecond tie misreading a finished action as still
in-flight. Re-submitting a **completed** key replays the stored `result`
without re-executing; re-submitting a key that's still `started` returns
**409 `in_progress`**. Because the ledger is never rewritten or deleted, it
doubles as the immutable audit trail for every cockpit action ever taken,
including denials (auth failures, unknown actions, disabled-feature attempts)
recorded with `phase: 'denied'`.

<Warning>
  `restore-checkpoint` is destructive: it overwrites the stage's current
  artifacts with the last verified checkpoint. It always requires a manager
  approval — there is no path that skips it, even with cockpit controls
  enabled.
</Warning>

## Try it

Enable cockpit controls for one project via `.rstack/policy.json`:

```json
{
  "cockpit_controls": {
    "enabled": true
  }
}
```

Or turn it on globally for the running hub process:

```bash
RSTACK_COCKPIT_CONTROLS=1 rstack-business --port 3008 --project .
```

With it enabled, open a run in the Business Hub's Run Workspace. Runs with
advanceable work show a **Resume run** control; runs with a restorable
checkpoint show **Restore checkpoint** per stage. Each opens a confirmation
modal naming the exact run/stage and the real consequence before submitting.
A restore that has no standing approval yet will come back with
`approval_required` — approve the `checkpoint-restore:<runId>:<stageId>`
request on the Approvals page, then retry the same action.

## Related

- [Business Hub overview & navigation](/business-hub/overview-and-navigation) — where cockpit controls appear in the Run Workspace
- [Approvals & governance](/business-hub/approvals-and-governance) — the approval queue `restore-checkpoint` requests land on
- [Governance model](/getting-started/governance-model) — the broader approval/guardrail model cockpit actions plug into
- [Business Flex profiles](/getting-started/business-flex-profiles) — `.rstack/policy.json` conventions
