fix(session): pin immutable MCP context; ban cross-host profile substitution (Closes #714) #715
Open
jcwalker3
wants to merge 5 commits from
fix/issue-714-session-context-immutability into master
pull from: fix/issue-714-session-context-immutability
merge into: :master
:master
:feat/issue-687-reconciler-branch-delete
: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
: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
Canonical Issue State
ISSUE: #714
PR: #715
STATE: OPEN
WHO_IS_NEXT: reviewer
NEXT_ACTION: Fresh independent prgs-reviewer review of PR #715 at head
943d40270eNEXT_PROMPT:
WHAT_HAPPENED: The independent review reproduced a real defect: the production first-bind path never pinned repository/org, so the repository and organization drift checks were skipped and a same-host mutation against another repository was not blocked. This head derives the session repository from the verified workspace git remote, checks it against a new per-profile
allowed_repositoriesauthorization boundary, and binds it immutably.WHY:
gitea_whoami,gitea_get_runtime_context,gitea_resolve_task_capabilityandgitea_activate_profileall seeded the context without repository/org. First-bind-wins then made the mutation gate's later seed a no-op, andassess_session_contextonly compares repository/org when the bound fields are non-null. The mutation gate also derived its values fromorg or REMOTES[...], so a caller-supplied org/repo could have established the binding.RELATED_PRS: #715 (this PR, Closes #714); #711 is the dependent follow-on and remains outstanding; MDCPS reconciliation stays prohibited until #714 and #711 both land. PR #710 is out of scope and untouched.
HEAD_SHA:
943d40270ePRIOR_HEAD_SHA:
d936da5c87REVIEWED_HEAD_SHA:
632c568865REVIEW_STATUS: CHANGES_REQUESTED
MERGE_READY: false
BLOCKERS: none
VALIDATION: Full suite 2739 pass, 6 skip, 1 pre-existing warning, 161 subtests in 25.80s
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;approval_at_current_headis false and no APPROVE verdict exists for any head. The independent reviewer that reproduced the first-bind defect could not record a verdict because its terminal-decision lock is still associated with open PR #710; that reproduction is treated here as review evidence, not as a recorded verdict.Local verdict/state: Author remediation is complete at head
943d40270e9ecdee48707eb09454f0321e8e4e14. This session holdsprgs-author, 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
Three pushes have landed on
fix/issue-714-session-context-immutabilitysince review 437:29ffe14— centralized test-boundary isolation of_SESSION_CONTEXT.d936da5— reverse cross-host, concurrent-init and repository/org drift tests.943d402— this session: workspace-verified repository scope bound at first bind (production + tests + schema + docs).Server-side mutation ledger: 1 commit (
943d402), 1 non-force push (d936da5..943d402, 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 reproduced defect no longer occurs at this head. The full suite reports zero failures. One environmental flake was observed once and is documented below; it is unrelated to this remediation.
Who/what acts next
Next actor: prgs-reviewer
Required action: Fresh independent review of PR #715 at head
943d40270e9ecdee48707eb09454f0321e8e4e14against Issue #714 AC 1-8 plus the repository-scope design below.Do not do: Do not treat review 437 as current. Do not review from the author session or the control checkout. Do not merge on the strength of this comment — it records author remediation only. Do not extend scope into general cross-project support, PR #710, or eAgenda.
[/THREAD STATE LEDGER]
Author remediation — production first-bind repository scope
Reproduced root cause
Reproduced in production order against the real
REMOTES, before any change:gitea_whoami,gitea_get_runtime_context,gitea_resolve_task_capabilityandgitea_activate_profileeach seeded the session without repository/org. Because first-bind-wins is correct and deliberate, the mutation gate's later seed became a no-op, andassess_session_contextonly compares repository/org when the bound fields are non-null — so those two checks never ran. Separately, the gate resolvedorg or REMOTES[remote]["org"]andrepo or REMOTES[remote]["repo"], meaning a caller-supplied argument could have established the binding on a mutation-first session.Why REMOTES could not be the trusted source
Binding the
REMOTESdefaults was measured, not assumed:REMOTES['prgs'].repoisTimesheetandREMOTES['dadeschools'].orgisContractor— these are default targets (see #530), not an authorization scope. Pinning them would fail closed on every legitimate Gitea-Tools mutation, including this comment. A trusted scope therefore had to be introduced.Trusted source and binding design
allowed_repositories— canonicalowner/repositoryslugs. It is config-only:gitea_auth.get_profilereads it from the JSON config layer and deliberately never from an environment variable, so no env var can widen or forge it. Checked at load bygitea_config._validate_allowed_repositories._workspace_repository_slug→remote_repo_guard.parse_org_repo_from_remote_url), never from a caller-suppliedorg/repo, and never fromREMOTES.allowed_repositoriesis an authorization boundary only. The workspace-derived slug is checked against it and the session binds immutably to that single canonicalowner/repository. The organization is derived from the slug; there is no independent caller-controlled organization value. If a profile authorizes several repositories, the verified workspace still selects exactly one — the session never binds to the list and never switches between entries.Fail-closed behavior:
blocker_kind: repository_scope).require_complete).org/repooverride that disagrees with the binding is rejected before the write (assess_repository_override).Enforcement is opt-in per profile: a profile that omits
allowed_repositorieskeeps prior behaviour, so existing static-profile namespaces stay functional (#714 AC8). Operator note: enforcement activates forprgs-authoronce"allowed_repositories": ["Scaled-Tech-Consulting/Gitea-Tools"]is provisioned in the private profile configuration, which is not committed. Only Gitea-Tools is authorized for this phase — Timesheet and eAgenda are deliberately excluded while cross-project use is paused.Concurrency
Unchanged and preserved: the binding is a frozen dataclass behind the existing module-level
RLock; first-bind-wins, atomic replace-as-whole-value, detached snapshots. Repository derivation happens before the lock is taken and only the winning first bind stores it, so eight concurrent mutation-gate calls each carrying different attacker-supplied org/repo still converge on the single workspace-verified binding.Verified during this remediation
REMOTES, never from caller arguments, never from env.gitea_whoami,gitea_get_runtime_context,gitea_resolve_task_capability,gitea_activate_profile,_session_context_mutation_block— five sites, all now routed through the same trusted derivation.repository=None/org=Nonefor a scoped profile: an unverifiable workspace fails closed rather than binding empty.gitea_activate_profileremains the only sanctioned rebind and now checks scope at that boundary.gitea_auth.get_profilebuilt an explicit whitelist dict and silently droppedallowed_repositories, so the scope was never enforced through the real path. Fixed by passing it through from the config layer.Files changed
session_context_binding.py— repository-scope primitives;require_completegitea_mcp_server.py— workspace-derived trusted repository; all bind sites; gate override checks; activation scope gategitea_auth.py—allowed_repositoriespassthrough (config-only)gitea_config.py— schema checksgitea-mcp.v2-contexts.example.json— synthetic exampledocs/gitea-execution-profiles.md— field + "Repository scope (#714)" sectiontests/test_issue_714_production_first_bind.py— new, 26 integration testsExact verification
Run from the PR #715 worktree only, with
venv/bin/python(barepython3is aliased to 3.13 and has no pytest):venv/bin/python -m pytest tests/test_issue_714_production_first_bind.py -q -s→ 26 pass, 0 fail, 0 skip, 0 error — 0.71s
venv/bin/python -m pytest tests/test_issue_714_production_first_bind.py tests/test_issue_714_session_context_immutability.py -q -s→ 46 pass, 0 fail — 1.71s
venv/bin/python -m pytest tests/test_resolve_task_capability.py tests/test_operation_scoped_roles.py tests/test_mcp_server.py -q -s→ 240 pass, 0 fail — 9.46s
venv/bin/python -m pytest tests/test_config.py tests/test_config_v2_contexts.py tests/test_credentials.py tests/test_remote_repo_guard.py tests/test_runtime_clarity.py tests/test_reconciler_profile.py tests/test_dual_namespace_docs.py tests/test_migrate_profiles.py -q -s→ 130 pass, 0 fail — 0.63s
venv/bin/python -m pytest tests/ -q -s→ 2739 pass, 6 skip, 0 fail, 0 error, 1 warning, 161 subtests — 25.80s
Baseline was 2713 pass / 6 skip at
d936da5; the +26 delta is exactly this file's new tests.The new tests are mutation-verified: disabling trusted derivation at first bind, the override checks, the allowlist scope check, or the
get_profilepassthrough causes 9, 8, 4 and 5 of them to fail respectively.Environmental failure, distinguished from remediation: one full-suite run reported
tests/test_mirror_refs.py::TestDryRunBanner::test_dry_run_banner_shown_by_defaultas failed while the machine was under load from a concurrent diagnostic run (that run took 54.18s versus a normal 25.80s). It passes in isolation (1 pass), across its whole module (15 pass), at the prior head, and on a clean full-suite re-run. It shells out tomirror_refs.shwith a 30s timeout and network/credential access, and its own docstring states it "will fail if credentials are unavailable". Unrelated to session context. The remaining warning is the pre-existingStarletteDeprecationWarningintests/test_webui_audit.py.Security readback
org/repo, which was the defect.PYTEST_CURRENT_TESTrequired, elseRuntimeError).masterat1eafb757, carrying only its pre-existing untracked files.Request
Requesting a fresh independent
prgs-reviewerreview of PR #715 at head943d40270e9ecdee48707eb09454f0321e8e4e14. 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: released
candidate_head:
d936da5c87target_branch: master
target_branch_sha: none
last_activity: 2026-07-15T21:04:44Z
expires_at: 2026-07-15T23:04:44Z
blocker: manual-release
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #715
issue: #714
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 25509-68cae3eac14e
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/reviewer-pr715-943d402
phase: claimed
candidate_head:
943d40270etarget_branch: master
target_branch_sha: none
last_activity: 2026-07-15T21:05:30Z
expires_at: 2026-07-15T23:05:30Z
blocker: none
Canonical Issue State
STATE: author remediation complete at head dc899d2; awaiting independent re-review
WHO_IS_NEXT: reviewer
NEXT_ACTION: Fresh native-MCP formal review of PR #715 at head dc899d23c85f879dfa5b2e03c376112f1ea8382a; do not reuse reviews of
943d402or comment 11662NEXT_PROMPT:
WHAT_HAPPENED:
943d402.dc899d2(fast-forward descendant of943d402) and pushed without force to fix/issue-714-session-context-immutability.dc899d2(was943d402).WHY:
943d402was not merge-ready: machine-dependent false green from env-only mutation fixtures, REMOTES Timesheet defaults treated as explicit caller input, and non-deterministic workspace Git remotes. Remediation requires config-backed non-empty allowed_repositories for mutations, preserved omitted/explicit provenance, workspace-aligned omitted targets, fail-closed Git/workspace verification, and deterministic fixtures without weakening security assertions.RELATED_PRS: #715 (this), Issue #714; Issue #716 session-state location not implemented here; PR #701 / review 431 out of scope.
BLOCKERS: none for re-review. Merge still requires independent approval at
dc899d2. Do not authorize merge on 943d402-era reviews.VALIDATION:
LAST_UPDATED_BY: prgs-author / jcwalker3 (author remediation continuation)
Delivery heads
943d40270edc899d23c8repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #715
issue: none
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 88969-eead1f5d688a
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr715-dc899d2
phase: claimed
candidate_head: none
target_branch: master
target_branch_sha: none
last_activity: 2026-07-16T03:33:28Z
expires_at: 2026-07-16T05:33:28Z
blocker: none
The PR #715 code was successfully isolated and verified on its pinned head (2744 tests passed). However, target-branch advancement to
1ab384a(PR #710 merge) introduced merge conflicts ingitea_mcp_server.py. Please resolve the conflicts and update the branch.Contaminated formal review quarantine (#695)
Status: QUARANTINED — merge authorization VOID
440#715dc899d23c85f879dfa5b2e03c376112f1ea8382asysadminprgs-reconciler#7172026-07-16T04:11:03.091864+00:00True6b6baa66ef52be1b[11725, 11727]This record does not delete Gitea reviews or historical comments. Fresh native-MCP re-review is required before merge.
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.