Files
Gitea-Tools/docs/sanctioned-restart-controls.md
T
sysadminandClaude Opus 4.8 e593444eea feat(webui): sanctioned restart and graceful reload controls (Closes #642)
Adds a gated `system.restart_namespace` action so operators and workers can
restart or gracefully reload MCP namespaces through an authorized, audited
path instead of manual host process killing (#630).

- webui/sanctioned_restart.py: restart/reload operation model, dry-run
  intent preview, confirmation-string enforcement, audit emission, and
  post-restart health verification. Fails closed on unknown auth, missing
  capability, or ambiguous target namespace.
- webui/console_authz.py: RBAC entries for the restart capability with
  secret redaction preserved.
- webui/gated_actions.py: registers the restart action in the gated action
  framework so it cannot be invoked without capability + confirmation.
- task_capability_map.py: capability mapping for the restart operation.
- docs/sanctioned-restart-controls.md: operator documentation for the
  sanctioned path and the explicit prohibition on pkill recovery.
- docs/webui-authz-audit.md: audit model updated for restart events.
- tests/test_webui_sanctioned_restart.py: authorized preview, unauthorized
  deny, confirmation enforcement, contamination classification, and audit
  emission coverage.

No unrestricted kill path is exposed; manual pkill remains classified as
contamination and continues to block clean claims.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-23 23:19:21 -04:00

5.7 KiB

Sanctioned restart and graceful reload controls (#642)

Sessions used to recover MCP connectivity by killing the host daemon (pkill -f mcp_server.py, #630). That is forbidden and stays forbidden: it kills every namespace on the host, contaminates whichever session survives, and leaves no audit trail. This document describes the sanctioned replacement, implemented in webui/sanctioned_restart.py.

What the console will and will not do

The console never restarts anything. It authorizes an intent, records it, and hands off to a host supervisor. There is no code path in which the console sends a signal, spawns a process, or renders a kill command — a regression test asserts the module contains no subprocess, signal, os.kill, os.system, or popen reference, and that no returned payload contains a kill command.

Operations

Mode Action Minimum role Behaviour
reload system.reload_namespace controller Host supervisor reloads the namespace in place, draining in-flight requests.
restart system.restart_namespace admin Host supervisor restarts the namespace. In-flight requests are lost.

Scope is always exactly one namespace. A fleet-wide restart is an explicit non-goal: all, *, fleet, and an empty scope are refused with fleet_scope_not_permitted, because that is precisely the blast radius the forbidden kill already had. An unrecognised namespace is refused rather than passed through to the host.

The gate sequence

assess_restart_request() applies every gate in order and reports the first failure with a stable reason code:

Order Gate Reason code on failure
1 Mode is restart or reload unknown_mode
2 Scope is a single known namespace fleet_scope_not_permitted, unknown_namespace
3 Principal holds the required console role unauthorized
4 Confirmation phrase supplied confirmation_required
5 Confirmation names this namespace and mode confirmation_mismatch
6 Out-of-band operator authorization present operator_authorization_missing
7 Runtime is not contaminated contaminated_runtime
8 Host restart hook configured restart_hook_not_configured

Passing every gate yields host_action_required, never "restarted".

Confirmation binds the namespace

The required phrase is "<mode> <namespace>" — for example restart gitea-author. Binding the namespace into the phrase is the point: a confirmation typed for one namespace cannot be replayed against another.

Operator authorization is not self-assertable

Host daemon maintenance is authorized out of band through GITEA_OPERATOR_DAEMON_MAINTENANCE_AUTHORIZATION, read from the process environment and nowhere else (#630; #710 finding F1). A worker session cannot set an environment variable for an already-running daemon, so this cannot be faked the way a tool argument could.

The host hook

GITEA_SANCTIONED_RESTART_HOOK holds an opaque reference the host resolves — a supervisor label such as a launchd job name, never a command line. With no hook configured the request is refused; the console does not fall back to a process kill. The value is read server-side and never rendered to a client.

Manual kill remains contamination

classify_restart_command() classifies an operator-proposed recovery command. A manual pkill/kill/killall of the MCP daemon is contamination, not a restart: it returns clean_claim_allowed: false and builds a durable contamination marker (redacted command only, never secrets) naming system.restart_namespace as the sanctioned alternative.

A live, uncleared contamination marker also blocks a restart. This is stricter than #630's task-scoped gate, which deliberately lets a contaminated worker keep commenting and handing off: restarting a contaminated runtime would launder the contamination rather than resolve it. Clear the marker through the reconciler path first.

Post-restart health verification

After the host supervisor acts, verify_post_restart_health() decides whether the session may claim to be clean:

Status Meaning Clean claim
clean Required tool callable, proven through the live client namespace Allowed
unproven Reported healthy without live client-namespace evidence Refused
unhealthy Probe failed Refused

Only probe_source=client_namespace evidence clears a session. Static tool registration is not proof, and neither is an offline subprocess probe — an IDE client can hold a registered tool list while live calls fail with client is closing: EOF (see mcp-namespace-health.md).

Audit

Every attempt — allowed or denied — is recorded through webui.console_audit with actor, target namespace, mode, result, and reason code, and is redacted before it is persisted. system.restart_namespace is break-glass, so its records are retained for 730 days. Records carry process_kill_executed: false, which is a fact about the code path rather than a claim: no such path exists.

Environment variables

Variable Purpose
GITEA_SANCTIONED_RESTART_HOOK Host supervisor reference; absent means restart is refused.
GITEA_OPERATOR_DAEMON_MAINTENANCE_AUTHORIZATION Out-of-band operator authorization reference.
WEBUI_AUDIT_LOG Console audit sink; absent means records are built but not persisted.

Non-goals

  • No unrestricted kill from the UI, in any role, in any phase.
  • No fleet-wide restart.
  • No silent auto-restart loop: every attempt is confirmed and audited.
  • This does not implement the Phase 1 health API (#634).