feat: enforce MCP restart class permissions #886
Open
jcwalker3
wants to merge 6 commits from
feat/issue-663-restart-classes into master
pull from: feat/issue-663-restart-classes
merge into: :master
:master
:feat/issue-663-restart-classes
:feat/issue-643-request-preview-initiate
:fix/issue-897-permission-stale-runtime-classification
:feat/issue-641-runtime-session-view
:feat/issue-661-drain-proof-hard-gate
:fix/issue-854-semantic-container-exclusion
:issue-640
:fix/issue-682-starlette-httpx2
Labels
Clear labels
allocator
anti-stomp
architecture
bug
chore
codex
concurrency
contamination
control-plane
dashboard
database
design
documentation
enhancement
gitea
glitchtip
important
incident
incident-bridge
integration
jenkins
labels
leases
mcp
mcp-health
mcp-menu
multi-project
mutating
nice-to-have
observability
portability
preflight
protected-branch
queue
read-only
reconnect
recovery
refactor
release
reliability
resumable-review
reviewer
roadmap
safety
security
self-hosted
sentry
stale-runtime
status:blocked
status:in-progress
status:pr-open
status:ready
terminal-lock
testing
tracker
type:bug
type:feature
type:feature
type:guardrail
visibility
workflow
workflow-hardening
workflow-hardening
Controller-owned work allocator
Prevent concurrent LLM session stomping
Architecture / structural design
OpenAI Codex client / workflow session surface
Concurrent session safety
Workflow or session contamination incident
MCP control-plane coordination and allocation authority
MCP operational dashboard/queue view
Internal coordination storage (SQLite/Postgres)
Design / investigation, no implementation
Docs / runbooks
New feature or improvement
Gitea MCP workflow
GlitchTip integration
Operational or process incident requiring durable audit trail
Sentry-to-Gitea incident bridging
Integration testing
Jenkins integration
Label taxonomy management
Lease adopt/release/expire lifecycle
MCP server / tooling
MCP namespace and runtime health
MCP menu surface
Work spanning multiple monitoring projects or Gitea repos
Mutating action; requires gating
Observability, metrics, traces, error reporting
Cross-platform / portability
Shared preflight gates before mutation
Protected branch / stable-branch policy concern
Work queue visibility and allocation
Read-only, no mutation
MCP client reconnect/reload recovery path
Recovery paths for stale/foreign leases
Code refactor / restructure
Release / versioning
Reliability / failure handling
Persist and resume prepared review verdicts across sessions
Reviewer workflow tooling
Roadmap / umbrella issue
Safety rails and fail-closed mutation guards
Security / trust boundary
Self-hosted infrastructure integration
Sentry error monitoring integration
Stale backend daemon / runtime-vs-master parity failures
Issue is blocked
Issue is being worked on
Issue has an open pull request
Issue is ready for work
Terminal review lock (#332) path
Tests / test coverage
Issue tracker hygiene / meta
Bug or defect
Feature or enhancement
Feature or enhancement
Safety gate or guardrail
Workflow state visibility for LLMs/operators
Cross-tool workflow
LLM workflow coordination hardening
LLM workflow coordination hardening
No labels
Milestone
No items
No Milestone
Projects
Clear projects
No projects
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: Scaled-Tech-Consulting/Gitea-Tools#886
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Validation
masterbaseline at870843f999fb4bb8fc8c15ff7ede730448cdb3d4: 5099 passed, 6 skipped, 802 subtests, the same 23 tests failedgit diff --check: passedScope and risk
The MCP restart request remains analysis-only; this change adds policy enforcement and audit metadata but does not add a process-kill or restart execution path. The principal risk is policy routing, covered by the nine-class permission and drain matrix tests plus existing coordinator, governance, Web UI, reconciliation, and preflight tests.
Closes #663
Canonical Issue State
STATE:
needs-review
WHO_IS_NEXT:
reviewer
NEXT_ACTION:
Review PR #886 at exact head
714190e02aagainst issue #663 and post a formal current-head review verdict.NEXT_PROMPT:
Load the canonical gitea-workflow skill first. In prgs/Scaled-Tech-Consulting/Gitea-Tools, use a sanctioned reviewer identity distinct from jcwalker3/prgs-author. Recover PR #886 for issue #663, verify its live head is
714190e02a, inspect all acceptance criteria and validation evidence, run proportionate reviewer validation, and post a formal review verdict at the current head. Do not merge and do not operate as the author.WHAT_HAPPENED:
The author implemented issue #663, committed and published the existing feature branch, and opened PR #886 targeting master.
WHY:
All issue acceptance criteria are implemented and the feature-specific validation passes; the remaining full-suite failures reproduce with the same signatures on the clean master baseline.
RELATED_DISCUSSION:
#663
RELATED_PRS:
BRANCH:
feat/issue-663-restart-classes
HEAD_SHA:
714190e02aVALIDATION:
Focused restart/governance suite: 122 passed, 16 subtests. Full branch suite: 5110 passed, 6 skipped, 802 subtests, 23 failed. Clean master baseline: 5099 passed, 6 skipped, 802 subtests, the same exact 23 failures. Eleven new tests pass. git diff --check passed. PR read-back confirms open, mergeable, base master, author jcwalker3, and the pinned current head.
BLOCKERS:
none
LAST_UPDATED_BY:
jcwalker3 / prgs-author / 2026-07-24T22:22:03Z
Canonical Handoff
Formal Review Report: APPROVED
STATE: approved
WHO_IS_NEXT: merger
NEXT_ACTION: merger lease acquisition and landing on master
Verification
docs/mcp-restart-classes.md(+53),docs/mcp-restart-coordinator.md(+14),gitea_mcp_server.py(+30/-6),restart_coordinator.py(+384/-13),tests/test_restart_classes.py(+232),webui/sanctioned_restart.py(+20/-2),tests/test_webui_sanctioned_restart.py(+6).pytest tests/test_restart_classes.py tests/test_webui_sanctioned_restart.py tests/test_restart_coordinator.py-> 67 passed, 12 subtestsrestart_classacross coordinator and Web UI ledgers.pr: #886
branch: feat/issue-663-restart-classes
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/issue-663-restart-classes
profile: prgs-author
session_id: unknown
phase: claimed
head_before:
930dc24632expires_at: 2026-07-25T07:17:24Z
reviewer_active: no
Conflict remediation complete — PR #886 no longer carries conflicts
Author-role conflict remediation for PR #886 (
feat/issue-663-restart-classes), performed in the bound worktree with no rebase and no force-push.Conflict-fix lease release
What conflicted
The branch was 8 commits behind
master. Exactly one file conflicted —gitea_mcp_server.py— in two adjacent hunks, both inside thegitea_request_mcp_restarttool. Both sides were purely additive to the same function:restart_class,target_session_id,target_role,target_connector; payload keyscontroller_approval_authorized,requester_role,requester_permissionsdrain_proof_json,request_break_glass; the comment describing the apply-path hard gate and break-glass authorizationHow it was resolved
Union of both sides, in full. Nothing was dropped, reordered, or reimplemented.
The union is not a stylistic choice — it is the only coherent resolution, because the auto-merge output for the function body already consumes every parameter from both sides.
restart_classand the threetarget_*arguments flow into therestart_coordinatorcall;drain_proof_jsonandrequest_break_glassdrive thedry_run=Falsehard gate and the break-glass branch. Dropping either side's parameters would leave the combined body referencing undefined names.Verification evidence
grep -rn '^<<<<<<< \|^>>>>>>> ' --include='*.py'returns empty).python -m py_compile gitea_mcp_server.pypasses.master@2f4dec832327513118f2fe92b74da25d124a01cbis now an ancestor of the branch head;commits_behindis0.930dc24..9bc021e, non-force. The prior head930dc246remains an ancestor.gitea_assess_pr_sync_status:mergeable: true,has_conflicts: false,commits_behind: 0.Review status — a fresh independent review is required
The prior review verdict was recorded against
930dc24632ceb87f88241020ee4bf78ea39b6d16and does not carry to the new head.gitea_assess_pr_sync_statusreportsapproval_at_current_head: false,stale_approval: true,approval_valid_for_merge: false,recommended_next_action: fresh_review_required.Canonical Issue State
STATE: awaiting-review
WHO_IS_NEXT: reviewer
NEXT_ACTION: Perform a fresh independent review of PR #886 at exact head
9bc021e9c0e7212d976d6448ba873eb3bb775a75, then record a verdict at that exact head.BLOCKERS: none
VALIDATION: focused suite over the combined tree — 165 passed, 68 subtests passed;
py_compilepasses; zero conflict markers; live statemergeable: true,has_conflicts: false,commits_behind: 0.LAST_UPDATED_BY: prgs-author (jcwalker3)
WHAT_HAPPENED: PR #886 stopped being mergeable after
masteradvanced 8 commits.master@2f4dec8323was integrated intofeat/issue-663-restart-classesvia a git merge commit inside the bound worktree, the single conflicted filegitea_mcp_server.pywas resolved as a full union of both sides, and the result was pushed as fast-forward930dc24..9bc021e.WHY: #663 (restart classes) and #661 (drain-proof hard gate, via PR #882) both extended the same tool
gitea_request_mcp_restartwith new parameters and new payload keys. Git could not order the two additive edits to the same parameter list, so it raised a content conflict even though the two changes are independent and non-overlapping in behaviour.RELATED_PRS: #886, #882
NEXT_PROMPT:
[THREAD STATE LEDGER]
What is true now
9bc021e9c0e7212d976d6448ba873eb3bb775a75; Gitea reportsmergeable: true,has_conflicts: false,commits_behind: 0;approval_at_current_head: falseandapproval_valid_for_merge: false.py_compilepasses; zero conflict markers remain.What changed
master@2f4dec832327513118f2fe92b74da25d124a01cbwas integrated intofeat/issue-663-restart-classesvia a git merge commit9bc021e9c0e7212d976d6448ba873eb3bb775a75with parents930dc24and2f4dec8.gitea_mcp_server.pywas resolved as a full union of both sides insidegitea_request_mcp_restart; no other file conflicted.930dc24..9bc021eas a fast-forward, non-force. No rebase occurred.930dc246no longer applies to the current head.What is blocked
Who/what acts next
9bc021e9c0e7212d976d6448ba873eb3bb775a75and record a verdict at that exact head.930dc24632ceb87f88241020ee4bf78ea39b6d16for merge eligibility; do not rebase or force-push this branch; do not merge from the author or reviewer role.repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #886
issue: #663
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: reviewer-886-9bc021e-fresh
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr886-head-9bc021e
phase: claimed
candidate_head:
9bc021e9c0target_branch: master
target_branch_sha:
2f4dec8323last_activity: 2026-07-25T06:05:48Z
expires_at: 2026-07-25T06:15:48Z
blocker: none
Formal Review Findings — REQUEST_CHANGES (verdict not recordable via the review API at this moment)
PR #886 · issue #663 · head
9bc021e9c0e7212d976d6448ba873eb3bb775a75· basemaster@2f4dec832327513118f2fe92b74da25d124a01cbIndependent re-review at the exact current head. Review
578(sysadmin, APPROVE verdict) was recorded at714190e02a89636aa515320cddef9437d46dbc37;approval_at_current_head: false, so it was not reused and carries no weight here.Union verification — the
9bc021econflict resolution9bc021ehas parents930dc246(#663 branch) and2f4dec83(master); merge base7af40fb5. One conflicted file,gitea_mcp_server.py, insidegitea_request_mcp_restart.Both directions verified by patch comparison:
diff(master → 9bc021e)on that file reproduces the #663 side exactly: the four new parameters (restart_class,target_session_id,target_role,target_connector), therequester_rolederivation,controller_approved,requester_permissions, all eight coordinator kwargs, and the three payload keys (controller_approval_authorized,requester_role,requester_permissions).diff(930dc246 → 9bc021e)on that file reproduces the #661 side exactly: thedrain_proofimport,drain_proof_json/request_break_glass, the rewritten docstring, the hard-gate comment, and the wholeif not dry_run:gate body.Stronger result: every added/removed line of the #663 delta is identical between the previously reviewed
714190eand the current head — 739 changed lines, zero differences. The union dropped no behaviour from either side.The #661 hard gate was re-verified at this head by direct probe. Apply requests deny on: absent proof (with
restart_drain_gate_deniedincident), unclean proof, tampered proof (signature mismatch), expired proof, and stale impact fingerprint. Break-glass authorization stays environment-derived and is never self-asserted —break_glass = request_break_glass and break_glass_authorized, the latter read only fromGITEA_BREAKGLASS_RESTART_AUTHORIZATION.The 15 commits
714190e..930dc246These carry no net new content. Only 3 of the 15 are not already ancestors of master, and all 3 are catch-up
Merge branch 'master' into feat/issue-663-restart-classescommits. All 6 non-merge commits —0a78da3(#854),5deb66candc74b8da(#682),069a9af,1948d3d,dac40ab(#640) — are already ancestors of master, having landed there via PRs #883, #884 and #885. The effective diff of this PR against master is exactly the seven #663 files.Independent baseline (author figures not used)
Both runs performed in
branches/worktrees; a/tmpcheckout produces phantom path-resolution failures and is not comparable.2f4dec839bc021eIdentical 23-test failure set on both sides; +11 passing, no new failure signature. Focused restart/drain suites: base 117 passed / 68 subtests → head 128 passed / 68 subtests.
AC verification (independent probe, not the branch's own assertions)
rolling_mcp_restart,full_mcp_restartandhost_restartdeny forauthor/reviewer/merger/reconcilerand allow forcontroller/operator/admin.verdict=unsafe,allow_restart=False, reasonunknown restart class ...; deny (fail closed).worker_restart,role_runtime_restart,connector_restarteach reporttarget required for <class>.client_reconnectandsession_reconnectstayverdict=safewhilefull_mcp_restartgoesunsafe. Narrower classes do not inherit a full-runtime drain.B1 — blocker —
apply_authorizeddoes not consult restart-class authorizationgitea_request_mcp_restartcomputes the apply decision purely from the drain gate:gate.allowis a function of the drain proof (or break-glass) only. It never readsreport.allow_restart,permission_authorized,role_authorizedorapproval_satisfied. The two gates introduced by #663 and #661 are therefore disjoint rather than conjoined, and the merge is the first point at which they coexist.Reproduced against this exact head. An
authorrole requestingfull_mcp_restartis denied by the class matrix:A drain proof minted against that same denied report is clean, because
_evaluate_checksderives from mutations, critical sections and disruptive leases and never inspects the authorization outcome. The gate then returns:The payload simultaneously states
allow_restart: Falsewith an explicit "role 'author' may not request full_mcp_restart" reason andapply_authorized: True. In a change whose purpose is least-privilege restart authorization, that is a self-contradictory authorization contract, andapply_authorizedis the field a later execution child is documented to consume.Reachability today is limited but not nil. No production caller mints proofs —
build_drain_proofhas no non-test caller and the signing secret is per-processos.urandom(32), so a proof cannot be forged through MCP. The break-glass path, however, needs no proof: withGITEA_BREAKGLASS_RESTART_AUTHORIZATIONpresent, any role callingrestart_class="host_restart", request_break_glass=True, dry_run=Falseobtainsapply_authorized: Truewhile the matrix denies the class. One environment variable collapses the whole nine-class matrix for the apply decision, including the class whose policy demandscontroller_approval_plus_infrastructure_operator.Nothing restarts today (
apply_supportedis alwaysFalseand no executor exists), so this is a latent contract defect rather than a live escalation — but it should not ship in this shape.Suggested fix, in
gitea_request_mcp_restart:and surface the class-authorization reasons in
apply_gatewhen they are what denies, so a denial is attributable.B2 — blocker —
docs/mcp-restart-coordinator.mdnow describes pre-#661 behaviourThe merge left the coordinator document describing the tool as it existed before the drain gate arrived.
drain_proof_jsonandrequest_break_glass. Neither parameter appears anywhere indocs/mcp-restart-coordinator.mdordocs/mcp-restart-classes.md(0 occurrences in each).dry_run=False"performs no restart and reports that apply is gated by a drain proof (a separate child)". At this headdry_run=Falseevaluates the hard gate inside this tool and returnsapply_gate,apply_authorized,break_glass_requested,break_glass_authorizedand, on denial,incident.A safety document asserting there is no gate where a gate now exists will mislead the next author. Both statements need updating to the post-union behaviour.
Non-blocking observations
configuration_reloadscopes sessions and leases out of its impact set but the terminal lock is deliberately retained (terminal_lock_in_scopeexcludes onlyclient_reconnectandsession_reconnect). The result isblast_radius=nonetogether withverdict=unsafewhenever a terminal lock exists. This is conservative and safe, but the pairing reads oddly in an audit record; worth an explicit comment stating the intent.permissions_for_rolegrants all four worker roles the request permission forworker_restart,role_runtime_restart,connector_restartandconfiguration_reload. That matches the documented table and execution stays restricted tooperator/admin, so it is by design and not a finding — noted only so the breadth is visible on the record.What is good here
The class policy table, the fail-closed handling of unknown classes and missing targets, the target-scoping that keeps narrow classes off a full-runtime drain, and the audit-record class field are all correct and well covered.
restart_coordinator.pyreads cleanly and the 11 added tests are meaningful. B1 and B2 are both small, contained changes.[THREAD STATE LEDGER]
what is true now:
9bc021e9c0e7212d976d6448ba873eb3bb775a75. Review578is stale —approval_at_current_head: false,recommended_next_action: fresh_review_required. No review verdict exists at the current head, so this PR is not eligible for landing.session_id: reviewer-886-9bc021e-fresh(claim comment16554) and released immediately after this record is posted.what changed:
9bc021ewas completed against base2f4dec83: union verified in both directions, #661 hard-gate denials re-proven, nine-class matrix and fail-closed paths re-proven, and an independent full-suite baseline measured on both trees.branches/review-pr886-base-2f4dec83andbranches/review-pr886-head-9bc021e.what is blocked:
gitea_mark_final_review_decision(pr_number=886, action=request_changes)fail-closes with "active conflict-fix lease on PR #886 (phase=claimed); reviewer mark_ready blocked". The holding lease is the author conflict-fix claim in comment16508(profile: prgs-author,head_before: 930dc246,head_after: null,expires_at: 2026-07-25T07:17:24Z). It is comment-only, so it is not releasable throughgitea_release_workflow_leaseand not releasable by a reviewer session at all.gitea_diagnose_review_decision_lockreportedclassification: foreign_pr_terminalwithmark_final_allowed: truefor the foreign terminal on PR #898, and the refusal that actually occurred cited the conflict-fix lease instead.who/what acts next:
prgs-author) that created claim comment16508.PHASE: released,HEAD_AFTER: 9bc021e9c0e7212d976d6448ba873eb3bb775a75and the claimcomment_id: 16508; or allow the TTL to lapse at2026-07-25T07:17:24Z. Then remediate B1 and B2 and hand back to a reviewer at the new head.578— it was recorded at714190eand is void at the current head. Do not treatapply_authorized: Trueas authorization while B1 stands. Do not release the conflict-fix lease from a reviewer session.Canonical Issue State
STATE: changes-requested
WHO_IS_NEXT: author
NEXT_ACTION: Release the stale conflict-fix lease from comment 16508, then fix B1 (conjoin apply_authorized with restart-class authorization) and B2 (update docs/mcp-restart-coordinator.md to the post-union behaviour)
BLOCKERS: B1 apply_authorized ignores restart-class authorization; B2 coordinator document describes pre-#661 behaviour; queue/lease blocker preventing the review API verdict from being recorded
VALIDATION: base
2f4dec83= 23 failed / 5190 passed / 6 skipped / 867 subtests; head9bc021e= 23 failed / 5201 passed / 6 skipped / 867 subtests; identical failure set, +11 passing; focused restart-drain suites 117 -> 128 passedRELATED_PRS: #886 (this PR), #882 (#661 drain proof, landed on master), #883, #884, #885 (sources of the catch-up commits in 714190e..930dc246)
WHAT_HAPPENED: An independent reviewer session re-reviewed head
9bc021eagainst base2f4dec83without reusing review 578. The union merge was proven complete in both directions and the #663 delta proven byte-identical to the previously reviewed delta. Two blockers were found, one of them an emergent property of combining #663 with #661.WHY: The prior approval was recorded at
714190eand is void at the current head, and the merge commit9bc021eplus the intervening commits carried no valid review.LAST_UPDATED_BY: prgs-reviewer (sysadmin), session reviewer-886-9bc021e-fresh
NEXT_PROMPT:
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #886
issue: #663
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: reviewer-886-9bc021e-fresh
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr886-head-9bc021e
phase: released
candidate_head:
9bc021e9c0target_branch: master
target_branch_sha:
2f4dec8323last_activity: 2026-07-25T06:10:24Z
expires_at: 2026-07-25T06:20:24Z
blocker: manual-release
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.