Files
Gitea-Tools/docs/architecture/mcp-stable-control-runtime-policy-adr.md
T
sysadminandClaude Opus 4.8 9bf3acfef6 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]>
2026-07-20 10:48:47 -04:00

12 KiB
Raw Blame History

ADR: Stable control runtime vs dev runtime (Gitea MCP)

  • Status: Accepted (policy effective immediately for LLM sessions; tooling may lag)
  • Date: 2026-07-09
  • Tracking issue: #615
  • Related:
    • #543 / docs/mcp-namespace-health.md — client-namespace health
    • docs/mcp-namespace-eof-recovery.md — reconnect-only EOF recovery (no PID kill)
    • #558 / docs/mcp-daemon-import-guard.md — sanctioned daemon
    • #557 / docs/bootstrap-review-path.md — controller bootstrap for self-hosted fixes
    • Allocator / control-plane ADR: docs/architecture/mcp-allocator-control-plane-observability-adr.md (#613 / PR #614)

1. Context

The Gitea MCP server is the control plane for real issue/PR mutations (create, comment, lock, review, merge, etc.). When author/reviewer/merger/reconciler sessions kill or restart that process, relaunch it from a feature worktree, or edit the checkout that process loads, operators observe:

  • Mid-session identity/preflight resets
  • Stale-runtime vs master parity failures
  • IDE transport EOF / “tool not found” while code on disk has changed
  • Accidental production mutations from experimental code

This ADR separates stable control runtime from dev/test runtime and defines promotion proof.

2. Decision

2.1 Stable control runtime

The Gitea MCP server used for real workflow mutations is the stable control runtime.

Characteristics:

  • Loads a known, promoted revision of Gitea-Tools (or the packaged release layout operators designate)
  • Registered in the IDE/client as the production namespaces (gitea-tools, gitea-reviewer, gitea-merger, gitea-reconciler, etc.)
  • Holds production profile credentials via sanctioned keychain/env paths only

2.2 Dev / test runtime

MCP server code development and testing:

  • Happens in isolated branches/ worktrees (or other non-stable checkouts)
  • May use a separate dev/test MCP runtime/process when process-level testing is required
  • Must not be used for real Gitea mutations on production issues/PRs

2.3 Forbidden actions (normal sessions)

Normal author, reviewer, merger, and reconciler LLM sessions must not:

Forbidden Why
Kill the running MCP server process Drops all concurrent sessions; loses preflight state
Restart / relaunch the MCP server process Same as kill; causes stale/identity churn mid-workflow
Relaunch MCP from a development worktree Runs unpromoted code against production mutations
Edit files in the stable runtime checkout Hot-mutates control plane under concurrent users
Use experimental/dev MCP for real Gitea mutations Bypasses promotion proof and audit expectations
Bypass or self-reset a stale master-parity gate The gate is fail-closed; only an operator reload restores parity

LLM-allowed vs operator-owned (authoritative split):

Actor May do Must not do
LLM session Call tools on the already-running stable namespaces; client reconnect after transport EOF (no process kill); pass worktree_path / role worktree args; report blockers and stop mutations when unhealthy/stale Kill, restart, or relaunch any MCP process; bump config mtimes to force reload; edit the stable checkout; switch to a dev MCP for production mutations
Operator / release-manager Supervised restart/reload of the stable control runtime; dual-namespace client configuration; §2.4 promotions; incident recovery

EOF / transport recovery for LLM sessions: client reconnect only (see docs/mcp-namespace-eof-recovery.md). Do not “fix” health by killing PIDs or bumping MCP config mtimes as a normal session procedure.

This ADR supersedes any older runbook wording that told the LLM to relaunch or restart the client/MCP as a self-service step. Where docs/llm-workflow-runbooks.md (or wiki runbooks) discuss dual-namespace setup or workspace rebind, process restart/relaunch is operator-owned; the LLM stops, reports, and waits.

2.4 Promotion (operator / release-manager only)

Promotion of a new revision into the stable control runtime is an explicit operator/release-manager action, not an LLM self-service step.

A promotion must record (issue comment, release note, or promotion ledger):

Field Description
previous runtime SHA Commit previously loaded by stable runtime
promoted runtime SHA Commit after promotion
source branch/PR Where the change was reviewed
restart/reload method How the process was cycled (e.g. supervised restart, client reload)
health check proof Client-namespace probe success (gitea_whoami / namespace health)
identity/profile proof Expected profile(s) and username(s) after reload
workspace/root proof Stable checkout path / root matches intended layout
mutation capability proof Required permissions for the target role present; forbidden ops still forbidden
rollback instructions How to restore previous SHA and re-verify health

Suggested durable marker:

## MCP STABLE RUNTIME PROMOTION (#615)

Status: COMPLETED | ROLLED_BACK | ABORTED
Previous-SHA: <full sha>
Promoted-SHA: <full sha>
Source-PR: <number>
Source-Branch: <name>
Reload-Method: <text>
Health-Proof: client_namespace whoami OK / assess_mcp_namespace_health OK
Identity-Proof: profile=<name> user=<name>
Workspace-Proof: root=<path>
Mutation-Proof: allowed_ops include <…>; forbidden include <…>
Rollback: checkout <previous sha>; reload method <…>; re-run health/identity proofs
Operator: <username>
Timestamp: <ISO-8601>

2.5 Unhealthy stable runtime → stop work

If the stable MCP runtime is unhealthy, including any of:

  • client-namespace probes fail
  • wrong identity / wrong profile
  • wrong workspace root
  • missing mutation capability for the intended role
  • persistent EOF after client reconnect
  • master parity is stale (startup_head behind on-disk master / restart_required from the parity gate)

then:

  1. Normal PR / review / merge / issue-mutation work must stop immediately.
  2. The session reports the unhealthy/stale state (tool error, CTH, or operator handoff) with startup vs current head when known.
  3. Do not improvise: no LLM process kill/restart, no dev-worktree MCP for production mutations, no env escape hatches, no manual gate bypass.
  4. Resume only after:
    • an operator restores the runtime (see §2.6 for routine post-merge parity reload), or
    • a controlled promotion/rollback completes with the §2.4 promotion record, or
    • a controller invokes the narrow bootstrap review path (#557) when the defect is self-hosted and documented,
    • and the session re-verifies health (and master parity when applicable) before the next mutation.

2.6 Routine post-merge master-parity staleness (operator reload, not promotion)

Symptom: After merges land on master, a long-lived stable MCP process still runs the pre-merge startup_head. The master-parity gate marks the server stale / restart_required and blocks mutations.

Sanctioned response (authoritative):

Step Actor Action
1 LLM session Mutations stop immediately when the gate reports stale.
2 LLM session Report the stale state (startup head, current master head, that operator reload is required). Do not retry mutations.
3 Operator Reload/restart the stable control MCP so it loads current master (supervised client/daemon reload).
4 LLM session Resume only after startup/current-head parity is verified (e.g. gitea_get_runtime_context / parity assessment shows in parity).

Not a §2.4 promotion: Catching the already-designated stable control checkout up to a newly advanced master is a routine operator reload of the stable runtime. It does not require the nine-field promotion ledger. Use §2.4 only when changing which unpromoted/dev revision becomes the stable control runtime (new source branch/PR into the stable designation).

Code note: master_parity_gate.py may still say “restart the server” in machine-facing reason strings. That string names the operator recovery action, not an LLM self-service instruction. This ADR and the runbooks define the actor split.

3. Relationship to other controls

Doc / mechanism Interaction
Namespace health (#543) Proves IDE client can call tools; does not authorize restart
EOF recovery Reconnect only; no process kill
Daemon import guard (#558) Mutations require sanctioned daemon; not a bare shell import
Bootstrap path (#557) Only controller-authorized exception when live runtime cannot review its own fix
Allocator / control-plane ADR Coordination DB is separate; still depends on a healthy MCP surface for Gitea writes

4. Consequences

Positive

  • Predictable control plane for concurrent LLMs
  • Clear operator-only promotion gate with rollback
  • Aligns session behavior with health/EOF docs already landed

Costs

  • LLM sessions must wait when runtime is sick (no DIY restart)
  • Operators must maintain promotion discipline and dual-runtime config if they use a dev MCP

Non-goals

  • Does not ban operator-supervised restarts during incidents
  • 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

The policy binds sessions now. The enforcement layer landed with issue #615 acceptance criteria 611 in stable_control_runtime.py:

  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.

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.

6. Acceptance for this ADR

  1. Document merged under docs/architecture/mcp-stable-control-runtime-policy-adr.md.
  2. Operator guide / runbooks cross-link this ADR (docs/wiki/Operator-Guide.md, docs/wiki/Runbooks.md, docs/llm-workflow-runbooks.md).
  3. Issue #615 references this path.
  4. LLM/operator runbooks treat kill/restart/relaunch-from-worktree as LLM violations; process restart is operator-owned.
  5. Unhealthy runtime (including stale master parity) stops normal mutation work until operator restore/reload, promotion/rollback, or #557 bootstrap — then re-verify parity before mutating.
  6. Routine post-merge parity reload is documented as operator reload (§2.6), not an LLM self-restart and not a full §2.4 promotion.

7. Document history

Date Change
2026-07-09 Initial ADR: stable vs dev runtime, forbidden session actions, promotion proof fields, stop-work rule
2026-07-16 Review 443 remediation (#615 / PR #616): mandatory cross-links; LLM vs operator restart split; routine post-merge parity staleness (§2.6); stale parity in §2.5 unhealthy triggers