Skip to main content
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: 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'.
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.

Try it

Enable cockpit controls for one project via .rstack/policy.json:
Or turn it on globally for the running hub process:
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.