RIG Runtime Hard-Cut Cutover Runbook
Status (2026-06-04): Cutover status is tracked in PR #1559 (dev→staging→main promotion flow) and ADR-028 (retire-agent-runtime-per-org-railway, impl PR #1584 merged). The legacy
/v1/tasksauthority returns HTTP 410 Gone (agent/routers/tasks.py); shared-Railway +ecs_per_orgare the only supportedAgentRuntimeKinds. Reuse this runbook only as a rehearsal template or post-mortem reference — do not re-enable legacy authority paths.
This runbook cuts production/staging to the Nest runtime control plane as the single authority for runs, events, approvals/input, tools, artifacts, model calls, runner claims, sandbox capability ops, and unresolved ingress.
Do not use this to re-enable legacy authority paths. If a gate fails, contain or forward-fix through runtime rows/events; do not fall back to /tools/execute, legacy Python task authority, org-less execution, raw shell, broad runner secrets, or persisted signed artifact URLs.
Roles
- Cutover lead: owns freeze/reopen calls and go/no-go.
- API operator: runs migrations, backfill, smoke checks, and DB evidence capture.
- Runner operator: verifies manifests, registration, heartbeats, and runner bootstrap gate.
- Frontend/Ops checker: verifies Ops/Expert/client surfaces are reading runtime DTOs.
- Incident lead: owns containment, rollback criteria, and forward-fix execution.
Required evidence
Record these in the release ticket before reopening traffic:
- Git SHA and deployed API/agent/frontend versions.
- Migration job output with
SYNCHRONIZE=false. - Freeze start time and freeze high-water mark.
- Backfill dry-run output before apply.
- Backfill apply output, if this is the real cutover and not rehearsal.
- Smoke script JSON output.
- Runner manifest IDs, ready runner registration IDs, and heartbeat timestamps.
- Screenshots or logs for
/ops/agent-runs,/ops/approvals, Expert approval surface, and client approval/input cards. - Quarantine sample for at least one unresolved/unknown ingress path in staging rehearsal.
Pre-freeze prerequisites
All are blockers:
- API build passes.
- Postgres migrations pass against staging/production-like Postgres with
SYNCHRONIZE=false. - Legacy authority routes are disabled or return
410 Gonein production mode. - Runtime action paths append
run_eventsand write scopedagent_runs. - RLS/session context is fail-closed unless
app.current_role_domain = 'internal'is explicitly set by a worker/backfill/smoke transaction. - Runner event signatures are verified before runner-originated events persist.
RUNNER_JWT_SECRETis set per environment, separate from userJWT_SECRET; runner tokens must carryaud=runner.- Runner/sandbox env does not include broad provider/vendor/R2/platform secrets.
- Artifact code issues short-lived leases only at response time; no signed URLs are persisted.
- Unknown/mismatched channel ingress is quarantined in
unresolved_channel_eventsand does not execute runtime work. - Ops/Expert/client UI can read runtime runs and approval/input requests.
Freeze and high-water marks
- Announce the freeze window.
- Disable client-visible auto-send and any non-runtime mutation entry points at the edge/feature-flag layer.
- Put runners into drain mode or stop new runner claims. Existing in-flight work may finish only if it is already runtime-backed.
- Stop or pause webhook workers that can create conversations/tasks/queue items outside the runtime path. Webhooks may still ACK and quarantine unknown ingress.
- Capture the high-water mark from the DB:
SELECT now() AS freeze_high_water_created_at;
SELECT max(created_at) AS conversations_high_water FROM conversations;
SELECT max(created_at) AS expert_queue_high_water FROM expert_queue;
SELECT max(created_at) AS agentic_tasks_high_water FROM agentic_tasks;
SELECT max(created_at) AS agent_runs_high_water FROM agent_runs;
SELECT max(created_at) AS run_events_high_water FROM run_events;
Use the freeze_high_water_created_at ISO timestamp as --high-water-created-at for dry-run, apply, and smoke so the rehearsal/cutover has one consistent boundary.
Drain
- Wait for in-flight non-runtime handlers to finish or be explicitly cancelled.
- Confirm there are no legacy tool/task/model side-effect jobs still running.
- Confirm no runner claims are accepted while the backfill applies.
- Confirm no org-less ingress is queued for execution; unknown ingress must be in quarantine/remediation only.
Suggested checks:
SELECT status, count(*) FROM agent_runs GROUP BY status ORDER BY status;
SELECT status, count(*) FROM unresolved_channel_events GROUP BY status ORDER BY status;
SELECT status, count(*) FROM runner_registrations GROUP BY status ORDER BY status;
Backfill runtime rows
The backfill script is dry-run by default. It finds active source rows at or before the high-water mark that do not have runtime coverage:
- active
expert_queuerows -> pausedconversation_draftagent_runswithexpert_queue_item_id. - active
agentic_tasksrows -> pausedagentic_taskagent_runswithagentic_task_id. - active conversations without active queue/task coverage -> paused
conversation_draftagent_runs.
Inserted runs are deliberately paused with metadata.backfill.containment = 'paused_until_reopen' and a first run.backfilled ledger event. This avoids accidental runner pickup during the freeze.
Dry-run:
cd api
DATABASE_URL="$DATABASE_URL" \
npx ts-node scripts/backfill-runtime-runs.ts \
--dry-run \
--high-water-created-at="$FREEZE_HIGH_WATER_CREATED_AT"
Apply during the approved freeze window only:
cd api
DATABASE_URL="$DATABASE_URL" \
RIG_RUNTIME_BACKFILL_CONFIRM=apply-runtime-runs \
RIG_RUNTIME_POLICY_HASH="$CURRENT_POLICY_HASH" \
RIG_RUNTIME_TOOL_MANIFEST_HASH="$CURRENT_TOOL_MANIFEST_HASH" \
RIG_RUNTIME_PROMPT_BUNDLE_HASH="$CURRENT_PROMPT_BUNDLE_HASH" \
npx ts-node scripts/backfill-runtime-runs.ts \
--apply \
--high-water-created-at="$FREEZE_HIGH_WATER_CREATED_AT"
Then rerun dry-run. The total missing runtime rows must be 0 for the same high-water mark.
Runner bootstrap gate
Before reopening, all blockers must pass:
- At least one active, unexpired
runner_manifestsrow exists for the target environment/org set. - Required runner registrations are
readyand have heartbeat fresher than the cutover threshold (default 120 seconds). - No
readyrunner registration has a stale/missing heartbeat. - Runner manifest hash, policy hash, tool manifest hash, and event schema version match the deployed API expectations.
- Runner callback signature verification is active; unsigned runner events are rejected and not persisted.
- Runner JWTs are signed with
RUNNER_JWT_SECRET, includeaud=runner, and are not accepted by user JWT auth paths. - Runner env has only scoped runtime credentials; no broad provider/vendor/R2/platform secrets.
Run the smoke/go-no-go script:
cd api
DATABASE_URL="$DATABASE_URL" \
FRONTEND_BASE_URL="https://staging.h853.work" \
npx ts-node scripts/smoke-runtime-cutover.ts \
--required-ready-runners=1 \
--heartbeat-max-age-seconds=120 \
--high-water-created-at="$FREEZE_HIGH_WATER_CREATED_AT"
The script exits 0 only for GO. A NO_GO report is a blocker unless the incident lead explicitly documents a non-blocking false positive.
Sandbox supervisor production gate
Before reopening any hostile-code sandbox capability traffic, also run the sandbox worker/supervisor gate from docs/runbooks/sandbox-worker-supervisor.md.
Required blockers:
- ADR-022 has recorded AlexD / Paul / Eusden cosign.
api/scripts/smoke-sandbox-supervisor.tsemitsGOon an eligible native/systemd host or a dedicated K8s node with proven primitives.- Deterministic CI units are green, including supervisor Zig tests, capability-shell tests, worker tests, API evidence tests, schema drift checks, seccomp validation, and static bypass audit.
- API production evidence validator is enabled and rejects missing/fake
linux-bwrap-supervisorevidence. - Direct-spawn bypass test/audit is green.
- Hostile-code deny battery is green on the eligible host.
Missing any gate keeps production sandbox capability execution disabled. Rollback must never route to backend=none, sandbox=off, direct spawn, raw bwrap success, degraded success, or unsandboxed worker success.
Reopen
- Re-enable runtime-backed workers and webhook processing.
- Allow runner claims only after the runner bootstrap gate is green.
- Re-enable frontend/Ops traffic.
- Keep default HITL: no channel send unless policy and approval state explicitly allow it.
- Keep legacy authority paths disabled; do not reopen them for convenience.
- Watch the first 30 minutes:
SELECT status, count(*) FROM agent_runs GROUP BY status ORDER BY status;
SELECT type, count(*) FROM run_events WHERE created_at >= now() - interval '30 minutes' GROUP BY type ORDER BY count(*) DESC;
SELECT status, count(*) FROM approval_requests GROUP BY status ORDER BY status;
SELECT status, count(*) FROM unresolved_channel_events GROUP BY status ORDER BY status;
SELECT status, count(*) FROM runner_registrations GROUP BY status ORDER BY status;
Backfilled paused rows should be advanced only by approved runtime code or an audited one-off forward-fix. Do not bulk-flip them to runner-claimable states without confirming HITL and policy state.
Go/no-go criteria
GO
- Backfill dry-run reports
Total missing runtime rows: 0at the freeze high-water mark. - Smoke script status is
GO. - Ops
/ops/agent-runsand/ops/approvalsshow scoped runtime data. - Expert and client approval/input surfaces can resolve staged requests.
- Unknown Slack/Telegram/Teams/email/WhatsApp/WeChat ingress remains quarantined.
- New channel sends require explicit runtime policy + approval state.
NO-GO
Any of these block reopen:
- Migration failure or schema drift in runtime tables.
- RLS internal context missing for workers/backfills/smoke or org context not fail-closed.
- Active source rows missing runtime coverage after apply.
- No fresh ready runner registration.
- Stale ready runner registration still eligible for claims.
- Runner callback signatures not enforced.
- Legacy authority path executes a side effect.
- Org-less ingress can execute runtime work.
- Tool/model/artifact/sandbox path bypasses runtime gateways.
Forward-fix containment
Prefer forward-fix when runtime rows/events are intact and the issue is scoped:
- Quarantine bad ingress by inserting/updating
unresolved_channel_events; do not execute it. - Mark broken runner registrations
draining,unhealthy, ordisabledand let healthy runners claim only compatible manifests. - Append compensating
run_eventsfor observable state correction; do not edit ledger rows in place. - Patch metadata with an audited script only when the correction is secret-free and org-scoped.
- Keep affected runs
paused,waiting_approval, orwaiting_inputuntil policy is clear.
Rollback criteria
Rollback is allowed only before reopen or when containment cannot protect clients. Rollback means closing new runtime work and restoring the previous deployed build; it does not mean re-enabling legacy authority routes for side effects.
Rollback if:
- Data corruption affects multiple orgs and cannot be isolated with RLS/runner disablement.
- Runtime event ledger is missing or duplicating events for new production work.
- Approval/input state can result in unauthorized sends.
- Runners accept unsigned events or incompatible manifests.
- Tool/model/artifact/sandbox gateway bypass is observed in production.
Rollback steps:
- Re-freeze ingress and runner claims.
- Disable affected runner registrations.
- Preserve all runtime rows/events for forensic analysis.
- Deploy last known safe build with legacy authority still disabled, or route all affected operations to HITL/manual remediation.
- Announce NO-GO/rollback evidence and assign forward-fix owners.
Staging rehearsal checklist
Run this end-to-end on staging before production:
- Deploy migrations and app builds with production-like env (
SYNCHRONIZE=false). - Register at least one runner from a signed active manifest.
- Create representative active conversations, Expert queue items, agentic tasks, approval requests, user input requests, artifacts, and unresolved ingress events.
- Execute freeze and capture high-water marks.
- Run backfill dry-run, apply, rerun dry-run.
- Run smoke/go-no-go script.
- Verify RLS by checking an org-scoped caller cannot see another org's run.
- Verify unknown/mismatched channel webhook goes to quarantine and creates no run.
- Verify approval resolution from Ops/Expert/client surfaces.
- Verify
/ops/agent-runsdetails show events, policy decisions, tool calls, artifacts, approvals, and input requests with redacted payloads. - Reopen staging and watch 30 minutes of run/event/approval/quarantine/runner metrics.
- Attach evidence to the release ticket.