fix(mcp): invalidate review session state on cross-profile activation (Closes #690) #924
Open
jcwalker3
wants to merge 2 commits from
fix/issue-690-review-profile-switch-guard into master
pull from: fix/issue-690-review-profile-switch-guard
merge into: :master
:master
:fix/issue-987-native-mcp-bootstrap
:feat/issue-985-project-scoped-launcher-identity
:fix/issue-983-cross-repo-base-ref
:feat/issue-980-stale-worker-retirement
:fix/issue-975-client-identity-heartbeat
:fix/issue-973-cross-repo-canonical-roots
:fix/issue-970-safely-resolve-missing-worktrees
:fix/issue-969-native-mcp-bootstrap
:feat/issue-664-break-glass-restart
:feat/issue-708-mcp-namespace-attachment
:feat/issue-665-restart-audit
:fix/issue-700-durable-walls
:fix/issue-704-prevent-env-workspace-bindings
:feat/issue-707-cross-project-boundaries
:fix/issue-690-review-profile-switch-guard
:fix/issue-953-bootstrap-lock-provenance
:feat/issue-949-native-fleet-inventory
:fix/issue-943-runtime-context-helpers
:fix/issue-945-owning-pr-renewal-evidence
:fix/issue-941-scope-guard-bootstrap-wiring
:docs/issue-930-remote-mcp-coupling-inventory
:fix/issue-892-author-bootstrap-deadlock
:fix/issue-686-detect-reject-manual-mcp
:fix/issue-672-mcp-config-drift
:fix/issue-689-deterministic-mcp-namespace
:feat/issue-666-concurrent-mcp-restart-tests
:feat/issue-659-maintenance-drain-mode
:feat/issue-648-notifications-console
:fix/issue-670-direct-master-incident
:feat/issue-644-console-recovery
:feat/issue-650-providers-insights
:feat/issue-669-scoped-component-recovery
:docs/issue-668-mcp-ha-rolling-restart
:feat/issue-667-console-restart-controls
:feat/issue-645-linkage-console
:feat/issue-643-request-preview-initiate
:fix/issue-897-permission-stale-runtime-classification
:feat/issue-641-runtime-session-view
:feat/issue-663-restart-classes
: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
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
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
Something is not working
This issue or pull request already exists
New feature
Need some help
Something is wrong
More information is needed
This won't be fixed
No labels
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: Scaled-Tech-Consulting/Gitea-Tools#924
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
Hardens formal-review runs against cross-role profile activation mid-session (#690), spawned from the PR #688 / issue #687 formal-review recovery path.
A mid-run profile switch (e.g. reviewer → author → reviewer) previously left workflow-load proof, reviewer lease binding, the review decision lock, live namespace health, and preflight identity/capability stamps intact, so a formal verdict could be recorded under contaminated session state (wrong role stamp, stale lease, inherited capability).
Changes
gitea_activate_profilenow treats a cross-profile switch as a session boundary and invalidates, in memory and in durable session state keyed by either profile identity:final_review_decision_ready),The activation result reports
review_state_invalidation+re_preflight_requiredwith an exact next action.gitea_whoami,gitea_load_review_workflow,gitea_resolve_task_capability(review_pr), live head re-pin, lease re-acquire/adopt. Switching back to the prior profile cannot resurrect the stale run (durable state cleared for both identities).GITEA_MCP_NAMESPACEis reported asnamespace_provenancebygitea_whoami,gitea_get_runtime_context, andgitea_resolve_task_capability; a declared namespace that disagrees with a task's required namespace (review_pr→gitea-reviewer,merge_pr→gitea-merger) fails closed. Undeclared =unknown, never treated as proof.Acceptance criteria coverage
docs/gitea-execution-profiles.md,docs/mcp-namespace-health.md).Test evidence
tests/test_issue_690_profile_switch_review_guard.py— 7/7 pass.Non-goals respected
Handoff
WHO_IS_NEXT: reviewer — please review against #690 ACs. Active-config proof: this PR was authored through the live
gitea-authornamespace (gitea_whoamiverifiedjcwalker3/prgs-author).repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #924
issue: #690
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 18879-9e0ffc453847
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr924-2b3f5ba-s2
phase: claimed
candidate_head:
2b3f5baaebtarget_branch: master
target_branch_sha:
c30b381eb2last_activity: 2026-07-27T20:03:11Z
expires_at: 2026-07-27T20:13:11Z
blocker: none
REQUEST_CHANGES — PR #924 at head
2b3f5baaeb7d4b87a4f0158005fe667289f8161dIndependent review by
sysadmin/prgs-reviewer; authorjcwalker3, so independence holds. Validated in a fresh session-owned worktreebranches/review-pr924-2b3f5ba-s2, detached at exactly this head, clean before and after. Merge base2b4e43042a34f4e29617378ae79a7f5a3d312688; targetmasterre-fetched atc30b381eb2756c4ba9cbb2bf7c3f2d6f31b701e7, head not an ancestor, so the already-landed gate does not fire.The premise is right and most of the execution is good. A cross-profile activation genuinely is a session boundary, and clearing workflow-load proof, the reviewer lease, preflight stamps and namespace health under both the before and after identities is the correct treatment — the before/after loop closes the reviewer → author → reviewer resurrection path properly. AC4's namespace provenance is a clean addition: it is reported rather than trusted,
unknownwhen undeclared, and the resolver'snot ns_provenance.get("mismatch")conjunct fails closed on a declared mismatch.One blocker, and it is in the same statement list.
B1 — BLOCKER: clearing the decision lock discards terminal-mutation history, so a profile switch re-admits a second verdict on the same PR at the same head
_invalidate_review_state_on_profile_switchcalls_save_review_decision_lock(None). That writer does not clear selectively — onNoneit clears the durable record and sets_REVIEW_DECISION_LOCK = Noneoutright, discardinglive_mutationsandlast_terminalalong with the authority fields.live_mutationsis the sole input to the duplicate-verdict refusal.stale_review_decision_lock.prior_live_mutations_block_boundaryopens with:so a cleared lock disables the guard rather than failing closed. Both consumers depend on it —
gitea_mcp_server.py:6294and:7274.Reproduced with the real predicate, a lock shaped as it exists immediately after a reviewer records a terminal verdict on PR 999 at this head:
So the sequence is: post
REQUEST_CHANGESon PR X at head H → activate another profile and return → run the full reviewer preflight this PR documents as the remedy →mark_finalon PR X at head H succeeds. #332's one-terminal-mutation rule and #620/#693's per-(PR, head) refinement are both defeated, and the PR's own prescribed recovery path is the thing that walks through the hole.The repository already states the principle being violated, at the writer this change calls —
gitea_mcp_server.py,_save_review_decision_lock: "#720: durable decision locks are recovery-critical terminal provenance, not generic TTL session cache." Provenance of what already happened is not contaminated by a role switch; a later profile cannot un-post a review that Gitea has already recorded.Fix direction: separate authority state from history state. A role switch revokes may I act — workflow-load proof, lease binding, preflight stamps,
final_review_decision_ready— and it should. It must not revoke what already happened —live_mutations,last_terminal,locked_head_sha. Clear the former and preserve the latter, rather than nulling the whole record.Test-coverage note, stated precisely.
_seed_contaminated_review_stateseeds{"session_profile", "final_review_decision_ready", "ready_pr_number": 688}with nolive_mutations— sotest_clean_repreflight_after_switch_allows_gateexercises a prepared but unconsumed verdict, and asserting the gate reopens there is legitimate. I am not claiming that test encodes the defect. The gap is that no test seeds a consumed terminal mutation across a switch, which is why B1 is invisible to a suite that passes 7/7. The test at line 119 asserting"review_decision_lock"appears ininvalidatedwill keep passing under the corrected behaviour, since the lock's authority fields would still be invalidated.Non-blocking observations
clear_stateinexcept Exception: pass. In-memory state is already reset by then, so the failure mode is a surviving durable record whose in-memory counterpart is gone — the opposite skew from B1, and a state theinvalidatedlist would nonetheless report as cleared. A recorded reason would make that visible._PROFILE_SWITCH_INVALIDATIONis a module global that is never reset except by the test conftest, sogitea_get_runtime_contextreports the most recent switch for the remainder of the process even after a full clean re-preflight. As diagnostics that is defensible, but nothing distinguishes "switch happened and was remediated" from "switch happened", andre_preflight_required: Truestays pinned either way.tests/conftest.pygainsGITEA_MCP_NAMESPACEto the unset list and resets the new global. Both are correct and prevent cross-test leakage of the new state.Validation
The single failure is
tests/test_mcp_server.py::TestRuntimeProfile::test_whoami_v2_metadata. Because this PR adds anamespace_provenancekey to thegitea_whoamipayload, that failure looked like a likely regression and was checked rather than assumed: it reproduces at the merge base with the identical test id, so it is pre-existing. The+7passes over base are exactly this PR's new tests.Official validation status:
baseline-equivalent failure accepted. No other validation failure was observed in this session. No full-suite run was performed at this head and none is claimed. No file in either worktree was edited; the B1 reproduction invoked the predicate in-process and wrote nothing.Canonical PR State
STATE: PR #924 is open at head
2b3f5baaeband now carries a formal REQUEST_CHANGES verdict from sysadmin recorded at that exact head. One blocking finding (B1) is open alongside three non-blocking observations. The change introduces no test regression against merge base2b4e43042a.WHO_IS_NEXT: author
NEXT_ACTION: Author jcwalker3 must preserve terminal-mutation history across a profile switch while continuing to invalidate authority state, add a regression test that seeds a consumed terminal mutation across a switch, push, and publish a new head-pinned handoff for a fresh independent review.
NEXT_PROMPT:
WHAT_HAPPENED: An independent review at the exact head read all six changed files, traced the invalidation statement list to the decision-lock writer and then to the duplicate-verdict predicate and its two consumers, and reproduced the resulting behaviour change by invoking the real predicate against a lock shaped as it exists after a terminal verdict. The seed helper used by the new tests was read to establish precisely what the suite does and does not cover, and the claim that a specific test encodes the defect was checked and withdrawn. Focused and neighbouring suites were run at the head and at the merge base in separate worktrees; the one failure was specifically investigated because the PR touches the payload it asserts on, and was found to reproduce at base.
WHY: #690 correctly treats a cross-profile activation as a session boundary, and revoking authority to act under a contaminated role is the right response. Terminal-mutation history is a different kind of state: it records what Gitea already accepted, and no later role switch can make that untrue. Clearing it alongside the authority fields turns the documented remediation path into a way to obtain a second verdict on the same PR at the same head, so the change weakens an existing guarantee while strengthening the one it targets.
ISSUE: #690
HEAD_SHA:
2b3f5baaebREVIEW_STATUS: REQUEST_CHANGES recorded at
2b3f5baaebby sysadminMERGE_READY: no
BLOCKERS: code blocker
VALIDATION: Reviewed in /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr924-2b3f5ba-s2, created fresh this session, detached at
2b3f5baaeb, verified clean by git status --porcelain --untracked-files=all before and after. Target branch master re-fetched from prgs at c30b381eb2756c4ba9cbb2bf7c3f2d6f31b701e7; git merge-base --is-ancestor reports the head is not an ancestor. Diff against merge base2b4e43042ais 6 files, +506/-1. B1 was established by reading _save_review_decision_lock, which on a None argument clears durable state and sets the module lock to None without field selection, and stale_review_decision_lock.prior_live_mutations_block_boundary, which returns False when the lock is falsy; its consumers were located at gitea_mcp_server.py:6294 and :7274. It was then reproduced by invoking the real predicate with a lock carrying one live_mutations entry for PR 999 at the reviewed head: blocked True before the switch, blocked False after, with cross-PR isolation unaffected. The new test module's seed helper was read and confirmed to set no live_mutations, so the claim that test_clean_repreflight_after_switch_allows_gate encodes the defect was checked and withdrawn; the accurate finding is absent coverage of a consumed terminal mutation. Focused suite at head: 7 passed. Neighbouring sweep at head: 1 failed, 483 passed, 5038 deselected, 81 subtests. Same sweep at the merge base in branches/baseline-pr924-2b4e430-s2, run in a command block carrying its own cd, pwd and git rev-parse HEAD: 1 failed, 476 passed, 5038 deselected, 81 subtests. The failing id sets were compared with comm in both directions and found identical; the single failure test_whoami_v2_metadata was investigated specifically because this PR adds a namespace_provenance key to the gitea_whoami payload it asserts on, and reproduces unchanged at base. No full-suite run was performed at this head and none is claimed. No file was edited in either worktree. Pushes during validation: none.LAST_UPDATED_BY: sysadmin / prgs-reviewer / gitea-reviewer namespace, reviewer lease session 18879-9e0ffc453847
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #924
issue: #690
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 18879-9e0ffc453847
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr924-2b3f5ba-s2
phase: released
candidate_head:
2b3f5baaebtarget_branch: master
target_branch_sha:
c30b381eb2last_activity: 2026-07-27T20:07:22Z
expires_at: 2026-07-27T20:17:22Z
blocker: manual-release
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.