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]>
172 lines
6.6 KiB
Bash
Executable File
172 lines
6.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
usage: scripts/promote-stable-runtime [--root <path>] [--promoted <sha>] \
|
|
[--source-branch <branch>] [--source-pr <n>] \
|
|
[--restart-method <text>] [--rollback <text>] \
|
|
[--health-check-proof <text>] \
|
|
[--identity-proof <text>] [--profile-proof <text>] \
|
|
[--workspace-proof <text>] \
|
|
[--mutation-capability-proof <text>]
|
|
|
|
Emit and validate a stable-control-runtime promotion record (#615).
|
|
|
|
This helper is READ-ONLY. It never fetches, merges, restarts, reloads, or kills
|
|
anything: promotion itself is an operator action documented in
|
|
docs/stable-runtime-promotion-runbook.md. The helper reads the current runtime
|
|
state, assembles the required record, validates it with
|
|
stable_control_runtime.assess_promotion_record(), and prints it for the operator
|
|
to act on and archive.
|
|
|
|
Defaults:
|
|
--root the repository root containing this script
|
|
--promoted HEAD of that root
|
|
|
|
Exit status is non-zero when the assembled record is incomplete, so a promotion
|
|
cannot be recorded without its proof fields.
|
|
|
|
Example:
|
|
scripts/promote-stable-runtime \
|
|
--source-branch feat/issue-615-runtime-mode-enforcement \
|
|
--source-pr 770 \
|
|
--restart-method "IDE client reconnect (/mcp)" \
|
|
--rollback "git -C <root> merge --ff-only <previous-sha>; reconnect client" \
|
|
--health-check-proof "gitea_assess_mcp_namespace_health: all four healthy" \
|
|
--identity-proof "gitea_whoami per namespace" \
|
|
--profile-proof "gitea_get_runtime_context per namespace" \
|
|
--workspace-proof "process root == canonical root; clean" \
|
|
--mutation-capability-proof "gitea_resolve_task_capability: allowed"
|
|
EOF
|
|
}
|
|
|
|
ROOT=""
|
|
PROMOTED=""
|
|
SOURCE_BRANCH=""
|
|
SOURCE_PR=""
|
|
RESTART_METHOD=""
|
|
ROLLBACK=""
|
|
HEALTH_PROOF=""
|
|
IDENTITY_PROOF=""
|
|
PROFILE_PROOF=""
|
|
WORKSPACE_PROOF=""
|
|
CAPABILITY_PROOF=""
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--root) ROOT="${2:-}"; shift 2 ;;
|
|
--promoted) PROMOTED="${2:-}"; shift 2 ;;
|
|
--source-branch) SOURCE_BRANCH="${2:-}"; shift 2 ;;
|
|
--source-pr) SOURCE_PR="${2:-}"; shift 2 ;;
|
|
--restart-method) RESTART_METHOD="${2:-}"; shift 2 ;;
|
|
--rollback) ROLLBACK="${2:-}"; shift 2 ;;
|
|
--health-check-proof) HEALTH_PROOF="${2:-}"; shift 2 ;;
|
|
--identity-proof) IDENTITY_PROOF="${2:-}"; shift 2 ;;
|
|
--profile-proof) PROFILE_PROOF="${2:-}"; shift 2 ;;
|
|
--workspace-proof) WORKSPACE_PROOF="${2:-}"; shift 2 ;;
|
|
--mutation-capability-proof) CAPABILITY_PROOF="${2:-}"; shift 2 ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
*) echo "unknown argument: $1" >&2; usage; exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
ROOT="${ROOT:-$(cd "$SCRIPT_DIR/.." && pwd)}"
|
|
|
|
if ! git -C "$ROOT" rev-parse --show-toplevel >/dev/null 2>&1; then
|
|
echo "error: '$ROOT' is not a git checkout; cannot read runtime SHAs" >&2
|
|
exit 1
|
|
fi
|
|
|
|
PREVIOUS="${GITEA_MCP_PREVIOUS_RUNTIME_SHA:-}"
|
|
if [[ -z "$PREVIOUS" ]]; then
|
|
# The runtime the operator is replacing. Best-effort: the commit master
|
|
# pointed at before the fast-forward, recorded in the reflog.
|
|
PREVIOUS="$(git -C "$ROOT" rev-parse 'master@{1}' 2>/dev/null || true)"
|
|
fi
|
|
PROMOTED="${PROMOTED:-$(git -C "$ROOT" rev-parse HEAD)}"
|
|
BRANCH="$(git -C "$ROOT" rev-parse --abbrev-ref HEAD)"
|
|
DIRTY="$(git -C "$ROOT" status --porcelain | wc -l | tr -d ' ')"
|
|
STAMP="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
|
|
if [[ -z "$WORKSPACE_PROOF" ]]; then
|
|
WORKSPACE_PROOF="root=$ROOT branch=$BRANCH dirty_files=$DIRTY"
|
|
fi
|
|
if [[ -z "$ROLLBACK" && -n "$PREVIOUS" ]]; then
|
|
ROLLBACK="git -C $ROOT merge --ff-only $PREVIOUS (or checkout $PREVIOUS), then reload the runtime by the same sanctioned method and re-prove every namespace"
|
|
fi
|
|
|
|
cat <<EOF
|
|
# Stable control runtime promotion record (#615)
|
|
# Generated $STAMP by scripts/promote-stable-runtime (read-only)
|
|
|
|
previous_runtime_sha: ${PREVIOUS:-<MISSING: record the SHA the runtime served before promotion>}
|
|
promoted_runtime_sha: ${PROMOTED}
|
|
source_branch: ${SOURCE_BRANCH:-<MISSING: pass --source-branch>}
|
|
source_pr: ${SOURCE_PR:-<MISSING: pass --source-pr>}
|
|
restart_method: ${RESTART_METHOD:-<MISSING: pass --restart-method>}
|
|
health_check_proof: ${HEALTH_PROOF:-<MISSING: pass --health-check-proof>}
|
|
identity_proof: ${IDENTITY_PROOF:-<MISSING: pass --identity-proof>}
|
|
profile_proof: ${PROFILE_PROOF:-<MISSING: pass --profile-proof>}
|
|
workspace_proof: ${WORKSPACE_PROOF}
|
|
mutation_capability_proof: ${CAPABILITY_PROOF:-<MISSING: pass --mutation-capability-proof>}
|
|
rollback_instructions: ${ROLLBACK:-<MISSING: pass --rollback>}
|
|
EOF
|
|
|
|
if [[ "$DIRTY" != "0" ]]; then
|
|
{
|
|
echo
|
|
echo "WARNING: the stable checkout has $DIRTY dirty file(s); a dirty stable"
|
|
echo " runtime is itself a mutation blocker (dirty_stable_runtime_checkout)."
|
|
} >&2
|
|
fi
|
|
|
|
PYTHON_BIN="${PYTHON_BIN:-python3}"
|
|
RECORD_JSON="$(
|
|
ROOT="$ROOT" \
|
|
PREVIOUS="$PREVIOUS" PROMOTED="$PROMOTED" \
|
|
SOURCE_BRANCH="$SOURCE_BRANCH" SOURCE_PR="$SOURCE_PR" \
|
|
RESTART_METHOD="$RESTART_METHOD" HEALTH_PROOF="$HEALTH_PROOF" \
|
|
IDENTITY_PROOF="$IDENTITY_PROOF" PROFILE_PROOF="$PROFILE_PROOF" \
|
|
WORKSPACE_PROOF="$WORKSPACE_PROOF" CAPABILITY_PROOF="$CAPABILITY_PROOF" \
|
|
ROLLBACK="$ROLLBACK" \
|
|
"$PYTHON_BIN" -c '
|
|
import json
|
|
import os
|
|
|
|
print(json.dumps({
|
|
"previous_runtime_sha": os.environ.get("PREVIOUS", ""),
|
|
"promoted_runtime_sha": os.environ.get("PROMOTED", ""),
|
|
"source_branch": os.environ.get("SOURCE_BRANCH", ""),
|
|
"source_pr": os.environ.get("SOURCE_PR", ""),
|
|
"restart_method": os.environ.get("RESTART_METHOD", ""),
|
|
"health_check_proof": os.environ.get("HEALTH_PROOF", ""),
|
|
"identity_proof": os.environ.get("IDENTITY_PROOF", ""),
|
|
"profile_proof": os.environ.get("PROFILE_PROOF", ""),
|
|
"workspace_proof": os.environ.get("WORKSPACE_PROOF", ""),
|
|
"mutation_capability_proof": os.environ.get("CAPABILITY_PROOF", ""),
|
|
"rollback_instructions": os.environ.get("ROLLBACK", ""),
|
|
}))
|
|
'
|
|
)"
|
|
|
|
RECORD_JSON="$RECORD_JSON" ROOT="$ROOT" "$PYTHON_BIN" -c '
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
sys.path.insert(0, os.environ["ROOT"])
|
|
import stable_control_runtime as scr
|
|
|
|
record = json.loads(os.environ["RECORD_JSON"])
|
|
assessment = scr.assess_promotion_record(record)
|
|
print()
|
|
print("validation:", json.dumps(assessment, indent=2))
|
|
print()
|
|
if not assessment["valid"]:
|
|
print("Promotion record is INCOMPLETE - do not archive it as a promotion.")
|
|
sys.exit(1)
|
|
print("Promotion record is complete. Archive it on the tracking issue.")
|
|
'
|