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
Owner

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-reviewer becoming prgs-author).

Root cause

_ensure_matching_profile and _try_auto_switch_for_operation auto-activated any configured profile that held the required permission, without regard to remote or host. After pinning mdcps-reviewer for dadeschools, resolving comment_issue switched the process to prgs-author.

Changes

  • New session_context_binding.py: bind, seed, and assess session context; filter profiles by remote host; identity expected-versus-actual checks.
  • Remove silent auto-switch; evaluate only the active profile for the requested remote.
  • Explicit gitea_activate_profile re-binds the session context.
  • whoami, runtime context, and capability resolver report the same context proof.
  • Mutation gates and audit records include the bound session context; cross-host, identity mismatch, and drift fail closed before writes.

Tests

  • New tests/test_issue_714_session_context_immutability.py reproduces the pin then drift then resolver sequence and remaining acceptance criteria.
  • Updated tests/test_operation_scoped_roles.py for fail-closed (no auto-switch); explicit activate still works.

Dependent work

  • #711 is the next dependent issue (MDCPS close path, worktree_path, reconciler routing).
  • MDCPS reconciliation remains prohibited until #714 and #711 are both merged and verified.

Verification

  • Targeted pytest: 90 tests passed in the worktree (issue 714, operation-scoped roles, role session router, runtime clarity, permission reports, workspace guards, session state).
  • Control checkout left on clean master (pre-existing untracked only).
  • Registered worktree: branches/fix-issue-714-session-context-immutability.

Do not self-review or merge.

## 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-reviewer` becoming `prgs-author`). ### Root cause `_ensure_matching_profile` and `_try_auto_switch_for_operation` auto-activated any configured profile that held the required permission, without regard to remote or host. After pinning `mdcps-reviewer` for `dadeschools`, resolving `comment_issue` switched the process to `prgs-author`. ### Changes - New `session_context_binding.py`: bind, seed, and assess session context; filter profiles by remote host; identity expected-versus-actual checks. - Remove silent auto-switch; evaluate only the active profile for the requested remote. - Explicit `gitea_activate_profile` re-binds the session context. - `whoami`, runtime context, and capability resolver report the same context proof. - Mutation gates and audit records include the bound session context; cross-host, identity mismatch, and drift fail closed before writes. ### Tests - New `tests/test_issue_714_session_context_immutability.py` reproduces the pin then drift then resolver sequence and remaining acceptance criteria. - Updated `tests/test_operation_scoped_roles.py` for fail-closed (no auto-switch); explicit activate still works. ### Dependent work - **#711** is the next dependent issue (MDCPS close path, worktree_path, reconciler routing). - **MDCPS reconciliation remains prohibited until #714 and #711 are both merged and verified.** ### Verification - Targeted pytest: 90 tests passed in the worktree (issue 714, operation-scoped roles, role session router, runtime clarity, permission reports, workspace guards, session state). - Control checkout left on clean `master` (pre-existing untracked only). - Registered worktree: `branches/fix-issue-714-session-context-immutability`. Do not self-review or merge.
jcwalker3 added 1 commit 2026-07-14 22:06:22 -05:00
Capability resolution and mutation gates no longer auto-switch profiles.
Session profile/remote/host/identity are bound after pin or first seed,
and drift or cross-host resolution fails closed before writes.

Co-Authored-By: Grok <[email protected]>
Owner

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

<!-- mcp-review-lease:v1 --> 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
Owner

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

<!-- mcp-review-lease:v1 --> 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
Owner

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

<!-- mcp-review-lease:v1 --> 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
sysadmin requested changes 2026-07-15 01:14:36 -05:00
Dismissed
sysadmin left a comment
Owner

Canonical PR State

STATE: OPEN
WHO_IS_NEXT: author
NEXT_ACTION: Fix test pollution in session context tests
NEXT_PROMPT:

The drift protection logic works correctly in the live server, but the PR introduces significant test pollution. The `_SESSION_CONTEXT` global variable in `session_context_binding.py` bleeds across tests in the same process because `session_ctx.clear_session_context()` is not called in the `setUp` of most test classes. This causes false-positive "drift block" failures when tests change profiles or identities (41 test failures across the suite). Please add `clear_session_context()` to a base test class or all relevant test classes, verify the full test suite passes reliably, and push the fix.

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_CONTEXT is not cleared between tests in several classes (e.g., TestResolveTaskCapability, TestReviewerAuthorMutationBlocks).
ISSUE: #714
HEAD_SHA: 632c568865
REVIEW_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-reviewer attempting to mutate dadeschools is correctly blocked and does not switch to mdcps-reviewer).

However, the PR introduces significant test pollution that breaks the broader test suite. The session_context_binding.py module uses a global _SESSION_CONTEXT dictionary. While TestOperationScopedRoles correctly calls session_ctx.clear_session_context() in setUp() and tearDown(), 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:

  • TestResolveTaskCapability
  • TestReviewerAuthorMutationBlocks
  • TestCreateIssue
  • TestCreatePR
  • etc.

Please add session_ctx.clear_session_context() to the setUp()/tearDown() of a base test class or ensure it is cleared in all relevant test classes so that the test suite passes reliably.

## Canonical PR State STATE: OPEN WHO_IS_NEXT: author NEXT_ACTION: Fix test pollution in session context tests NEXT_PROMPT: ```text The drift protection logic works correctly in the live server, but the PR introduces significant test pollution. The `_SESSION_CONTEXT` global variable in `session_context_binding.py` bleeds across tests in the same process because `session_ctx.clear_session_context()` is not called in the `setUp` of most test classes. This causes false-positive "drift block" failures when tests change profiles or identities (41 test failures across the suite). Please add `clear_session_context()` to a base test class or all relevant test classes, verify the full test suite passes reliably, and push the fix. ``` 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_CONTEXT` is not cleared between tests in several classes (e.g., `TestResolveTaskCapability`, `TestReviewerAuthorMutationBlocks`). ISSUE: #714 HEAD_SHA: 632c568865cab3de7a8d9970463ec182578d0a86 REVIEW_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-reviewer` attempting to mutate `dadeschools` is correctly blocked and does not switch to `mdcps-reviewer`). However, the PR introduces significant test pollution that breaks the broader test suite. The `session_context_binding.py` module uses a global `_SESSION_CONTEXT` dictionary. While `TestOperationScopedRoles` correctly calls `session_ctx.clear_session_context()` in `setUp()` and `tearDown()`, 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: - `TestResolveTaskCapability` - `TestReviewerAuthorMutationBlocks` - `TestCreateIssue` - `TestCreatePR` - etc. Please add `session_ctx.clear_session_context()` to the `setUp()`/`tearDown()` of a base test class or ensure it is cleared in all relevant test classes so that the test suite passes reliably.
jcwalker3 added 1 commit 2026-07-15 01:26:52 -05:00
Author
Owner

Canonical Issue State

STATE: OPEN
WHO_IS_NEXT: reviewer
NEXT_ACTION: Independently re-review PR #715 at head 29ffe1407f.
NEXT_PROMPT:

Independently re-review PR #715 at 29ffe1407fa9620b4a75035bb9be3a254e74a674. Verify the immutable session-context lifecycle, the pytest-only before/finally isolation boundary, concurrency safety, the preserved cross-host/repository/identity fail-closed gates, and the full-suite result. Do not rely on the author's conclusions.

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

  • The first whoami/capability/runtime/mutation gate atomically binds profile, remote, host, org/repository, identity, role, and PID.
  • Ordinary calls and environment changes cannot rebind it. A new PID re-seeds; explicit gitea_activate_profile is the sanctioned logical-session transition.
  • The shared autouse fixture resets before each pytest item and in a finally block after it, so failures and teardown exceptions cannot pollute later tests. Ad hoc class cleanup was removed.
  • The reset helper is private, requires pytest's per-test marker, is not an MCP tool, and is rejected outside a pytest boundary. Production never clears context between calls.
  • The binding is a frozen dataclass behind an RLock; first bind and sanctioned rebind are atomic, snapshots are detached, and parallel/interleaved seed calls cannot overwrite an established binding.
  • Unsupported permissions and namespace mismatches still fail before writes. Configured identity mismatch, profile drift, cross-host, org, and repository mismatch remain blocked. Legacy env-only profiles may establish only their first remote/host/repository pin.

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 STATE: OPEN WHO_IS_NEXT: reviewer NEXT_ACTION: Independently re-review PR #715 at head 29ffe1407fa9620b4a75035bb9be3a254e74a674. NEXT_PROMPT: ```text Independently re-review PR #715 at 29ffe1407fa9620b4a75035bb9be3a254e74a674. Verify the immutable session-context lifecycle, the pytest-only before/finally isolation boundary, concurrency safety, the preserved cross-host/repository/identity fail-closed gates, and the full-suite result. Do not rely on the author's conclusions. ``` WHAT_HAPPENED: The author remediated review #437 on the existing PR branch, committed 29ffe1407fa9620b4a75035bb9be3a254e74a674, 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 - The first whoami/capability/runtime/mutation gate atomically binds profile, remote, host, org/repository, identity, role, and PID. - Ordinary calls and environment changes cannot rebind it. A new PID re-seeds; explicit `gitea_activate_profile` is the sanctioned logical-session transition. - The shared autouse fixture resets before each pytest item and in a `finally` block after it, so failures and teardown exceptions cannot pollute later tests. Ad hoc class cleanup was removed. - The reset helper is private, requires pytest's per-test marker, is not an MCP tool, and is rejected outside a pytest boundary. Production never clears context between calls. - The binding is a frozen dataclass behind an `RLock`; first bind and sanctioned rebind are atomic, snapshots are detached, and parallel/interleaved seed calls cannot overwrite an established binding. - Unsupported permissions and namespace mismatches still fail before writes. Configured identity mismatch, profile drift, cross-host, org, and repository mismatch remain blocked. Legacy env-only profiles may establish only their first remote/host/repository pin. The only warning is the existing Starlette/httpx deprecation in `tests/test_webui_audit.py`. The author did not self-review or merge.
jcwalker3 added 1 commit 2026-07-15 14:52:05 -05:00
The formal REQUEST_CHANGES review on PR #715 was resolved by the test
isolation commit (29ffe14); the full suite is green. Auditing the required
session-lifecycle coverage surfaced three gaps that the suite did not prove:

- Every cross-host test pinned mdcps-reviewer and denied prgs. The reverse
  direction (a pinned MDCPS profile must never serve a prgs request, nor be
  swapped for prgs-author) was unproven.
- test_parallel_calls_cannot_overwrite_established_binding binds first and
  then races, so concurrent *initialization* from an unbound context — N
  threads racing to first-bind, exactly one winner, no torn context — was
  unproven.
- assess_session_context checks repository and org drift, but no test bound a
  repository/org and asserted a same-host mismatch fails closed.

Adds three tests covering those cases. Each was mutation-verified: disabling
the first-bind-wins guard, the cross-host denial, and the repository/org drift
checks makes the corresponding test fail.

Additive only — no production code changed and no existing security assertion
weakened, skipped, or rewritten.

Focused: 20 passed. Full suite: 2713 passed, 6 skipped, 1 pre-existing warning,
161 subtests passed in 23.05s.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Author
Owner

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 d936da5c87
NEXT_PROMPT:

REVIEWER task — Gitea-Tools PR #715 (Issue #714), repository Scaled-Tech-Consulting/Gitea-Tools, remote prgs.

Use the native gitea-reviewer namespace with the prgs-reviewer profile. Acquire your own reviewer lease; do not adopt the author session.

Pin and verify head d936da5c872fb4bad31536d80b7c5f57462b05a9 before analysis. Review 437 (REQUEST_CHANGES) was recorded at head 632c5688 and predates two pushes; it does not describe this head.

Scope: Issue #714 acceptance criteria 1-8 (cross-host MCP profile drift and capability substitution fail closed).

Verify specifically:
- Repeated calls after pinning mdcps-reviewer never report prgs-author.
- A dadeschools request cannot resolve through a prgs profile, and a prgs request cannot resolve through an MDCPS profile.
- Unsupported capability returns a structured denial without profile substitution.
- Identity, repository, org, host, and profile mismatch each fail closed before mutation.
- _reset_session_context_for_testing cannot be reached by a production caller.
- Concurrent initialization yields one winning binding; interleaved calls cannot overwrite an established binding.
- Existing static-profile namespaces remain functional.

Run the full suite from a reviewer worktree using the project venv interpreter (bare python3 is aliased to 3.13 and has no pytest):
  venv/bin/python -m pytest tests/ -q -s
Author measured 2713 pass, 6 skip, 1 pre-existing warning, 161 subtests, 23.05s.

Do not merge. Record a formal verdict via the review API only.

WHAT_HAPPENED: Author remediated the test-context contamination cited by review 437 and pushed two commits. 29ffe14 centralized _SESSION_CONTEXT isolation at the test boundary; d936da5 adds 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_CONTEXT is a process-global binding that only TestOperationScopedRoles reset, 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: d936da5c87
PRIOR_HEAD_SHA: 29ffe1407f
REVIEWED_HEAD_SHA: 632c568865
REVIEW_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 the prgs-author profile, 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-immutability since 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 StarletteDeprecationWarning in tests/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_CONTEXT is a process-global binding. Review 437 (head 632c5688) found it leaking across test boundaries: only TestOperationScopedRoles reset 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:

  • _SessionContext is 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_unbound binds 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_context is the single sanctioned re-bind, reachable only through explicit gitea_activate_profile — the legitimate new-logical-session boundary.
  • get_session_context returns a detached snapshot, so a caller mutating the returned dict cannot reach the binding.
  • assess_session_context compares live profile, remote, host, identity, repository, and org against the binding and fails closed on any drift before a mutation is attempted; require_bound=True also fails closed when unbound.

Reset-boundary security design

Reset is _reset_session_context_for_testing: private, and it raises RuntimeError unless PYTEST_CURRENT_TEST is 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_authority fixture in tests/conftest.py, which resets before the test and again in a finally, 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_CONTEXT is guarded by a module-level threading.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, which d936da5 closes:

  1. Reverse cross-host denial — every existing cross-host test pinned mdcps-reviewer and denied prgs. test_prgs_request_cannot_resolve_through_mdcps_profile proves the mirror direction: a pinned MDCPS profile never serves a prgs request and is never swapped for prgs-author.
  2. Concurrent initialization — the existing parallel test binds first and then races, so racing first-binds from an unbound context were unproven. test_concurrent_initialization_has_single_winning_binding races 8 threads from unbound and asserts a single winner with no spliced identity.
  3. Repository/org driftassess_session_context checks these, but no test bound a repository/org and asserted that a same-host mismatch fails closed. test_repository_mismatch_blocks_before_mutation covers 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 bare python3 at 3.13, which has no pytest installed):

  1. python -m pytest tests/test_issue_714_session_context_immutability.py -q -s
    20 pass, 0 fail, 0 skip, 0 error — 1.09s
  2. 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
  3. 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
  4. 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 29ffe14 measured 2710 pass / 6 skip; the +3 delta is exactly the three tests added here.

Security readback

  • Production session bindings remain immutable; the only sanctioned transitions are process start / PID change and explicit gitea_activate_profile.
  • No security assertion was weakened, removed, skipped, or rewritten. This commit adds tests only and changes no production code.
  • No mutation gate was bypassed. Cross-host, identity, repository, org, host, and profile mismatches each fail closed before any write is attempted.
  • The reset mechanism is unavailable to normal production callers (private, plus PYTEST_CURRENT_TEST required, else RuntimeError).
  • The Gitea-Tools control checkout was untouched: still on master at 1eafb757, carrying only its pre-existing untracked files.
  • No eAgenda repository or worktree was accessed or modified; eAgenda PR #194 untouched.
  • PR #710 untouched.

Request

Requesting independent prgs-reviewer re-review of PR #715 at head d936da5c872fb4bad31536d80b7c5f57462b05a9. Author identity posted no review verdict and performed no 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 d936da5c872fb4bad31536d80b7c5f57462b05a9 NEXT_PROMPT: ```text REVIEWER task — Gitea-Tools PR #715 (Issue #714), repository Scaled-Tech-Consulting/Gitea-Tools, remote prgs. Use the native gitea-reviewer namespace with the prgs-reviewer profile. Acquire your own reviewer lease; do not adopt the author session. Pin and verify head d936da5c872fb4bad31536d80b7c5f57462b05a9 before analysis. Review 437 (REQUEST_CHANGES) was recorded at head 632c5688 and predates two pushes; it does not describe this head. Scope: Issue #714 acceptance criteria 1-8 (cross-host MCP profile drift and capability substitution fail closed). Verify specifically: - Repeated calls after pinning mdcps-reviewer never report prgs-author. - A dadeschools request cannot resolve through a prgs profile, and a prgs request cannot resolve through an MDCPS profile. - Unsupported capability returns a structured denial without profile substitution. - Identity, repository, org, host, and profile mismatch each fail closed before mutation. - _reset_session_context_for_testing cannot be reached by a production caller. - Concurrent initialization yields one winning binding; interleaved calls cannot overwrite an established binding. - Existing static-profile namespaces remain functional. Run the full suite from a reviewer worktree using the project venv interpreter (bare python3 is aliased to 3.13 and has no pytest): venv/bin/python -m pytest tests/ -q -s Author measured 2713 pass, 6 skip, 1 pre-existing warning, 161 subtests, 23.05s. Do not merge. Record a formal verdict via the review API only. ``` WHAT_HAPPENED: Author remediated the test-context contamination cited by review 437 and pushed two commits. `29ffe14` centralized `_SESSION_CONTEXT` isolation at the test boundary; `d936da5` adds 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_CONTEXT` is a process-global binding that only `TestOperationScopedRoles` reset, 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: d936da5c872fb4bad31536d80b7c5f57462b05a9 PRIOR_HEAD_SHA: 29ffe1407fa9620b4a75035bb9be3a254e74a674 REVIEWED_HEAD_SHA: 632c568865cab3de7a8d9970463ec182578d0a86 REVIEW_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 the `prgs-author` profile, 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-immutability` since 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 `StarletteDeprecationWarning` in `tests/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_CONTEXT` is a process-global binding. Review 437 (head `632c5688`) found it leaking across test boundaries: only `TestOperationScopedRoles` reset 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: - `_SessionContext` is 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_unbound` binds 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_context` is the single sanctioned re-bind, reachable only through explicit `gitea_activate_profile` — the legitimate new-logical-session boundary. - `get_session_context` returns a detached snapshot, so a caller mutating the returned dict cannot reach the binding. - `assess_session_context` compares live profile, remote, host, identity, repository, and org against the binding and fails closed on any drift before a mutation is attempted; `require_bound=True` also fails closed when unbound. ### Reset-boundary security design Reset is `_reset_session_context_for_testing`: private, and it raises `RuntimeError` unless `PYTEST_CURRENT_TEST` is 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_authority` fixture in `tests/conftest.py`, which resets before the test and again in a `finally`, 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_CONTEXT` is guarded by a module-level `threading.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, which `d936da5` closes: 1. **Reverse cross-host denial** — every existing cross-host test pinned `mdcps-reviewer` and denied `prgs`. `test_prgs_request_cannot_resolve_through_mdcps_profile` proves the mirror direction: a pinned MDCPS profile never serves a `prgs` request and is never swapped for `prgs-author`. 2. **Concurrent initialization** — the existing parallel test binds first and then races, so racing *first-binds* from an unbound context were unproven. `test_concurrent_initialization_has_single_winning_binding` races 8 threads from unbound and asserts a single winner with no spliced identity. 3. **Repository/org drift** — `assess_session_context` checks these, but no test bound a repository/org and asserted that a same-host mismatch fails closed. `test_repository_mismatch_blocks_before_mutation` covers 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 bare `python3` at 3.13, which has no pytest installed): 1. `python -m pytest tests/test_issue_714_session_context_immutability.py -q -s` → **20 pass, 0 fail, 0 skip, 0 error — 1.09s** 2. `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** 3. `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** 4. `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 `29ffe14` measured 2710 pass / 6 skip; the +3 delta is exactly the three tests added here. ### Security readback - Production session bindings remain immutable; the only sanctioned transitions are process start / PID change and explicit `gitea_activate_profile`. - No security assertion was weakened, removed, skipped, or rewritten. This commit adds tests only and changes no production code. - No mutation gate was bypassed. Cross-host, identity, repository, org, host, and profile mismatches each fail closed before any write is attempted. - The reset mechanism is unavailable to normal production callers (private, plus `PYTEST_CURRENT_TEST` required, else `RuntimeError`). - The Gitea-Tools control checkout was untouched: still on `master` at `1eafb757`, carrying only its pre-existing untracked files. - No eAgenda repository or worktree was accessed or modified; eAgenda PR #194 untouched. - PR #710 untouched. ### Request Requesting independent `prgs-reviewer` re-review of PR #715 at head `d936da5c872fb4bad31536d80b7c5f57462b05a9`. Author identity posted no review verdict and performed no merge.
Owner

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: d936da5c87
target_branch: master
target_branch_sha: none
last_activity: 2026-07-15T20:05:21Z
expires_at: 2026-07-15T22:05:21Z
blocker: none

<!-- mcp-review-lease:v1 --> 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: d936da5c872fb4bad31536d80b7c5f57462b05a9 target_branch: master target_branch_sha: none last_activity: 2026-07-15T20:05:21Z expires_at: 2026-07-15T22:05:21Z blocker: none
Owner

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: d936da5c87
target_branch: master
target_branch_sha: none
last_activity: 2026-07-15T20:08:19Z
expires_at: 2026-07-15T22:08:19Z
blocker: none

<!-- mcp-review-lease:v1 --> 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: d936da5c872fb4bad31536d80b7c5f57462b05a9 target_branch: master target_branch_sha: none last_activity: 2026-07-15T20:08:19Z expires_at: 2026-07-15T22:08:19Z blocker: none
jcwalker3 added 1 commit 2026-07-15 15:51:01 -05:00
The independent review reproduced a real hole: the production first-bind path
never pinned repository/org, so the drift checks it feeds were skipped.

Root cause
----------
gitea_whoami, gitea_get_runtime_context, gitea_resolve_task_capability and
gitea_activate_profile all seeded the session context without repository or
org. First-bind-wins then made the mutation gate's later seed a no-op, and
assess_session_context only compares repository/org when the bound fields are
non-null. Result, reproduced in production order with the real REMOTES:

  after whoami:               repository=None org=None
  same-host repo=Other-Tools  -> NOT BLOCKED
  same-host org=Other-Org     -> NOT BLOCKED
  cross-host                  -> blocked (this part always worked)

Binding REMOTES defaults would not fix it: REMOTES['prgs'].repo is 'Timesheet'
(a default *target*, not an authorization scope, see #530), so pinning it fails
closed on every legitimate Gitea-Tools mutation. There was no trusted source
for repository scope, so this adds one.

Design
------
- New optional profile field `allowed_repositories`: canonical owner/repository
  slugs. An authorization boundary, never the binding itself. Config-only —
  no env var can widen or forge it.
- The session repository is derived from the verified, workspace-aligned git
  remote, never from caller-supplied org/repo, and validated against that
  allowlist. The session binds immutably to exactly one canonical slug; org is
  derived from it. A profile authorizing several repositories still binds to
  the single verified one.
- Every first-bind entry point now pins the same complete context. Activation
  rejects an unauthorized/unverifiable workspace. Mutations fail closed when
  repository/org is unverified, and a tool-level org/repo override that
  disagrees with the binding is rejected before the write.
- A mutation request can no longer establish, complete, or replace the binding
  (it previously seeded from `org or REMOTES[...]`, i.e. caller-controlled).
- Enforcement is opt-in per profile: a profile without the field keeps prior
  behaviour, so existing static-profile namespaces stay functional.

First-bind-wins, immutability, the RLock, profile isolation, host/identity
validation and the private pytest-only reset are unchanged.

gitea_auth.get_profile() built an explicit whitelist dict and silently dropped
`allowed_repositories`; the new integration tests caught that the scope was
never enforced through the real path.

Tests
-----
New tests/test_issue_714_production_first_bind.py drives the real entry points
in production order rather than constructing _SessionContext directly. Covers
complete first bind via each entry point, same-host repo/org drift, Timesheet
and other-repo/other-org rejection, unverified and unauthorized workspaces,
caller values unable to establish the binding, concurrent init selecting one
repository, and the PR #715 author path staying functional. Mutation-verified:
disabling trusted derivation, override validation, the scope check, or the
get_profile passthrough each fails these tests (9/8/4/5 failures).

No security assertion was weakened, removed, skipped, or rewritten.

Focused: 26 passed. Issue #714 total: 46 passed. Prior failing modules: 240
passed. Config/profile/remote modules: 130 passed. Full suite: 2739 passed,
6 skipped, 1 pre-existing warning, 161 subtests in 25.80s.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Author
Owner

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 943d40270e
NEXT_PROMPT:

REVIEWER task — Gitea-Tools PR #715 (Issue #714), Scaled-Tech-Consulting/Gitea-Tools, remote prgs.

Use the native gitea-reviewer namespace with the prgs-reviewer profile. Acquire your own reviewer lease.

Pin and verify head 943d40270e9ecdee48707eb09454f0321e8e4e14. Review 437 was recorded at 632c5688 and predates three pushes.

Scope: Issue #714 AC 1-8 plus the repository-scope enforcement added at this head.

Verify the reproduced first-bind defect is closed:
- After gitea_whoami on prgs, the context carries repository=Gitea-Tools and org=Scaled-Tech-Consulting (previously None/None).
- Same-host repo=Other-Tools and org=Other-Org now fail closed.
- Cross-host still fails closed.
- A legitimate Gitea-Tools mutation still succeeds.

Verify the trusted source: the session repository is derived from the workspace git remote and checked against the profile allowed_repositories. Confirm REMOTES is NOT used for scope (REMOTES['prgs'].repo is Timesheet — a default target, not a scope; binding it would fail closed on all Gitea-Tools work).

Confirm a mutation request cannot establish, complete, or replace the binding, that org/repo overrides are checked against it, that first-bind-wins and the pytest-only reset are intact, and that allowed_repositories cannot be set from an environment variable.

Run from a reviewer worktree with the project venv (bare python3 is aliased to 3.13, no pytest):
  venv/bin/python -m pytest tests/ -q -s
Author measured 2739 passed, 6 skipped, 1 pre-existing warning, 161 subtests, 25.80s.

Note: tests/test_mirror_refs.py::TestDryRunBanner::test_dry_run_banner_shown_by_default is an environmental flake (shells out to mirror_refs.sh with a 30s timeout and network/credential access). It failed once under load and passed in isolation, in its module, at the prior head, and on a clean full-suite re-run.

Do not merge. Record a formal verdict via the review API only.

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_repositories authorization boundary, and binds it immutably.
WHY: gitea_whoami, gitea_get_runtime_context, gitea_resolve_task_capability and gitea_activate_profile all seeded the context without repository/org. First-bind-wins then made the mutation gate's later seed a no-op, and assess_session_context only compares repository/org when the bound fields are non-null. The mutation gate also derived its values from org 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: 943d40270e
PRIOR_HEAD_SHA: d936da5c87
REVIEWED_HEAD_SHA: 632c568865
REVIEW_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_head is 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 holds prgs-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-immutability since 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 943d40270e9ecdee48707eb09454f0321e8e4e14 against 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:

after gitea_whoami(remote="prgs"):  repository=None  org=None
same-host repo=Other-Tools       -> NOT BLOCKED
same-host org=Other-Org          -> NOT BLOCKED
cross-host dadeschools           -> BLOCKED   (this part always worked)

gitea_whoami, gitea_get_runtime_context, gitea_resolve_task_capability and gitea_activate_profile each seeded the session without repository/org. Because first-bind-wins is correct and deliberate, the mutation gate's later seed became a no-op, and assess_session_context only compares repository/org when the bound fields are non-null — so those two checks never ran. Separately, the gate resolved org or REMOTES[remote]["org"] and repo 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 REMOTES defaults was measured, not assumed:

simulated first bind with trusted REMOTES defaults (org=Scaled-Tech-Consulting, repo=Timesheet):
  attack repo=Other-Tools  -> BLOCKED        (defect closed)
  LEGIT  repo=Gitea-Tools  -> BLOCKED        reasons: ["repository drift: live 'Gitea-Tools' != bound 'Timesheet' (fail closed)"]

REMOTES['prgs'].repo is Timesheet and REMOTES['dadeschools'].org is Contractor — 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

  • Trusted source of scope: a new optional profile field allowed_repositories — canonical owner/repository slugs. It is config-only: gitea_auth.get_profile reads 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 by gitea_config._validate_allowed_repositories.
  • Trusted source of identity: the session repository is derived from the verified, workspace-aligned git remote (_workspace_repository_slugremote_repo_guard.parse_org_repo_from_remote_url), never from a caller-supplied org/repo, and never from REMOTES.
  • Boundary versus binding: allowed_repositories is an authorization boundary only. The workspace-derived slug is checked against it and the session binds immutably to that single canonical owner/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.
  • Every entry point binds completely: whoami, runtime context, capability preflight, activation and the mutation gate all seed through one path that derives the trusted repository, so no ordering leaves repository/org unset.

Fail-closed behavior:

  • Activation is rejected when the workspace repository is unverifiable or absent from the allowlist (blocker_kind: repository_scope).
  • Mutation is rejected when no verified workspace repository can be established (require_complete).
  • A tool-level org/repo override that disagrees with the binding is rejected before the write (assess_repository_override).
  • A mutation request can no longer establish, complete, or replace the binding.

Enforcement is opt-in per profile: a profile that omits allowed_repositories keeps prior behaviour, so existing static-profile namespaces stay functional (#714 AC8). Operator note: enforcement activates for prgs-author once "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

  • Trusted values originate from the workspace git remote (identity) and the config-only profile allowlist (authorization). Never from REMOTES, never from caller arguments, never from env.
  • Production seed/bind call sites audited: 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.
  • No first-bind path can still create repository=None/org=None for a scoped profile: an unverifiable workspace fails closed rather than binding empty.
  • Caller-controlled arguments cannot establish or replace the binding; they are only compared against it.
  • Repository/organization checks occur before mutation, in the gate that returns before any API call.
  • gitea_activate_profile remains the only sanctioned rebind and now checks scope at that boundary.
  • The private pytest-only reset is unchanged and still rejected outside a pytest boundary.
  • One real gap was found by these tests during development: gitea_auth.get_profile built an explicit whitelist dict and silently dropped allowed_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_complete
  • gitea_mcp_server.py — workspace-derived trusted repository; all bind sites; gate override checks; activation scope gate
  • gitea_auth.pyallowed_repositories passthrough (config-only)
  • gitea_config.py — schema checks
  • gitea-mcp.v2-contexts.example.json — synthetic example
  • docs/gitea-execution-profiles.md — field + "Repository scope (#714)" section
  • tests/test_issue_714_production_first_bind.py — new, 26 integration tests

Exact verification

Run from the PR #715 worktree only, with venv/bin/python (bare python3 is aliased to 3.13 and has no pytest):

  1. 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
  2. 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
  3. 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
  4. 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
  5. 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_profile passthrough 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_default as 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 to mirror_refs.sh with 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-existing StarletteDeprecationWarning in tests/test_webui_audit.py.

Security readback

  • Production session bindings remain immutable; first-bind-wins, the RLock, profile isolation, host checks and identity checks are unchanged.
  • No security assertion was weakened, removed, skipped, or rewritten. The change adds gates; the only behaviour removed is the mutation gate's ability to seed the binding from caller-supplied org/repo, which was the defect.
  • No mutation gate was bypassed. Repository, organization, host, profile and identity mismatches all fail closed before the mutation implementation is entered.
  • The reset mechanism remains unavailable to production callers (private, plus PYTEST_CURRENT_TEST required, else RuntimeError).
  • No secrets and no private profile configuration were committed; the example config uses synthetic values only.
  • The Gitea-Tools control checkout was untouched: still on master at 1eafb757, carrying only its pre-existing untracked files.
  • No eAgenda repository or worktree was accessed or modified; eAgenda PR #194 untouched. PR #710 untouched.

Request

Requesting a fresh independent prgs-reviewer review of PR #715 at head 943d40270e9ecdee48707eb09454f0321e8e4e14. Author identity posted no review verdict and performed no merge.

## 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 943d40270e9ecdee48707eb09454f0321e8e4e14 NEXT_PROMPT: ```text REVIEWER task — Gitea-Tools PR #715 (Issue #714), Scaled-Tech-Consulting/Gitea-Tools, remote prgs. Use the native gitea-reviewer namespace with the prgs-reviewer profile. Acquire your own reviewer lease. Pin and verify head 943d40270e9ecdee48707eb09454f0321e8e4e14. Review 437 was recorded at 632c5688 and predates three pushes. Scope: Issue #714 AC 1-8 plus the repository-scope enforcement added at this head. Verify the reproduced first-bind defect is closed: - After gitea_whoami on prgs, the context carries repository=Gitea-Tools and org=Scaled-Tech-Consulting (previously None/None). - Same-host repo=Other-Tools and org=Other-Org now fail closed. - Cross-host still fails closed. - A legitimate Gitea-Tools mutation still succeeds. Verify the trusted source: the session repository is derived from the workspace git remote and checked against the profile allowed_repositories. Confirm REMOTES is NOT used for scope (REMOTES['prgs'].repo is Timesheet — a default target, not a scope; binding it would fail closed on all Gitea-Tools work). Confirm a mutation request cannot establish, complete, or replace the binding, that org/repo overrides are checked against it, that first-bind-wins and the pytest-only reset are intact, and that allowed_repositories cannot be set from an environment variable. Run from a reviewer worktree with the project venv (bare python3 is aliased to 3.13, no pytest): venv/bin/python -m pytest tests/ -q -s Author measured 2739 passed, 6 skipped, 1 pre-existing warning, 161 subtests, 25.80s. Note: tests/test_mirror_refs.py::TestDryRunBanner::test_dry_run_banner_shown_by_default is an environmental flake (shells out to mirror_refs.sh with a 30s timeout and network/credential access). It failed once under load and passed in isolation, in its module, at the prior head, and on a clean full-suite re-run. Do not merge. Record a formal verdict via the review API only. ``` 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_repositories` authorization boundary, and binds it immutably. WHY: `gitea_whoami`, `gitea_get_runtime_context`, `gitea_resolve_task_capability` and `gitea_activate_profile` all seeded the context without repository/org. First-bind-wins then made the mutation gate's later seed a no-op, and `assess_session_context` only compares repository/org when the bound fields are non-null. The mutation gate also derived its values from `org 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: 943d40270e9ecdee48707eb09454f0321e8e4e14 PRIOR_HEAD_SHA: d936da5c872fb4bad31536d80b7c5f57462b05a9 REVIEWED_HEAD_SHA: 632c568865cab3de7a8d9970463ec182578d0a86 REVIEW_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_head` is 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 holds `prgs-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-immutability` since 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 `943d40270e9ecdee48707eb09454f0321e8e4e14` against 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: ``` after gitea_whoami(remote="prgs"): repository=None org=None same-host repo=Other-Tools -> NOT BLOCKED same-host org=Other-Org -> NOT BLOCKED cross-host dadeschools -> BLOCKED (this part always worked) ``` `gitea_whoami`, `gitea_get_runtime_context`, `gitea_resolve_task_capability` and `gitea_activate_profile` each seeded the session without repository/org. Because first-bind-wins is correct and deliberate, the mutation gate's later seed became a no-op, and `assess_session_context` only compares repository/org when the bound fields are non-null — so those two checks never ran. Separately, the gate resolved `org or REMOTES[remote]["org"]` and `repo 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 `REMOTES` defaults was measured, not assumed: ``` simulated first bind with trusted REMOTES defaults (org=Scaled-Tech-Consulting, repo=Timesheet): attack repo=Other-Tools -> BLOCKED (defect closed) LEGIT repo=Gitea-Tools -> BLOCKED reasons: ["repository drift: live 'Gitea-Tools' != bound 'Timesheet' (fail closed)"] ``` `REMOTES['prgs'].repo` is `Timesheet` and `REMOTES['dadeschools'].org` is `Contractor` — 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 - **Trusted source of scope:** a new optional profile field `allowed_repositories` — canonical `owner/repository` slugs. It is **config-only**: `gitea_auth.get_profile` reads 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 by `gitea_config._validate_allowed_repositories`. - **Trusted source of identity:** the session repository is derived from the **verified, workspace-aligned git remote** (`_workspace_repository_slug` → `remote_repo_guard.parse_org_repo_from_remote_url`), never from a caller-supplied `org`/`repo`, and never from `REMOTES`. - **Boundary versus binding:** `allowed_repositories` is an authorization boundary only. The workspace-derived slug is checked against it and the session binds immutably to that single canonical `owner/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. - **Every entry point binds completely:** whoami, runtime context, capability preflight, activation and the mutation gate all seed through one path that derives the trusted repository, so no ordering leaves repository/org unset. Fail-closed behavior: - Activation is rejected when the workspace repository is unverifiable or absent from the allowlist (`blocker_kind: repository_scope`). - Mutation is rejected when no verified workspace repository can be established (`require_complete`). - A tool-level `org`/`repo` override that disagrees with the binding is rejected before the write (`assess_repository_override`). - A mutation request can no longer establish, complete, or replace the binding. Enforcement is opt-in per profile: a profile that omits `allowed_repositories` keeps prior behaviour, so existing static-profile namespaces stay functional (#714 AC8). **Operator note:** enforcement activates for `prgs-author` once `"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 - Trusted values originate from the workspace git remote (identity) and the config-only profile allowlist (authorization). Never from `REMOTES`, never from caller arguments, never from env. - Production seed/bind call sites audited: `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. - No first-bind path can still create `repository=None`/`org=None` for a scoped profile: an unverifiable workspace fails closed rather than binding empty. - Caller-controlled arguments cannot establish or replace the binding; they are only compared against it. - Repository/organization checks occur before mutation, in the gate that returns before any API call. - `gitea_activate_profile` remains the only sanctioned rebind and now checks scope at that boundary. - The private pytest-only reset is unchanged and still rejected outside a pytest boundary. - One real gap was found by these tests during development: `gitea_auth.get_profile` built an explicit whitelist dict and silently dropped `allowed_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_complete` - `gitea_mcp_server.py` — workspace-derived trusted repository; all bind sites; gate override checks; activation scope gate - `gitea_auth.py` — `allowed_repositories` passthrough (config-only) - `gitea_config.py` — schema checks - `gitea-mcp.v2-contexts.example.json` — synthetic example - `docs/gitea-execution-profiles.md` — field + "Repository scope (#714)" section - `tests/test_issue_714_production_first_bind.py` — new, 26 integration tests ### Exact verification Run from the PR #715 worktree only, with `venv/bin/python` (bare `python3` is aliased to 3.13 and has no pytest): 1. `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** 2. `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** 3. `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** 4. `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** 5. `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_profile` passthrough 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_default` as 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 to `mirror_refs.sh` with 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-existing `StarletteDeprecationWarning` in `tests/test_webui_audit.py`. ### Security readback - Production session bindings remain immutable; first-bind-wins, the RLock, profile isolation, host checks and identity checks are unchanged. - No security assertion was weakened, removed, skipped, or rewritten. The change adds gates; the only behaviour removed is the mutation gate's ability to seed the binding from caller-supplied `org`/`repo`, which was the defect. - No mutation gate was bypassed. Repository, organization, host, profile and identity mismatches all fail closed before the mutation implementation is entered. - The reset mechanism remains unavailable to production callers (private, plus `PYTEST_CURRENT_TEST` required, else `RuntimeError`). - No secrets and no private profile configuration were committed; the example config uses synthetic values only. - The Gitea-Tools control checkout was untouched: still on `master` at `1eafb757`, carrying only its pre-existing untracked files. - No eAgenda repository or worktree was accessed or modified; eAgenda PR #194 untouched. PR #710 untouched. ### Request Requesting a fresh independent `prgs-reviewer` review of PR #715 at head `943d40270e9ecdee48707eb09454f0321e8e4e14`. Author identity posted no review verdict and performed no merge.
Owner

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: d936da5c87
target_branch: master
target_branch_sha: none
last_activity: 2026-07-15T21:04:44Z
expires_at: 2026-07-15T23:04:44Z
blocker: manual-release

<!-- mcp-review-lease:v1 --> 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: d936da5c872fb4bad31536d80b7c5f57462b05a9 target_branch: master target_branch_sha: none last_activity: 2026-07-15T21:04:44Z expires_at: 2026-07-15T23:04:44Z blocker: manual-release
Owner

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: 943d40270e
target_branch: master
target_branch_sha: none
last_activity: 2026-07-15T21:05:30Z
expires_at: 2026-07-15T23:05:30Z
blocker: none

<!-- mcp-review-lease:v1 --> 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: 943d40270e9ecdee48707eb09454f0321e8e4e14 target_branch: master target_branch_sha: none last_activity: 2026-07-15T21:05:30Z expires_at: 2026-07-15T23:05:30Z blocker: none
jcwalker3 added 1 commit 2026-07-15 20:13:32 -05:00
Close the #714 remediation gap left at 943d402 where env-only mutation
profiles, REMOTES Timesheet defaults treated as explicit caller input, and
machine-dependent Git remotes produced false greens.

- Fail closed when mutations lack a config-backed profile with non-empty
  allowed_repositories; env ops cannot invent mutation authority.
- Preserve omitted-vs-explicit org/repo provenance through the mutation
  gate; omitted targets bind to the verified workspace repository.
- Prefer workspace-aligned Git remotes over historical Timesheet defaults
  in MCP _resolve and CLI resolve_remote.
- Centralize deterministic config-backed mutation fixtures; migrate
  mutation tests off env-only authority without weakening security
  assertions.

Full suite: 2744 passed, 6 skipped.
Author
Owner

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 943d402 or comment 11662
NEXT_PROMPT:

Role: prgs-reviewer (independent)
Repo: prgs / Scaled-Tech-Consulting / Gitea-Tools
PR: #715 @ dc899d23c85f879dfa5b2e03c376112f1ea8382a
Issue: #714

1. gitea_whoami + resolve review capability for prgs Gitea-Tools.
2. Treat comment 11662 and any review of 943d402 as superseded / non-authoritative.
3. Review the full diff from base master to dc899d2 for #714 acceptance:
   config-backed mutation authority, omitted-vs-explicit provenance,
   workspace-bound targets (not REMOTES Timesheet defaults),
   fail-closed Git/workspace path, fixture migration without weakened assertions.
4. Re-check full pytest green claim (2744 passed, 6 skipped).
5. Submit formal review only via native MCP; no offline import recovery.

WHAT_HAPPENED:

  • Author resumed documented WIP at branches/author-pr715-714-remediation on base 943d402.
  • Initial full suite: 2696 passed, 48 failed, 6 skipped.
  • Completed production security refactor + centralized mutation_profile_fixture migration across 35+ test modules.
  • Full suite after fix: 2744 passed, 6 skipped, 1 warning, 161 subtests passed (~30s).
  • Committed dc899d2 (fast-forward descendant of 943d402) and pushed without force to fix/issue-714-session-context-immutability.
  • Live PR #715 head is now dc899d2 (was 943d402).
  • Control checkout remains clean master at 1eafb757; original worktree branches/fix-issue-714-session-context-immutability left untouched.
  • This comment supersedes comment 11662.

WHY:
943d402 was 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:

  • Original 48 failures re-run: 48 passed
  • Full suite pytest tests/ -q: 2744 passed, 6 skipped, 1 warning, 161 subtests passed in ~30s
  • Security readback: env-only cannot mutate; config omission fails closed; malformed scope fails closed; Git failure fails closed; workspace alignment before remote trust; omitted targets use immutable binding; REMOTES defaults not caller overrides; explicit drift fails closed; #530 intact; first-bind-wins/concurrency intact; private test reset; no auto-switch; no security assertion weakened.

LAST_UPDATED_BY: prgs-author / jcwalker3 (author remediation continuation)

Delivery heads

  • Old PR head: 943d40270e
  • New PR head: dc899d23c8
  • Branch: fix/issue-714-session-context-immutability (fast-forward, no force)
## 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 943d402 or comment 11662 NEXT_PROMPT: ```text Role: prgs-reviewer (independent) Repo: prgs / Scaled-Tech-Consulting / Gitea-Tools PR: #715 @ dc899d23c85f879dfa5b2e03c376112f1ea8382a Issue: #714 1. gitea_whoami + resolve review capability for prgs Gitea-Tools. 2. Treat comment 11662 and any review of 943d402 as superseded / non-authoritative. 3. Review the full diff from base master to dc899d2 for #714 acceptance: config-backed mutation authority, omitted-vs-explicit provenance, workspace-bound targets (not REMOTES Timesheet defaults), fail-closed Git/workspace path, fixture migration without weakened assertions. 4. Re-check full pytest green claim (2744 passed, 6 skipped). 5. Submit formal review only via native MCP; no offline import recovery. ``` WHAT_HAPPENED: - Author resumed documented WIP at branches/author-pr715-714-remediation on base 943d402. - Initial full suite: 2696 passed, 48 failed, 6 skipped. - Completed production security refactor + centralized mutation_profile_fixture migration across 35+ test modules. - Full suite after fix: 2744 passed, 6 skipped, 1 warning, 161 subtests passed (~30s). - Committed dc899d2 (fast-forward descendant of 943d402) and pushed without force to fix/issue-714-session-context-immutability. - Live PR #715 head is now dc899d2 (was 943d402). - Control checkout remains clean master at 1eafb757; original worktree branches/fix-issue-714-session-context-immutability left untouched. - This comment supersedes comment 11662. WHY: 943d402 was 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: - Original 48 failures re-run: 48 passed - Full suite pytest tests/ -q: 2744 passed, 6 skipped, 1 warning, 161 subtests passed in ~30s - Security readback: env-only cannot mutate; config omission fails closed; malformed scope fails closed; Git failure fails closed; workspace alignment before remote trust; omitted targets use immutable binding; REMOTES defaults not caller overrides; explicit drift fails closed; #530 intact; first-bind-wins/concurrency intact; private test reset; no auto-switch; no security assertion weakened. LAST_UPDATED_BY: prgs-author / jcwalker3 (author remediation continuation) ### Delivery heads - Old PR head: 943d40270e9ecdee48707eb09454f0321e8e4e14 - New PR head: dc899d23c85f879dfa5b2e03c376112f1ea8382a - Branch: fix/issue-714-session-context-immutability (fast-forward, no force)
Owner

repo: 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

<!-- mcp-review-lease:v1 --> repo: 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
sysadmin requested changes 2026-07-15 22:47:45 -05:00
sysadmin left a comment
Owner

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 in gitea_mcp_server.py. Please resolve the conflicts and update the branch.

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 in `gitea_mcp_server.py`. Please resolve the conflicts and update the branch.
Owner

Contaminated formal review quarantine (#695)

Status: QUARANTINED — merge authorization VOID

  • review_id: 440
  • pr: #715
  • reviewed_head_sha: dc899d23c85f879dfa5b2e03c376112f1ea8382a
  • actor: sysadmin
  • profile: prgs-reconciler
  • incident_issue: #717
  • reason: Procedurally invalid formal review under Issue #717 controller incident: review 440 was submitted after cross-binding remote=dadeschools with host=gitea.prgs.cc, violating immutable PRGS session-context / cross-instance-drift rules. Contaminated REQUEST_CHANGES must not count toward eligibility. Forensic Gitea review object retained; audit comment 11725 and lease comment 11727 preserved. No replacement review; no merge.
  • created_at: 2026-07-16T04:11:03.091864+00:00
  • native_transport: True
  • native_token_fingerprint: 6b6baa66ef52be1b
  • forensic_comment_ids retained: [11725, 11727]

This record does not delete Gitea reviews or historical comments. Fresh native-MCP re-review is required before merge.

## Contaminated formal review quarantine (#695) Status: **QUARANTINED — merge authorization VOID** - review_id: `440` - pr: `#715` - reviewed_head_sha: `dc899d23c85f879dfa5b2e03c376112f1ea8382a` - actor: `sysadmin` - profile: `prgs-reconciler` - incident_issue: `#717` - reason: Procedurally invalid formal review under Issue #717 controller incident: review 440 was submitted after cross-binding remote=dadeschools with host=gitea.prgs.cc, violating immutable PRGS session-context / cross-instance-drift rules. Contaminated REQUEST_CHANGES must not count toward eligibility. Forensic Gitea review object retained; audit comment 11725 and lease comment 11727 preserved. No replacement review; no merge. - created_at: `2026-07-16T04:11:03.091864+00:00` - native_transport: `True` - native_token_fingerprint: `6b6baa66ef52be1b` - forensic_comment_ids retained: `[11725, 11727]` This record does **not** delete Gitea reviews or historical comments. Fresh native-MCP re-review is required before merge.
This pull request has changes conflicting with the target branch.
  • gitea_mcp_server.py
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin fix/issue-714-session-context-immutability:fix/issue-714-session-context-immutability
git checkout fix/issue-714-session-context-immutability
Sign in to join this conversation.
No Reviewers
No labels
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Scaled-Tech-Consulting/Gitea-Tools#715