fix(session): pin immutable MCP context; ban cross-host profile substitution (Closes #714) #715
Open
jcwalker3
wants to merge 3 commits from
fix/issue-714-session-context-immutability into master
pull from: fix/issue-714-session-context-immutability
merge into: :master
:master
:fix/issue-714-session-context-immutability
:fix/issue-709-decision-lock-cross-profile
:fix/issue-695-native-transport-quarantine
:fix/issue-698-report-validator-schema
:fix/issue-702-stale-binding-lease-recovery
:fix/issue-699-structured-auth-mcp-errors
:fix/issue-695-native-quarantine-v2
:fix/issue-693-review-decision-lock-recovery
:fix/issue-691-obsolete-reviewer-lease-cleanup
:feat/issue-687-reconciler-branch-delete
:fix/issue-683-workflow-guard-hardening
:chore/issue-681-preserve-review-session-wip
:feat/issue-604-anti-stomp-preflight
:feat/issue-606-sentry-observability
:fix/issue-671-block-stable-branch-push
:fix/issue-675-residual-preflight-remediation
:fix/issue-673-remediate-regressions-part2
:fix/issue-673-remediate-regressions
:feat/issue-603-lifecycle-labels
:fix/issue-627-set-issue-labels-pagination
:feat/issue-601-first-class-leases
:feat/issue-612-incident-bridge
:feat/issue-600-controller-allocator-api
:fix/issue-620-head-scoped-review-locks
:feat/issue-613-allocator-db-substrate
:docs/mcp-stable-control-runtime-policy
:feat/issue-609-prepared-review-verdict-resume
:feat/issue-610-live-remote-parity
:feat/issue-503-reviewer-active-worktree
:feat/issue-470-preflight-contract
:feat/issue-440-lock-recovery
:feat/issue-440-branch-recovery
:feat/issue-458-queue-fail-closed-copy
:feat/issue-400-early-duplicate-work-gate
:feat/issue-308-reconcile-inventory-pagination
:feat/issue-308-reconcile-pagination-proof
:docs/issue-261-agent-temp-artifact-cleanup
:feat/issue-262-map-commit-files
:fix/infra-stop-conflict-marker-false-positive
:feat/issue-139-role-aware-task-routing
:feat/issue-188-continuation-selection-wall
:feat/issue-189-continuation-mode-proofs
:feat/issue-232-refresh-wiki-proof-heads
:feat/issue-210-block-workspace-edits
:feat/issue-204-exact-issue-lock
:docs/issue-80-label-taxonomy
:docs/issue-79-safety-boundary-updates
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#715
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
Closes #714.
Makes the MCP session profile, remote, host, repository, namespace, and authenticated identity immutable (or fail closed on drift). Capability resolution and mutation gates no longer silently substitute a more privileged or cross-host profile (for example
mdcps-reviewerbecomingprgs-author).Root cause
_ensure_matching_profileand_try_auto_switch_for_operationauto-activated any configured profile that held the required permission, without regard to remote or host. After pinningmdcps-reviewerfordadeschools, resolvingcomment_issueswitched the process toprgs-author.Changes
session_context_binding.py: bind, seed, and assess session context; filter profiles by remote host; identity expected-versus-actual checks.gitea_activate_profilere-binds the session context.whoami, runtime context, and capability resolver report the same context proof.Tests
tests/test_issue_714_session_context_immutability.pyreproduces the pin then drift then resolver sequence and remaining acceptance criteria.tests/test_operation_scoped_roles.pyfor fail-closed (no auto-switch); explicit activate still works.Dependent work
Verification
master(pre-existing untracked only).branches/fix-issue-714-session-context-immutability.Do not self-review or merge.
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #715
issue: none
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 54162-69761428cb3b
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr-715-drift-fix
phase: claimed
candidate_head: none
target_branch: master
target_branch_sha: none
last_activity: 2026-07-15T06:01:30Z
expires_at: 2026-07-15T08:01:30Z
blocker: none
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #715
issue: none
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 54162-69761428cb3b
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr-715-drift-fix
phase: released
candidate_head: none
target_branch: master
target_branch_sha: none
last_activity: 2026-07-15T06:13:58Z
expires_at: 2026-07-15T08:13:58Z
blocker: manual-release
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #715
issue: none
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 54162-5bf29bfb7884
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr-715-drift-fix
phase: claimed
candidate_head: none
target_branch: master
target_branch_sha: none
last_activity: 2026-07-15T06:14:11Z
expires_at: 2026-07-15T08:14:11Z
blocker: none
Canonical PR State
STATE: OPEN
WHO_IS_NEXT: author
NEXT_ACTION: Fix test pollution in session context tests
NEXT_PROMPT:
WHAT_HAPPENED: The PR was reviewed and the core drift-protection logic works successfully (failed closed properly in manual cross-host mutation testing). However, the automated test suite fails due to context leakage across test boundaries.
WHY:
_SESSION_CONTEXTis not cleared between tests in several classes (e.g.,TestResolveTaskCapability,TestReviewerAuthorMutationBlocks).ISSUE: #714
HEAD_SHA:
632c568865REVIEW_STATUS: CHANGES_REQUESTED
MERGE_READY: false
BLOCKERS: Test pollution breaking the automated test suite.
VALIDATION: Manual testing of cross-host mutation bypass successfully blocked.
pytest tests/fails with 41 errors.LAST_UPDATED_BY: sysadmin
Review Details
The implementation correctly prevents cross-host profile drift and enforces session context immutability. The manual testing confirms that mutation attempts with a cross-host or mismatched profile now fail closed rather than silently substituting a different profile (e.g.,
prgs-reviewerattempting to mutatedadeschoolsis correctly blocked and does not switch tomdcps-reviewer).However, the PR introduces significant test pollution that breaks the broader test suite. The
session_context_binding.pymodule uses a global_SESSION_CONTEXTdictionary. WhileTestOperationScopedRolescorrectly callssession_ctx.clear_session_context()insetUp()andtearDown(), this was omitted in several other test classes. As a result, the bound context bleeds across tests in the same process, causing false positive "drift block" failures in tests that change profiles or identities.This causes 41 test failures across the suite, including in:
TestResolveTaskCapabilityTestReviewerAuthorMutationBlocksTestCreateIssueTestCreatePRPlease add
session_ctx.clear_session_context()to thesetUp()/tearDown()of a base test class or ensure it is cleared in all relevant test classes so that the test suite passes reliably.Canonical Issue State
STATE: OPEN
WHO_IS_NEXT: reviewer
NEXT_ACTION: Independently re-review PR #715 at head
29ffe1407f.NEXT_PROMPT:
WHAT_HAPPENED: The author remediated review #437 on the existing PR branch, committed
29ffe1407f, and pushed normally without force.WHY: Pytest reused the process-local session binding across independent tests, and the mutable check-then-set dictionary allowed competing first calls to overwrite the established context.
RELATED_PRS: #715
BLOCKERS: Formal review #437 remains REQUEST_CHANGES pending independent re-review; it was not dismissed or rewritten.
VALIDATION: Issue #714 plus operation-scoped roles: 23 passed. Previously failing modules: 333 passed. Isolation tests: 2 passed in forward order and 2 passed in reverse order. Full suite
python3 -m pytest tests/: 2710 passed, 6 skipped, 1 warning in 30.83s.LAST_UPDATED_BY: jcwalker3 / prgs-author
Lifecycle and isolation design
gitea_activate_profileis the sanctioned logical-session transition.finallyblock after it, so failures and teardown exceptions cannot pollute later tests. Ad hoc class cleanup was removed.RLock; first bind and sanctioned rebind are atomic, snapshots are detached, and parallel/interleaved seed calls cannot overwrite an established binding.The only warning is the existing Starlette/httpx deprecation in
tests/test_webui_audit.py. The author did not self-review or merge.Canonical Issue State
ISSUE: #714
PR: #715
STATE: OPEN
WHO_IS_NEXT: reviewer
NEXT_ACTION: Independent prgs-reviewer re-review of PR #715 at head
d936da5c87NEXT_PROMPT:
WHAT_HAPPENED: Author remediated the test-context contamination cited by review 437 and pushed two commits.
29ffe14centralized_SESSION_CONTEXTisolation at the test boundary;d936da5adds three tests closing coverage gaps found while auditing the required session-lifecycle properties. The full suite now reports zero failures where review 437 measured 41.WHY:
session_context_binding._SESSION_CONTEXTis a process-global binding that onlyTestOperationScopedRolesreset, so one test's binding stayed live for later tests in the same process; tests that legitimately change profile or identity were then measured against a stale predecessor binding and rejected as drift. The drift protection was correct — the test boundary was not re-establishing a clean session.RELATED_PRS: #715 (this PR, Closes #714); #711 is the dependent follow-on (MDCPS close path, worktree_path, reconciler routing) and remains outstanding; MDCPS reconciliation stays prohibited until #714 and #711 are both landed and verified. PR #710 is out of scope and untouched.
HEAD_SHA:
d936da5c87PRIOR_HEAD_SHA:
29ffe1407fREVIEWED_HEAD_SHA:
632c568865REVIEW_STATUS: CHANGES_REQUESTED
MERGE_READY: false
BLOCKERS: none
VALIDATION: Full suite 2713 pass, 6 skip, 1 pre-existing warning, 161 subtests in 23.05s
LAST_UPDATED_BY: jcwalker3
[THREAD STATE LEDGER]
What is true now
Server-side decision state: PR #715 carries one REQUEST_CHANGES review verdict (review_id 437) recorded via the review API by sysadmin at head
632c5688. It is undismissed and stale against the current head; approval_at_current_head is false and no APPROVE verdict exists for any head.Local verdict/state: Author remediation is complete at head
d936da5c872fb4bad31536d80b7c5f57462b05a9. This session holds theprgs-authorprofile, which forbids gitea.pr.approve, gitea.pr.merge, and gitea.pr.request_changes; no review verdict was prepared or posted from this session.What changed
Two pushes have landed on
fix/issue-714-session-context-immutabilitysince review 437 was recorded:29ffe14— centralized test-boundary isolation of_SESSION_CONTEXT(resolves the contamination review 437 identifies).d936da5— this session: three additional tests closing coverage gaps found while auditing the required session-lifecycle properties. Tests only; no production code changed.Server-side mutation ledger: 1 commit (
d936da5), 1 non-force push (29ffe14..d936da5, fast-forward), 1 issue comment. No review verdict, no merge, no label change, no issue state change.What is blocked
Blocker classification: no blocker
The 41 suite failures cited by review 437 no longer reproduce at the current head. The full suite reports zero failures and zero errors. The one remaining warning is a pre-existing
StarletteDeprecationWarningintests/test_webui_audit.py(httpx/starlette testclient), unrelated to Issue #714 and present independently of this branch.Who/what acts next
Next actor: prgs-reviewer
Required action: Independent re-review of PR #715 at head
d936da5c872fb4bad31536d80b7c5f57462b05a9, against Issue #714 acceptance criteria 1-8. Review 437 predates two pushes and does not describe this head.Do not do: Do not treat review 437 as current. Do not re-review from the author session or the control checkout. Do not merge on the strength of this comment — it records author remediation only, not a review outcome. Do not include PR #710 or any eAgenda scope.
[/THREAD STATE LEDGER]
Author remediation — Issue #714 / PR #715
Cause of the contamination
session_context_binding._SESSION_CONTEXTis a process-global binding. Review 437 (head632c5688) found it leaking across test boundaries: onlyTestOperationScopedRolesreset it, so a binding established by one test stayed live for every later test in the same process. Tests that legitimately change profile or identity were then measured against a stale predecessor binding and rejected as drift — 41 false-positive "drift block" failures. The drift protection itself was correct; the fault was that no test boundary re-established a clean session.Lifecycle design
One atomic binding per logical MCP session:
_SessionContextis a frozen, slotted dataclass — a binding is replaced as a whole value, never partially mutated, so no caller can observe a half-initialized context.seed_session_context_if_unboundbinds only when the process has no current context (first-bind-wins). A changed environment or an interleaved call is not a session boundary and cannot displace an established binding. A new or forked process is recognized by PID.bind_session_contextis the single sanctioned re-bind, reachable only through explicitgitea_activate_profile— the legitimate new-logical-session boundary.get_session_contextreturns a detached snapshot, so a caller mutating the returned dict cannot reach the binding.assess_session_contextcompares live profile, remote, host, identity, repository, and org against the binding and fails closed on any drift before a mutation is attempted;require_bound=Truealso fails closed when unbound.Reset-boundary security design
Reset is
_reset_session_context_for_testing: private, and it raisesRuntimeErrorunlessPYTEST_CURRENT_TESTis present in the environment. It is therefore unreachable as a production bypass — a normal MCP caller cannot invoke it to clear a binding mid-request and re-bind to another profile. Production sessions transition only via process start / PID change or the explicit activation re-bind. Test isolation is centralized in the existing autouse_reset_mutation_authorityfixture intests/conftest.py, which resets before the test and again in afinally, so a failure in setup, test body, assertion, or teardown cannot leak a binding into the next test. No per-test resets were scattered across modules.Concurrency treatment
_SESSION_CONTEXTis guarded by a module-levelthreading.RLock. Read, first-bind, assess, and re-bind all take the lock, and the stored value is immutable. Concurrent initialization resolves to exactly one winning binding, and every racing caller returns that same complete binding rather than its own proposed one. Interleaved calls cannot overwrite an established binding, and no caller can observe a torn or partially initialized context.Files changed in this commit
tests/test_issue_714_session_context_immutability.py(+106, tests only)The isolation fix itself landed earlier on this branch in
29ffe14(tests/conftest.py,session_context_binding.py,gitea_mcp_server.py, and test modules). Auditing the required coverage against that head surfaced three properties the suite asserted nowhere, whichd936da5closes:mdcps-reviewerand deniedprgs.test_prgs_request_cannot_resolve_through_mdcps_profileproves the mirror direction: a pinned MDCPS profile never serves aprgsrequest and is never swapped forprgs-author.test_concurrent_initialization_has_single_winning_bindingraces 8 threads from unbound and asserts a single winner with no spliced identity.assess_session_contextchecks these, but no test bound a repository/org and asserted that a same-host mismatch fails closed.test_repository_mismatch_blocks_before_mutationcovers repository and org drift independently of host.Each new test was mutation-verified: disabling the first-bind-wins guard, the cross-host denial, or the repository/org drift checks each makes the corresponding test fail. They are not vacuous.
Exact verification
Run from the PR #715 worktree only, using the project venv interpreter (
venv/bin/python; a shell alias points barepython3at 3.13, which has no pytest installed):python -m pytest tests/test_issue_714_session_context_immutability.py -q -s→ 20 pass, 0 fail, 0 skip, 0 error — 1.09s
python -m pytest tests/test_resolve_task_capability.py tests/test_operation_scoped_roles.py tests/test_mcp_server.py -q -s(modules named in review 437)→ 240 pass, 0 fail, 0 skip, 0 error — 5.87s
Classes named in review 437 (
TestResolveTaskCapability,TestReviewerAuthorMutationBlocks,TestCreateIssue,TestCreatePR)→ 62 pass, 2654 deselected — 2.35s
python -m pytest tests/test_issue_714_session_context_immutability.py -q -s -k "concurrent or parallel or boundary or reset or drift or mismatch or contaminat or escape or survives"→ 12 pass, 8 deselected — 0.47s
python -m pytest tests/ -q -s→ 2713 pass, 6 skip, 0 fail, 0 error, 1 warning, 161 subtests pass — 23.05s
The 41 failures reported in review 437 are gone. The prior head
29ffe14measured 2710 pass / 6 skip; the +3 delta is exactly the three tests added here.Security readback
gitea_activate_profile.PYTEST_CURRENT_TESTrequired, elseRuntimeError).masterat1eafb757, carrying only its pre-existing untracked files.Request
Requesting independent
prgs-reviewerre-review of PR #715 at headd936da5c872fb4bad31536d80b7c5f57462b05a9. Author identity posted no review verdict and performed no merge.repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #715
issue: #714
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 25509-f2c2f35d026b
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/reviewer-pr715-d936da5
phase: claimed
candidate_head:
d936da5c87target_branch: master
target_branch_sha: none
last_activity: 2026-07-15T20:05:21Z
expires_at: 2026-07-15T22:05:21Z
blocker: none
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #715
issue: #714
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 25509-f2c2f35d026b
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/reviewer-pr715-d936da5
phase: decision
candidate_head:
d936da5c87target_branch: master
target_branch_sha: none
last_activity: 2026-07-15T20:08:19Z
expires_at: 2026-07-15T22:08:19Z
blocker: none
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.