feat: enforce stable-control vs dev runtime modes for Gitea MCP (Closes #615)
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]>
This commit is contained in:
@@ -171,13 +171,18 @@ then:
|
||||
- Does not replace CI or code review for MCP changes
|
||||
- Does not authorize editing stable checkout “because tests need a quick fix”
|
||||
|
||||
## 5. Implementation follow-ups (optional tooling)
|
||||
## 5. Implementation follow-ups
|
||||
|
||||
These may land in later issues; the **policy binds sessions now**:
|
||||
The **policy binds sessions now**. The enforcement layer landed with issue #615
|
||||
acceptance criteria 6–11 in `stable_control_runtime.py`:
|
||||
|
||||
1. Session preflight that refuses mutations if workspace root equals a `branches/` feature worktree configured as “dev only.”
|
||||
2. Explicit `runtime_kind=stable|dev` in MCP config and `gitea_whoami` profile metadata.
|
||||
3. Promotion checklist script that emits the durable promotion marker fields.
|
||||
1. ~~Session preflight that refuses mutations if workspace root equals a `branches/` feature worktree configured as “dev only.”~~ **Landed.** `_runtime_mode_block()` refuses every mutating operation from a `dev-test`, dev-worktree-launched, dirty-stable, misaligned, or `unknown` runtime; `gitea.read` is never blocked, so an operator can still diagnose a sick runtime.
|
||||
2. ~~Explicit `runtime_kind=stable|dev` in MCP config and `gitea_whoami` profile metadata.~~ **Landed** as `runtime_mode` (`stable-control` | `dev-test` | `unknown`), reported by `gitea_get_runtime_context` under `stable_control_runtime` together with the runtime git SHA, branch, checkout path, process root, active workspace, alignment, dirty files, and `real_mutations_allowed`. Operators running a packaged layout with no git checkout declare the mode explicitly with `GITEA_MCP_RUNTIME_MODE`.
|
||||
3. ~~Promotion checklist script that emits the durable promotion marker fields.~~ **Landed** as `scripts/promote-stable-runtime` (read-only; emits and validates the record) plus [`../stable-runtime-promotion-runbook.md`](../stable-runtime-promotion-runbook.md).
|
||||
|
||||
Post-transport-flap proof is enforced per namespace: a flap invalidates every
|
||||
`gitea-*` namespace at once, and author proof never transfers to reviewer,
|
||||
merger, or reconciler (`namespace_not_reproven_after_flap`).
|
||||
|
||||
**Not optional (issue #615 acceptance criterion 2):** operator guide and runbooks **must** cross-link this ADR (see §6). Cross-links are documentation acceptance, not deferred tooling.
|
||||
|
||||
|
||||
@@ -1241,6 +1241,7 @@ When posting a Canonical Thread Handoff after a binding blocker:
|
||||
## Related documents
|
||||
|
||||
- [`architecture/mcp-stable-control-runtime-policy-adr.md`](architecture/mcp-stable-control-runtime-policy-adr.md) — stable control runtime vs dev runtime; LLM must not kill/restart MCP; operator-owned reload and promotions; routine post-merge parity staleness (#615).
|
||||
- [`stable-runtime-promotion-runbook.md`](stable-runtime-promotion-runbook.md) — operator promotion procedure, required promotion-record fields, per-namespace post-flap re-proving, and rollback for the stable control runtime (#615).
|
||||
- [`reviewer-handoff-consistency.md`](reviewer-handoff-consistency.md) — reject contradictory reviewer handoffs (#501).
|
||||
- [`issue-acceptance-gate.md`](issue-acceptance-gate.md) — controller issue-acceptance audit after PR merge (#500).
|
||||
- [`../skills/llm-project-workflow/SKILL.md`](../skills/llm-project-workflow/SKILL.md) — portable cross-project LLM workflow skill.
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
# 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`](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_parity` reports `stale: true` / `restart_required: true`.
|
||||
- `gitea_get_runtime_context` reports a `runtime_mode` other than
|
||||
`stable-control`, or `real_mutations_allowed: false`.
|
||||
|
||||
## 2. Pre-promotion checks
|
||||
|
||||
Run these **before** advancing the stable checkout:
|
||||
|
||||
1. The target revision is on remote `master` and was merged through
|
||||
`gitea_merge_pr` (never a direct stable-branch push — see #671).
|
||||
2. The stable control checkout is clean (`git status --porcelain` empty) and on
|
||||
`master`. A dirty stable runtime is itself a mutation blocker.
|
||||
3. The advance is strictly fast-forwardable: local `master` is an ancestor of
|
||||
`prgs/master`.
|
||||
4. No active workflow lease is mid-mutation (`gitea_list_workflow_leases`).
|
||||
|
||||
## 3. Promotion steps
|
||||
|
||||
1. Record the **previous** runtime SHA (`gitea_assess_master_parity` →
|
||||
`startup_head`).
|
||||
2. `git fetch --prune prgs` in the stable control checkout.
|
||||
3. `git merge --ff-only prgs/master` — never rebase, reset, or force.
|
||||
4. Record the **promoted** runtime SHA (`git rev-parse HEAD`).
|
||||
5. Reload the runtime using the sanctioned client path (IDE/client reconnect or
|
||||
the operator's supervised service reload). Never `pkill` the daemon from a
|
||||
workflow session.
|
||||
6. Re-prove **each** namespace independently (see §5).
|
||||
7. 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:
|
||||
|
||||
1. `gitea_whoami`
|
||||
2. `gitea_get_runtime_context`
|
||||
3. `gitea_resolve_task_capability` immediately before the intended mutation
|
||||
4. 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`:
|
||||
|
||||
1. **Stop all PR/review/merge work.** An unhealthy stable runtime fails closed;
|
||||
do not route around it.
|
||||
2. Fast-forward or check out `previous_runtime_sha` in the stable checkout.
|
||||
3. Reload the runtime by the same sanctioned method.
|
||||
4. Re-prove every namespace (§5).
|
||||
5. Record the rollback as a promotion record whose `promoted_runtime_sha` is the
|
||||
restored SHA, with the failure evidence in `health_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 recovery
|
||||
- `mcp-daemon-import-guard.md` — sanctioned daemon only (#558)
|
||||
- `bootstrap-review-path.md` — controller bootstrap when the live runtime cannot
|
||||
review its own fix (#557)
|
||||
Reference in New Issue
Block a user