The stable-control runtime ADR (docs/architecture/mcp-stable-control-runtime-policy-adr.md)
established the policy but had no runtime enforcement: a daemon relaunched from a
feature worktree still holds production credentials and will happily mutate real
issues. This adds the enforcement layer (acceptance criteria 6-11).
stable_control_runtime.py:
- classify_runtime_mode(): stable-control | dev-test | unknown, inferred from the
process root and checkout branch, with an explicit GITEA_MCP_RUNTIME_MODE
declaration for packaged layouts that have no git checkout.
- build_runtime_report(): runtime mode, git SHA, branch, checkout path, process
root, active workspace, repo binding, profile, identity, dirty files,
alignment, and real_mutations_allowed.
- assess_runtime_mutation_gate(): fails closed on dev-test targeting production,
unknown runtime, dirty stable checkout, dev-worktree launch, and unsafe
process-root/workspace alignment.
- Post-transport-flap re-proving tracked per namespace, so proving the author
namespace never implies reviewer, merger, or reconciler (#584).
- assess_promotion_record(): promotion must record previous and promoted SHAs
plus health, identity, profile, workspace, capability, and rollback proof.
Server wiring:
- _profile_operation_gate() consults the runtime gate alongside the #420 parity
gate. gitea.read is never blocked, so an operator can still diagnose a sick
runtime.
- The gate reads a startup snapshot rather than shelling out per mutation, for
the same reason parity uses a startup baseline: the runtime a process serves
from is fixed when it loads its code. Enforcement is decided from how the
process was loaded, so per-test production simulation cannot switch it on.
- gitea_get_runtime_context() reports the live runtime under
stable_control_runtime and points at the promotion runbook when blocked.
Docs and tooling:
- docs/stable-runtime-promotion-runbook.md: operator promotion procedure,
required record fields, per-namespace re-proving, rollback.
- scripts/promote-stable-runtime: read-only helper that emits and validates a
promotion record; it never restarts anything.
- Five canonical [THREAD STATE LEDGER] examples: runtime healthy, transport flap
recovered, namespace not yet re-proven, promotion completed, rollback required.
- ADR section 5 follow-ups marked landed; runbooks cross-link the new runbook.
Validation: 47 new tests in tests/test_stable_control_runtime.py. Full suite
3827 passed / 6 skipped / 2 failed; both failures are pre-existing on master
059ee77 (verified in a clean baseline worktree: identical 2 failures,
3780 passed).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
5.6 KiB
Stable control runtime — promotion runbook (#615)
Operator / release-manager procedure for promoting a revision into the stable control runtime: the Gitea MCP server that performs real issue/PR mutations.
Policy source: architecture/mcp-stable-control-runtime-policy-adr.md.
Enforcement: stable_control_runtime.py (runtime mode classification, mutation
gates, per-namespace post-flap re-proving, promotion-record validation).
Promotion is operator-owned. Normal author / reviewer / merger / reconciler
sessions must never kill, restart, or relaunch the MCP server, and must never
edit the stable runtime checkout. A session that needs newer server code stops
with BLOCKED + DIAGNOSE and hands off to the operator.
1. When a promotion is required
- A merged PR changes MCP server code the control plane must now enforce.
gitea_assess_master_parityreportsstale: true/restart_required: true.gitea_get_runtime_contextreports aruntime_modeother thanstable-control, orreal_mutations_allowed: false.
2. Pre-promotion checks
Run these before advancing the stable checkout:
- The target revision is on remote
masterand was merged throughgitea_merge_pr(never a direct stable-branch push — see #671). - The stable control checkout is clean (
git status --porcelainempty) and onmaster. A dirty stable runtime is itself a mutation blocker. - The advance is strictly fast-forwardable: local
masteris an ancestor ofprgs/master. - No active workflow lease is mid-mutation (
gitea_list_workflow_leases).
3. Promotion steps
- Record the previous runtime SHA (
gitea_assess_master_parity→startup_head). git fetch --prune prgsin the stable control checkout.git merge --ff-only prgs/master— never rebase, reset, or force.- Record the promoted runtime SHA (
git rev-parse HEAD). - Reload the runtime using the sanctioned client path (IDE/client reconnect or
the operator's supervised service reload). Never
pkillthe daemon from a workflow session. - Re-prove each namespace independently (see §5).
- Record the promotion (see §4) and post it as a durable comment on the tracking issue.
4. Promotion record (required fields)
Every promotion must record all of the following. assess_promotion_record()
validates them and fails closed on any missing field, or when
previous_runtime_sha equals promoted_runtime_sha (nothing was promoted).
| Field | Meaning |
|---|---|
previous_runtime_sha |
SHA the stable runtime was serving before promotion |
promoted_runtime_sha |
SHA the stable runtime serves after promotion |
source_branch |
Branch the promoted revision came from |
source_pr |
PR number that merged it |
restart_method |
Exact reload/restart mechanism the operator used |
health_check_proof |
gitea_assess_mcp_namespace_health result per namespace |
identity_proof |
gitea_whoami username + profile per namespace |
profile_proof |
gitea_get_runtime_context active profile per namespace |
workspace_proof |
Process root, canonical root, alignment, clean state |
mutation_capability_proof |
gitea_resolve_task_capability for the intended task |
rollback_instructions |
Exact steps to return to previous_runtime_sha |
Helper: scripts/promote-stable-runtime emits and validates the record. It
never restarts anything — it reads state and prints the record for the operator
to act on and archive.
5. Post-promotion namespace re-proving
A restart or transport flap drops every gitea-* namespace together. Author
proof is not global proof. For each of author, reviewer, merger,
reconciler, in that namespace:
gitea_whoamigitea_get_runtime_contextgitea_resolve_task_capabilityimmediately before the intended mutation- Mutate only when no reconnect / restart / stale-runtime gate is reported
Until a namespace passes all four, its mutations stay blocked with
namespace_not_reproven_after_flap.
6. Rollback
If the promoted runtime is unhealthy — namespace EOF that does not recover,
identity or profile mismatch, capability resolution failure, or an unexpected
runtime_mode:
- Stop all PR/review/merge work. An unhealthy stable runtime fails closed; do not route around it.
- Fast-forward or check out
previous_runtime_shain the stable checkout. - Reload the runtime by the same sanctioned method.
- Re-prove every namespace (§5).
- Record the rollback as a promotion record whose
promoted_runtime_shais the restored SHA, with the failure evidence inhealth_check_proof.
7. Runtime modes seen in reports
| Mode | Meaning | Real mutations |
|---|---|---|
stable-control |
Promoted revision, stable branch, clean checkout | Allowed |
dev-test |
Launched from a branches/ worktree or a feature branch |
Blocked against production |
unknown |
Root unresolvable, not a git checkout, or detached HEAD with no declaration | Blocked |
A packaged deployment with no git checkout must declare itself explicitly with
GITEA_MCP_RUNTIME_MODE=stable-control; an unset or misspelled value falls back
to inference and, failing that, to unknown.
8. Related
architecture/mcp-stable-control-runtime-policy-adr.md— the policy (#615)mcp-namespace-health.md— client-namespace health (#543)mcp-namespace-eof-recovery.md— reconnect-only EOF recoverymcp-daemon-import-guard.md— sanctioned daemon only (#558)bootstrap-review-path.md— controller bootstrap when the live runtime cannot review its own fix (#557)