fix(mcp): block manual daemon killing as workflow recovery (Closes #630)

A session that ran `pkill -f mcp_server.py`, waited for the IDE to respawn
the daemons, and then closed an issue left no trace distinguishing that
closure from one performed over a sanctioned runtime. Detection existed only
as advisory strings (`native_mcp_preference.classify_command_path`,
`review_workflow_boundary`) and never failed closed on the mutations that
followed.

Adds `runtime_recovery_guard.py`, mirroring the #671 stable-branch
contamination model so the two cannot drift apart: classification of
kill/pkill/killall commands and known-pid kills, a durable
`runtime_recovery_contamination` marker, a fail-closed pre-flight gate over
the shared review/merge/close/completion task set, and reconciler-only
clearing. `comment_issue` and `lock_issue` stay allowed so a contaminated
worker can still post its audit comment and hand off. The marker is
recovery-critical, so contamination cannot expire into cleanliness with the
session-state TTL.

Read-only inspection (`ps aux | grep mcp_server`), sanctioned reconnects,
and process management unrelated to the daemons are never flagged; a bare
`kill` of an unknown pid is reported as ambiguous rather than contaminating,
so ordinary subprocess work is not false-blocked. A pattern broad enough to
sweep unrelated namespaces (`pkill -f python`) is contamination even when it
never names MCP.

Operator-authorized host maintenance stays permitted, but the authorization
is read from GITEA_OPERATOR_DAEMON_MAINTENANCE_AUTHORIZATION in the process
environment only and never from a tool argument, so a session cannot
authorize itself.

Final-report rules reject clean-session claims and require the contaminated
recovery to be surfaced. Two tools are registered (inventory 112 to 114) and
the sanctioned-versus-forbidden contrast is documented in the namespace
recovery doc and the workflow skill.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
2026-07-21 17:54:19 -05:00
co-authored by Claude Opus 4.8
parent 7ecf7bf2d6
commit 1ec4672fad
8 changed files with 1315 additions and 0 deletions
+36
View File
@@ -168,6 +168,42 @@ Tooling: call `gitea_record_stable_branch_push_attempt` to classify/record a
proposed push before running it; `gitea_audit_stable_branch_contamination` to
inspect or (reconciler-only) clear the marker.
## Runtime Recovery Protection (#630)
MCP connectivity is recovered through **sanctioned reconnect/restart only**:
host auto-reconnect, an explicit client reconnect, an IDE/client relaunch, or an
operator-owned restart. Worker sessions must never kill the daemons their own
proof depends on.
**Forbidden for author/reviewer/merger sessions:**
- `pkill -f mcp_server.py`, `pkill -f gitea_mcp_server`, broad `pkill -f mcp`.
- `killall` of a daemon, or `kill <pid>` of an MCP daemon pid.
- Any pattern broad enough to sweep unrelated namespaces (`pkill -f python`),
even when it never names MCP.
**Allowed (never blocked):** read-only inspection (`ps aux | grep mcp_server`),
and process management unrelated to the daemons — a `kill` of some other pid is
reported as *ambiguous*, not as contamination.
**What happens on a detected attempt:** the session is marked
workflow-contaminated (durable marker, redacted command summary + session id +
remote + role). While contaminated, all review / merge / close / completion
mutations fail closed. `comment_issue` and `lock_issue` remain allowed so the
contaminated worker can post the durable audit comment and hand off.
Contamination **cannot be self-cleared** — only a reconciler audit may clear it,
and it does not expire with the session-state TTL. The final report must surface
the contaminated recovery and must not claim a clean session.
Operator-authorized host maintenance stays permitted, but the authorization is
read from the operator's environment, never from a tool argument: a session must
not be able to authorize itself.
Tooling: call `gitea_record_daemon_process_kill_attempt` to classify/record a
proposed command before running it; `gitea_audit_runtime_recovery_contamination`
to inspect or (reconciler-only) clear the marker. Full contrast in
`docs/mcp-namespace-eof-recovery.md`.
## Shell Spawn Hard-Stop Rule
`exit_code: -1` with empty stdout/stderr means the shell failed to spawn — not a