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:
@@ -304,6 +304,193 @@ Who/what acts next:
|
||||
)
|
||||
)
|
||||
|
||||
# ── Stable control runtime states (#615) ─────────────────────────────────────
|
||||
|
||||
EXAMPLES.append(
|
||||
_example(
|
||||
"runtime_healthy",
|
||||
"""
|
||||
[CONTROLLER HANDOFF] Runtime check — stable control runtime healthy
|
||||
|
||||
Server-side mutation ledger:
|
||||
- none — no server-side state changed
|
||||
|
||||
Blockers:
|
||||
- none
|
||||
""",
|
||||
f"""
|
||||
[THREAD STATE LEDGER] Runtime — stable control runtime healthy
|
||||
|
||||
What is true now:
|
||||
- Runtime mode: stable-control
|
||||
- Runtime git SHA: {HEAD_SHA}
|
||||
- Server-side decision state: no server-side state changed
|
||||
- Local verdict/state: runtime reported real_mutations_allowed=true
|
||||
- Latest known validation: gitea_get_runtime_context read in this session
|
||||
|
||||
What changed:
|
||||
- nothing; this is a read-only runtime observation
|
||||
|
||||
What is blocked:
|
||||
- Blocker classification: no blocker
|
||||
|
||||
Who/what acts next:
|
||||
- Next actor: author
|
||||
- Required action: proceed with the allocated workflow phase
|
||||
- Do not do: restart or relaunch the stable runtime
|
||||
- Resume from: gitea_workflow_dashboard
|
||||
""",
|
||||
)
|
||||
)
|
||||
|
||||
EXAMPLES.append(
|
||||
_example(
|
||||
"transport_flap_recovered",
|
||||
"""
|
||||
[CONTROLLER HANDOFF] Runtime check — transport flap recovered
|
||||
|
||||
Server-side mutation ledger:
|
||||
- none — no server-side state changed
|
||||
|
||||
Blockers:
|
||||
- environment/tooling blocker: MCP transport dropped mid-session and recovered
|
||||
""",
|
||||
f"""
|
||||
[THREAD STATE LEDGER] Runtime — transport flap recovered, namespaces re-proven
|
||||
|
||||
What is true now:
|
||||
- Runtime mode: stable-control
|
||||
- Runtime git SHA: {HEAD_SHA}
|
||||
- Server-side decision state: no server-side state changed
|
||||
- Local verdict/state: all four namespaces re-proven after the flap
|
||||
- Latest known validation: whoami + runtime context + capability resolve per namespace
|
||||
|
||||
What changed:
|
||||
- author, reviewer, merger, and reconciler namespaces each re-proven independently
|
||||
|
||||
What is blocked:
|
||||
- Blocker classification: no blocker
|
||||
|
||||
Who/what acts next:
|
||||
- Next actor: author
|
||||
- Required action: resume the interrupted workflow phase from its last durable state
|
||||
- Do not do: treat author proof as proof of the other namespaces
|
||||
- Resume from: the phase handoff that preceded the flap
|
||||
""",
|
||||
)
|
||||
)
|
||||
|
||||
EXAMPLES.append(
|
||||
_example(
|
||||
"namespace_not_yet_reproven",
|
||||
"""
|
||||
[CONTROLLER HANDOFF] Runtime check — reviewer namespace not re-proven
|
||||
|
||||
Server-side mutation ledger:
|
||||
- none — no server-side state changed
|
||||
|
||||
Blockers:
|
||||
- environment/tooling blocker: reviewer namespace not re-proven since the transport flap
|
||||
""",
|
||||
f"""
|
||||
[THREAD STATE LEDGER] Runtime — reviewer namespace not re-proven after flap
|
||||
|
||||
What is true now:
|
||||
- Runtime mode: stable-control
|
||||
- Runtime git SHA: {HEAD_SHA}
|
||||
- Server-side decision state: no server-side state changed
|
||||
- Local verdict/state: reviewer namespace unproven; mutation gate fails closed
|
||||
- Latest known validation: author namespace re-proven; reviewer not attempted
|
||||
|
||||
What changed:
|
||||
- reviewer mutations blocked with namespace_not_reproven_after_flap
|
||||
|
||||
What is blocked:
|
||||
- Blocker classification: environment/tooling blocker
|
||||
|
||||
Who/what acts next:
|
||||
- Next actor: reviewer
|
||||
- Required action: run whoami, runtime context, and capability resolve in the reviewer namespace
|
||||
- Do not do: substitute author proof for reviewer proof
|
||||
- Resume from: docs/stable-runtime-promotion-runbook.md section 5
|
||||
""",
|
||||
)
|
||||
)
|
||||
|
||||
EXAMPLES.append(
|
||||
_example(
|
||||
"promotion_completed",
|
||||
"""
|
||||
[CONTROLLER HANDOFF] Runtime promotion — completed
|
||||
|
||||
Server-side mutation ledger:
|
||||
- gitea_create_issue_comment on #615 with the promotion record
|
||||
|
||||
Blockers:
|
||||
- none
|
||||
""",
|
||||
f"""
|
||||
[THREAD STATE LEDGER] Runtime — promotion completed and re-proven
|
||||
|
||||
What is true now:
|
||||
- Runtime mode: stable-control
|
||||
- Runtime git SHA: {HEAD_SHA}
|
||||
- Server-side decision state: server-side state changed
|
||||
- Local verdict/state: promotion record carries every required field
|
||||
- Latest known validation: assess_promotion_record valid=true; all namespaces re-proven
|
||||
|
||||
What changed:
|
||||
- stable control runtime advanced to the promoted SHA and reloaded by the operator
|
||||
|
||||
What is blocked:
|
||||
- Blocker classification: no blocker
|
||||
|
||||
Who/what acts next:
|
||||
- Next actor: author
|
||||
- Required action: resume normal workflow phases on the promoted runtime
|
||||
- Do not do: promote again without a fresh record
|
||||
- Resume from: docs/stable-runtime-promotion-runbook.md section 4
|
||||
""",
|
||||
)
|
||||
)
|
||||
|
||||
EXAMPLES.append(
|
||||
_example(
|
||||
"rollback_required",
|
||||
"""
|
||||
[CONTROLLER HANDOFF] Runtime promotion — rollback required
|
||||
|
||||
Server-side mutation ledger:
|
||||
- gitea_create_issue_comment on #615 with the rollback evidence
|
||||
|
||||
Blockers:
|
||||
- environment/tooling blocker: promoted runtime unhealthy, rollback required
|
||||
""",
|
||||
f"""
|
||||
[THREAD STATE LEDGER] Runtime — promoted runtime unhealthy, rollback required
|
||||
|
||||
What is true now:
|
||||
- Runtime mode: unknown
|
||||
- Runtime git SHA: {HEAD_SHA}
|
||||
- Server-side decision state: no server-side state changed after the promotion record
|
||||
- Local verdict/state: promoted runtime failed namespace health; mutations blocked
|
||||
- Latest known validation: namespace health probe reported EOF after reload
|
||||
|
||||
What changed:
|
||||
- all PR/review/merge work stopped pending rollback to the previous runtime SHA
|
||||
|
||||
What is blocked:
|
||||
- Blocker classification: environment/tooling blocker
|
||||
|
||||
Who/what acts next:
|
||||
- Next actor: controller
|
||||
- Required action: operator rolls back to the previous runtime SHA and re-proves every namespace
|
||||
- Do not do: route around the unhealthy runtime or mutate from a dev/test runtime
|
||||
- Resume from: docs/stable-runtime-promotion-runbook.md section 6
|
||||
""",
|
||||
)
|
||||
)
|
||||
|
||||
EXAMPLES.append(
|
||||
_example(
|
||||
"duplicate_canonicalization_blocker",
|
||||
|
||||
Reference in New Issue
Block a user