feat(mcp): client/session-aware runtime ownership and provenance (Closes #948) #968

Merged
sysadmin merged 2 commits from feat/issue-948-client-session-provenance into master 2026-07-29 03:29:47 -05:00
Owner

Closes #948.

Root cause

Two surfaces computed provenance independently and disagreed about one process.

  • gitea_get_runtime_context called _is_client_managed_process(), which reads the live os.environ, and reported server_provenance="client_managed".
  • mcp_namespace_health.classify_namespace_probe derived is_client_managed from _safe_env_summary(process). That summary is filtered through SAFE_ENV_KEYS, which contains only GITEA_MCP_PROFILE, GITEA_PROFILE_NAME, GITEA_SERVICE, GITEA_EXECUTION_ROLE, GITEA_MCP_CONFIG — and none of the three provenance keys it then looked up. Those lookups could only ever return None, so this surface was structurally incapable of returning anything but manual_launch, for any process, ever.

An allowlist whose job is deciding what may be echoed was being used to decide what may be believed.

Underneath that, neither model could name which client or which session owned a runtime. Ownership was inferred from a process environment flag, which the launcher sets once and which says nothing about who owns the process now. So a healthy daemon serving a second client could not be distinguished from a duplicate, and the cohort scan walled every process sharing a profile.

Authoritative provenance model

New module mcp_worker_identity.py, consulted by runtime context, namespace health, namespace attachment, the fleet/cohort scan, capability resolution, and typed reconnect assessment.

It splits two claims the old code ran together:

Dimension Question Evidence
launch_provenance Was this hand-launched from a terminal? Environment — legitimate, because the launcher sets it. Keeps the #686 wall intact.
session_ownership Which live client session owns this runtime now? A live attachment record only. No environment flag can establish it.

is_client_managed stays bound to the launch dimension, so unifying the implementations changes which code decides, not what gets decided — nothing previously refused becomes permitted.

Invariants enforced: UNIQUE worker_identity, NOT UNIQUE role, NOT UNIQUE profile, EXCLUSIVE active lease target.

  • Identities are <llm-name>-<UTC-timestamp>-<short-sha>, digest over sha256(client + session_id + timestamp_ns + nonce).
  • Atomic SQLite registry (BEGIN IMMEDIATE) with fencing epochs.
  • Registering an existing identity fails closed — never replaced, adopted, or merged with. The caller mints a different identity.
  • A generation held by a live session cannot be claimed by a second one. A generation whose claimant is not live is taken over with a higher fencing epoch, so stale ownership cannot permanently strand a healthy daemon.
  • A superseded session presenting an old epoch is refused and performs no write.
  • Liveness is heartbeat freshness; a live PID cannot resurrect an expired record, and a dead PID withdraws liveness. Not PID comparison alone.
  • Refusals are scoped to a worker identity or generation — never profile-wide, never fleet-wide.

Peer processes use a stricter declared_only strategy: their stdin is not ours to inspect and they inherit GITEA_MCP_PROFILE from any shell that exported it, so only an explicit declaration counts.

Vocabulary change

Absence of proof is now unproven instead of asserted manual_launch. Both fail closed. The old label claimed a hand-launched terminal process the code had no evidence for, which is why remediation pointed operators at the wrong recovery.

Other defects closed

  • Reconnect guidance no longer defaults to Codex. DEFAULT_CLIENT becomes generic; an unidentified client gets host-agnostic steps, and gitea_request_mcp_reconnect(client=...) defaults to resolving the client from the live attachment record. Codex callers keep their exact steps.
  • resolve_bound_remote keeps a bound namespace on its remote rather than falling through to the dadeschools library default; a request contradicting the binding is refused.

Files changed

File Change
mcp_worker_identity.py new — the authority: identity, registry, assess_provenance, cohort classification, failure scoping, remote binding
gitea_mcp_server.py worker registration + accessors; _is_client_managed_process delegates; runtime context publishes both dimensions; cohort scan keyed on identity; codex literals removed
mcp_namespace_health.py classify_namespace_probe consumes the shared verdict instead of the allowlist-derived one
mcp_client_reconnect.py DEFAULT_CLIENT becomes generic; normalize_client shares alias resolution
tests/test_issue_948_client_session_provenance.py new — 43 cases
tests/test_issue_686_manual_mcp_provenance.py vocabulary assertion updated; every wall-preserving assertion kept
docs/remote-mcp/threat-model{,-anchors}.{md,json} #956 anchors restamped for line movement

Tests

pytest tests/test_issue_948_client_session_provenance.py     -> 43 passed
pytest <9 directly affected suites>                          -> 181 passed, 18 subtests
pytest <34 category suites: capability, runtime-context,
  namespace health/attachment, fleet/provenance, reconnect,
  profile/repo binding, session state, process generation,
  SQLite/registry persistence>                               -> 668 passed, 153 subtests

Full suite, run from a branches/ worktree, -p no:randomly:

Failed Passed Skipped
head fa510dd 28 5953 6
merge base 8eada1fb 28 5910 6

diff of the failing-ID sets is empty — the 28 are pre-existing at the merge base, proven by that comparison rather than assumed. The +43 passing are the new cases.

Deployment

Serving daemons run the code loaded at their boot commit, so the new surfaces appear only after the control checkout advances and the MCP namespaces are reconnected by the operator. No process kill, restart loop, raw API call, fallback transport, or configuration edit is required or performed.

The registry auto-creates at ~/.cache/gitea-tools/control-plane/worker_registry.sqlite3 (override GITEA_WORKER_REGISTRY_DB). Launchers may optionally set GITEA_MCP_CLIENT, GITEA_MCP_CLIENT_INSTANCE, GITEA_MCP_CLIENT_SESSION to name themselves; unset values are reported as unknown, never guessed.

Mutation accounting

No MCP configuration, session-state file, credential, or runtime process was modified. Under pytest the registry is opened only when a test pins a path, so no test run writes to the operator's registry.

Scope

#948 is a large issue. This PR delivers the authoritative provenance model and the surfaces named in the task: identity, ownership, fencing, liveness, cohort scoping, client naming, remote binding. Issue-lease acquisition through work_issue (AC9-AC16) and the merge-gate criteria (AC19-AC21) build on this model and are not in this change.

Scope boundary with #934: #934 defines provenance for a remote, non-stdio transport and is blocked on #931/#932. This PR governs client/session ownership for the local stdio fleet and leaves every stdio guard in force.

Ready for independent review. Not self-reviewed or self-merged.

Closes #948. ## Root cause Two surfaces computed provenance independently and disagreed about one process. - `gitea_get_runtime_context` called `_is_client_managed_process()`, which reads the live `os.environ`, and reported `server_provenance="client_managed"`. - `mcp_namespace_health.classify_namespace_probe` derived `is_client_managed` from `_safe_env_summary(process)`. That summary is filtered through `SAFE_ENV_KEYS`, which contains only `GITEA_MCP_PROFILE`, `GITEA_PROFILE_NAME`, `GITEA_SERVICE`, `GITEA_EXECUTION_ROLE`, `GITEA_MCP_CONFIG` — and **none** of the three provenance keys it then looked up. Those lookups could only ever return `None`, so this surface was structurally incapable of returning anything but `manual_launch`, for any process, ever. An allowlist whose job is deciding what may be **echoed** was being used to decide what may be **believed**. Underneath that, neither model could name *which* client or *which* session owned a runtime. Ownership was inferred from a process environment flag, which the launcher sets once and which says nothing about who owns the process now. So a healthy daemon serving a second client could not be distinguished from a duplicate, and the cohort scan walled every process sharing a profile. ## Authoritative provenance model New module `mcp_worker_identity.py`, consulted by runtime context, namespace health, namespace attachment, the fleet/cohort scan, capability resolution, and typed reconnect assessment. It splits two claims the old code ran together: | Dimension | Question | Evidence | |---|---|---| | `launch_provenance` | Was this hand-launched from a terminal? | Environment — legitimate, because the launcher sets it. Keeps the #686 wall intact. | | `session_ownership` | Which live client session owns this runtime now? | A live attachment record only. No environment flag can establish it. | `is_client_managed` stays bound to the launch dimension, so unifying the implementations changes *which code decides*, not *what gets decided* — nothing previously refused becomes permitted. Invariants enforced: `UNIQUE worker_identity`, `NOT UNIQUE role`, `NOT UNIQUE profile`, `EXCLUSIVE active lease target`. - Identities are `<llm-name>-<UTC-timestamp>-<short-sha>`, digest over `sha256(client + session_id + timestamp_ns + nonce)`. - Atomic SQLite registry (`BEGIN IMMEDIATE`) with fencing epochs. - Registering an existing identity fails closed — never replaced, adopted, or merged with. The caller mints a different identity. - A generation held by a *live* session cannot be claimed by a second one. A generation whose claimant is not live is taken over with a higher fencing epoch, so stale ownership cannot permanently strand a healthy daemon. - A superseded session presenting an old epoch is refused and performs no write. - Liveness is heartbeat freshness; a live PID cannot resurrect an expired record, and a dead PID withdraws liveness. Not PID comparison alone. - Refusals are scoped to a worker identity or generation — never profile-wide, never fleet-wide. Peer processes use a stricter `declared_only` strategy: their stdin is not ours to inspect and they inherit `GITEA_MCP_PROFILE` from any shell that exported it, so only an explicit declaration counts. ### Vocabulary change Absence of proof is now `unproven` instead of asserted `manual_launch`. Both fail closed. The old label claimed a hand-launched terminal process the code had no evidence for, which is why remediation pointed operators at the wrong recovery. ### Other defects closed - Reconnect guidance no longer defaults to Codex. `DEFAULT_CLIENT` becomes `generic`; an unidentified client gets host-agnostic steps, and `gitea_request_mcp_reconnect(client=...)` defaults to resolving the client from the live attachment record. Codex callers keep their exact steps. - `resolve_bound_remote` keeps a bound namespace on its remote rather than falling through to the `dadeschools` library default; a request contradicting the binding is refused. ## Files changed | File | Change | |---|---| | `mcp_worker_identity.py` | new — the authority: identity, registry, `assess_provenance`, cohort classification, failure scoping, remote binding | | `gitea_mcp_server.py` | worker registration + accessors; `_is_client_managed_process` delegates; runtime context publishes both dimensions; cohort scan keyed on identity; codex literals removed | | `mcp_namespace_health.py` | `classify_namespace_probe` consumes the shared verdict instead of the allowlist-derived one | | `mcp_client_reconnect.py` | `DEFAULT_CLIENT` becomes `generic`; `normalize_client` shares alias resolution | | `tests/test_issue_948_client_session_provenance.py` | new — 43 cases | | `tests/test_issue_686_manual_mcp_provenance.py` | vocabulary assertion updated; every wall-preserving assertion kept | | `docs/remote-mcp/threat-model{,-anchors}.{md,json}` | #956 anchors restamped for line movement | ## Tests ``` pytest tests/test_issue_948_client_session_provenance.py -> 43 passed pytest <9 directly affected suites> -> 181 passed, 18 subtests pytest <34 category suites: capability, runtime-context, namespace health/attachment, fleet/provenance, reconnect, profile/repo binding, session state, process generation, SQLite/registry persistence> -> 668 passed, 153 subtests ``` Full suite, run from a `branches/` worktree, `-p no:randomly`: | | Failed | Passed | Skipped | |---|---|---|---| | head `fa510dd` | 28 | 5953 | 6 | | merge base `8eada1fb` | 28 | 5910 | 6 | `diff` of the failing-ID sets is **empty** — the 28 are pre-existing at the merge base, proven by that comparison rather than assumed. The +43 passing are the new cases. ## Deployment Serving daemons run the code loaded at their boot commit, so the new surfaces appear only after the control checkout advances and the MCP namespaces are reconnected by the operator. No process kill, restart loop, raw API call, fallback transport, or configuration edit is required or performed. The registry auto-creates at `~/.cache/gitea-tools/control-plane/worker_registry.sqlite3` (override `GITEA_WORKER_REGISTRY_DB`). Launchers may optionally set `GITEA_MCP_CLIENT`, `GITEA_MCP_CLIENT_INSTANCE`, `GITEA_MCP_CLIENT_SESSION` to name themselves; unset values are reported as unknown, never guessed. ## Mutation accounting No MCP configuration, session-state file, credential, or runtime process was modified. Under pytest the registry is opened only when a test pins a path, so no test run writes to the operator's registry. ## Scope #948 is a large issue. This PR delivers the authoritative provenance model and the surfaces named in the task: identity, ownership, fencing, liveness, cohort scoping, client naming, remote binding. Issue-lease acquisition through `work_issue` (AC9-AC16) and the merge-gate criteria (AC19-AC21) build on this model and are not in this change. Scope boundary with #934: #934 defines provenance for a **remote, non-stdio transport** and is blocked on #931/#932. This PR governs client/session ownership for the **local stdio fleet** and leaves every stdio guard in force. Ready for independent review. Not self-reviewed or self-merged.
jcwalker3 added 2 commits 2026-07-29 01:55:47 -05:00
Two surfaces reported different provenance for one process.
`gitea_get_runtime_context` read the live environment and reported
`client_managed`; `mcp_namespace_health.classify_namespace_probe` derived
provenance from `_safe_env_summary()`, whose `SAFE_ENV_KEYS` allowlist never
contained `GITEA_CLIENT_MANAGED`, `GITEA_MCP_CLIENT_MANAGED`, or
`GITEA_SERVER_PROVENANCE`. That lookup could only ever miss, so the health
surface was structurally incapable of returning anything but `manual_launch`.

Neither model could name which client or which session owned a runtime, so a
healthy daemon serving a second client was indistinguishable from a duplicate,
and the profile-wide duplicate scan walled the whole fleet.

Introduce `mcp_worker_identity` as the one authority, splitting two claims the
old code ran together:

* launch provenance — was this hand-launched from a terminal? Answered from the
  environment, which is legitimate because the launcher sets it. Preserves the
  #686 wall unchanged.
* session ownership — which live client session owns this runtime now? Answered
  only from a live attachment record; no environment flag can establish it.

The module provides collision-resistant worker identities
(`<llm-name>-<UTC-timestamp>-<short-sha>`), an atomic SQLite registry with
fencing epochs, heartbeat-based liveness, generation takeover that supersedes
only a non-live claimant, cohort classification, and failure scoping.

Behaviour changes:

* Registering an existing worker identity fails closed; it is never replaced,
  adopted, or merged with. The caller mints a different identity instead.
* A generation held by a live session cannot be claimed by a second one. A
  generation whose claimant is not live is taken over with a higher fencing
  epoch, so stale ownership cannot permanently strand a healthy daemon.
* A superseded session presenting an old epoch is refused and performs no write.
* Liveness comes from heartbeat freshness; a live PID cannot resurrect an
  expired record, and a dead PID withdraws liveness.
* Workers sharing a role or profile no longer trigger a profile-wide duplicate
  block, provided each carries a distinct identity. Processes with no identity
  evidence remain classified as duplicates, so the #686 wall still holds.
* Runtime failures are scoped to a worker identity or generation, never to a
  profile or the fleet.
* Reconnect guidance no longer defaults to Codex. An unidentified client gets
  host-agnostic steps; `gitea_request_mcp_reconnect(client=...)` defaults to
  resolving the client from the live attachment record.
* `resolve_bound_remote` keeps a bound namespace on its remote instead of
  falling through to the `dadeschools` library default.

Absence of proof is now reported as `unproven` rather than asserted as
`manual_launch`. Both still fail closed — `is_client_managed` is unchanged, so
nothing previously refused is now permitted — but remediation names the proof
that is actually missing instead of describing a terminal launch it cannot
evidence. The #686 test is updated for that vocabulary and keeps every
wall-preserving assertion.

Threat-model anchors and their citations in docs/remote-mcp/threat-model.md are
restamped for the line movement in gitea_mcp_server.py.

Tests: tests/test_issue_948_client_session_provenance.py adds 43 cases covering
Codex/Gemini/Antigravity/Claude attachment, same-client new session, cross-client
takeover after a session ends, two live conflicting sessions, stale records,
missing attachment proof, environment flags without attachment, mixed
generations, duplicate cohorts, the hardcoded-client regression, explicit PRGS
selection, default-remote host drift, cross-surface agreement, and fail-closed
handling without false reconnect loops. Synthetic identifiers throughout.

Full suite from a branches/ worktree: 28F/5953P/6S at head vs 28F/5910P/6S at
merge base 8eada1fb, identical failing ID sets.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01F6Vomtndpq2gSBa88Tfcwy
The anchors and their citations moved in the previous commit because
gitea_mcp_server.py gained the #948 worker-identity block. The fixture still
named the commit the old line numbers resolved at, so the recorded provenance
pointed at a tree where the new numbers do not hold.

No anchor target or expectation changes; only the recorded commit does.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01F6Vomtndpq2gSBa88Tfcwy
Owner

repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #968
issue: none
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 59450-a817617163f8
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr968-fa510dd2
phase: claimed
candidate_head: none
target_branch: master
target_branch_sha: none
last_activity: 2026-07-29T07:27:20Z
expires_at: 2026-07-29T07:37:20Z
blocker: none

<!-- mcp-review-lease:v1 --> repo: Scaled-Tech-Consulting/Gitea-Tools pr: #968 issue: none reviewer_identity: sysadmin profile: prgs-reviewer session_id: 59450-a817617163f8 worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr968-fa510dd2 phase: claimed candidate_head: none target_branch: master target_branch_sha: none last_activity: 2026-07-29T07:27:20Z expires_at: 2026-07-29T07:37:20Z blocker: none
Owner

repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #968
issue: none
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 59450-a817617163f8
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr968-fa510dd2
phase: released
candidate_head: none
target_branch: master
target_branch_sha: none
last_activity: 2026-07-29T07:33:31Z
expires_at: 2026-07-29T07:43:31Z
blocker: manual-release

<!-- mcp-review-lease:v1 --> repo: Scaled-Tech-Consulting/Gitea-Tools pr: #968 issue: none reviewer_identity: sysadmin profile: prgs-reviewer session_id: 59450-a817617163f8 worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr968-fa510dd2 phase: released candidate_head: none target_branch: master target_branch_sha: none last_activity: 2026-07-29T07:33:31Z expires_at: 2026-07-29T07:43:31Z blocker: manual-release
Owner

repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #968
issue: #948
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 82756-059259bef0c8
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr968-fa510dd2
phase: claimed
candidate_head: fa510dd28d
target_branch: master
target_branch_sha: 8eada1fbe4
last_activity: 2026-07-29T07:48:23Z
expires_at: 2026-07-29T07:58:23Z
blocker: none

<!-- mcp-review-lease:v1 --> repo: Scaled-Tech-Consulting/Gitea-Tools pr: #968 issue: #948 reviewer_identity: sysadmin profile: prgs-reviewer session_id: 82756-059259bef0c8 worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr968-fa510dd2 phase: claimed candidate_head: fa510dd28dd5f9ca4f81e5b3048ceab584f0b7d7 target_branch: master target_branch_sha: 8eada1fbe45289d6b92d291b3d5e5e56ae64ac5d last_activity: 2026-07-29T07:48:23Z expires_at: 2026-07-29T07:58:23Z blocker: none
Owner

repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #968
issue: #948
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 82756-515cd5c58ac7
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr968-fa510dd2
phase: claimed
candidate_head: fa510dd28d
target_branch: master
target_branch_sha: 8eada1fbe4
last_activity: 2026-07-29T07:59:02Z
expires_at: 2026-07-29T08:09:02Z
blocker: none

<!-- mcp-review-lease:v1 --> repo: Scaled-Tech-Consulting/Gitea-Tools pr: #968 issue: #948 reviewer_identity: sysadmin profile: prgs-reviewer session_id: 82756-515cd5c58ac7 worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr968-fa510dd2 phase: claimed candidate_head: fa510dd28dd5f9ca4f81e5b3048ceab584f0b7d7 target_branch: master target_branch_sha: 8eada1fbe45289d6b92d291b3d5e5e56ae64ac5d last_activity: 2026-07-29T07:59:02Z expires_at: 2026-07-29T08:09:02Z blocker: none
sysadmin approved these changes 2026-07-29 02:59:40 -05:00
sysadmin left a comment
Owner

Canonical PR State

STATE: APPROVED at head fa510dd28dd5f9ca4f81e5b3048ceab584f0b7d7; formal APPROVE review submitted by sysadmin through the gitea-reviewer namespace.
WHO_IS_NEXT: merger
NEXT_ACTION: A merger session (profile prgs-merger) acquires or adopts the PR lease at head fa510dd28d and merges PR #968 into master, then closes #948 and cleans up branch feat/issue-948-client-session-provenance.
NEXT_PROMPT:

Invoke the canonical gitea-workflow skill as your first action.

Perform the merger-side completion for:
Scaled-Tech-Consulting/Gitea-Tools — PR #968 (Issue #948)

Use the sanctioned gitea-merger namespace for all Gitea reads and mutations.

Required gates:
1. Verify active profile prgs-merger, identity sysadmin, role merger, repository binding
   Scaled-Tech-Consulting/Gitea-Tools, runtime parity mutation-safe, and merge_pr capability
   resolves ready.
2. Load PR #968 from live state. Confirm it is open, mergeable, and that the current head is
   exactly fa510dd28dd5f9ca4f81e5b3048ceab584f0b7d7. If the head has advanced, stop and hand
   back to the reviewer — the approval below is head-scoped and does not carry forward.
3. Confirm through gitea_get_pr_review_feedback that a formal APPROVE by sysadmin is visible
   at that exact head, with approval_at_current_head true and no outstanding REQUEST_CHANGES.
4. Acquire or adopt the merger PR lease pinned to that head.
5. Merge PR #968 into master pinned to that exact head.
6. Verify from live state that the merge landed, then release the merger lease.
7. Report the merge commit SHA, issue #948 closure state, label hygiene, lease cleanup status,
   and WHO_IS_NEXT.

Do not re-review. Do not re-approve. Verify everything from live native state rather than from
this prompt.

WHAT_HAPPENED: Narrow live-state verification of PR #968 was performed through the gitea-reviewer namespace. A prior reviewer session had recorded gitea_mark_final_review_decision (ready_action approve, ready_expected_head_sha fa510dd28d) and then released its lease without ever submitting the formal verdict, leaving reviews empty and approval_visible false. This session re-verified every gate from live state, re-acquired the reviewer lease at the pinned head, and submitted the missing formal APPROVE.

WHY: The recorded final decision was never converted into a native review, so nothing on the PR proved the approval and no merger could act on it. Submitting the formal verdict at the exact head closes that gap without altering the decision that was already validated.

ISSUE: #948
HEAD_SHA: fa510dd28d
REVIEW_STATUS: APPROVE submitted by sysadmin at head fa510dd28d. No REQUEST_CHANGES verdict is open on this PR.
MERGE_READY: yes — open, mergeable true, approved at the current head, no blocking change requests.
BLOCKERS: none
VALIDATION: Targeted suites executed from a branches/ worktree pinned to head fa510dd28d — tests/test_issue_948_client_session_provenance.py, tests/test_issue_956_threat_model.py, tests/test_issue_686_manual_mcp_provenance.py — 66 passed, 6 subtests passed. Merge base against live prgs master resolves to 8eada1fbe4, matching the base stated in the PR body. Changed surface matches the declared scope exactly: 8 files, +2493/-97, with no out-of-scope paths in the diff. The #956 threat-model anchor guard resolves cleanly at this head, confirming the anchor restamp tracks the line movement this change introduces. The #686 manual-provenance wall assertions remain in force under the new vocabulary; moving to unproven changes the label, not what is refused.
NATIVE_REVIEW_PROOF: this verdict is a native MCP review mutation submitted through gitea_submit_pr_review on the gitea-reviewer namespace, pinned to expected_head_sha fa510dd28d. Namespace gitea-reviewer on remote prgs; active profile prgs-reviewer; authenticated identity sysadmin; role reviewer. Repository binding Scaled-Tech-Consulting/Gitea-Tools. PR author jcwalker3; reviewer sysadmin; author and reviewer are distinct accounts. Runtime parity: in_parity true, live_stale false, restart_required false, mutation_safe true, at 8eada1fbe4. gitea_resolve_task_capability(task='review_pr') returned allowed_in_current_session true, stop_required false. Reviewer lease session 82756-515cd5c58ac7, worktree branches/review-pr968-fa510dd2 pinned to the head above. Live PR state and native review state were read through gitea_view_pr and gitea_get_pr_review_feedback before this verdict.

Substantive basis for approval:

  • The root cause is correctly identified and correctly scoped. SAFE_ENV_KEYS is an echo allowlist, and classify_namespace_probe read provenance through it, so that surface could never return client_managed for any process. Routing both surfaces through one authority removes the disagreement at its source.
  • Splitting launch_provenance from session_ownership is the right decomposition. Keeping is_client_managed bound to the launch dimension preserves the existing wall, so unifying the implementations changes which code decides rather than what is decided.
  • Ownership refusals are scoped to a worker identity or generation rather than profile-wide or fleet-wide, and fencing epochs stop a dead claimant from permanently stranding a healthy daemon.

LAST_UPDATED_BY: sysadmin (prgs-reviewer), reviewer lease session 82756-515cd5c58ac7

## Canonical PR State STATE: APPROVED at head fa510dd28dd5f9ca4f81e5b3048ceab584f0b7d7; formal APPROVE review submitted by sysadmin through the gitea-reviewer namespace. WHO_IS_NEXT: merger NEXT_ACTION: A merger session (profile prgs-merger) acquires or adopts the PR lease at head fa510dd28dd5f9ca4f81e5b3048ceab584f0b7d7 and merges PR #968 into master, then closes #948 and cleans up branch feat/issue-948-client-session-provenance. NEXT_PROMPT: ```text Invoke the canonical gitea-workflow skill as your first action. Perform the merger-side completion for: Scaled-Tech-Consulting/Gitea-Tools — PR #968 (Issue #948) Use the sanctioned gitea-merger namespace for all Gitea reads and mutations. Required gates: 1. Verify active profile prgs-merger, identity sysadmin, role merger, repository binding Scaled-Tech-Consulting/Gitea-Tools, runtime parity mutation-safe, and merge_pr capability resolves ready. 2. Load PR #968 from live state. Confirm it is open, mergeable, and that the current head is exactly fa510dd28dd5f9ca4f81e5b3048ceab584f0b7d7. If the head has advanced, stop and hand back to the reviewer — the approval below is head-scoped and does not carry forward. 3. Confirm through gitea_get_pr_review_feedback that a formal APPROVE by sysadmin is visible at that exact head, with approval_at_current_head true and no outstanding REQUEST_CHANGES. 4. Acquire or adopt the merger PR lease pinned to that head. 5. Merge PR #968 into master pinned to that exact head. 6. Verify from live state that the merge landed, then release the merger lease. 7. Report the merge commit SHA, issue #948 closure state, label hygiene, lease cleanup status, and WHO_IS_NEXT. Do not re-review. Do not re-approve. Verify everything from live native state rather than from this prompt. ``` WHAT_HAPPENED: Narrow live-state verification of PR #968 was performed through the gitea-reviewer namespace. A prior reviewer session had recorded gitea_mark_final_review_decision (ready_action approve, ready_expected_head_sha fa510dd28dd5f9ca4f81e5b3048ceab584f0b7d7) and then released its lease without ever submitting the formal verdict, leaving reviews empty and approval_visible false. This session re-verified every gate from live state, re-acquired the reviewer lease at the pinned head, and submitted the missing formal APPROVE. WHY: The recorded final decision was never converted into a native review, so nothing on the PR proved the approval and no merger could act on it. Submitting the formal verdict at the exact head closes that gap without altering the decision that was already validated. ISSUE: #948 HEAD_SHA: fa510dd28dd5f9ca4f81e5b3048ceab584f0b7d7 REVIEW_STATUS: APPROVE submitted by sysadmin at head fa510dd28dd5f9ca4f81e5b3048ceab584f0b7d7. No REQUEST_CHANGES verdict is open on this PR. MERGE_READY: yes — open, mergeable true, approved at the current head, no blocking change requests. BLOCKERS: none VALIDATION: Targeted suites executed from a branches/ worktree pinned to head fa510dd28dd5f9ca4f81e5b3048ceab584f0b7d7 — tests/test_issue_948_client_session_provenance.py, tests/test_issue_956_threat_model.py, tests/test_issue_686_manual_mcp_provenance.py — 66 passed, 6 subtests passed. Merge base against live prgs master resolves to 8eada1fbe45289d6b92d291b3d5e5e56ae64ac5d, matching the base stated in the PR body. Changed surface matches the declared scope exactly: 8 files, +2493/-97, with no out-of-scope paths in the diff. The #956 threat-model anchor guard resolves cleanly at this head, confirming the anchor restamp tracks the line movement this change introduces. The #686 manual-provenance wall assertions remain in force under the new vocabulary; moving to `unproven` changes the label, not what is refused. NATIVE_REVIEW_PROOF: this verdict is a native MCP review mutation submitted through gitea_submit_pr_review on the gitea-reviewer namespace, pinned to expected_head_sha fa510dd28dd5f9ca4f81e5b3048ceab584f0b7d7. Namespace gitea-reviewer on remote prgs; active profile prgs-reviewer; authenticated identity sysadmin; role reviewer. Repository binding Scaled-Tech-Consulting/Gitea-Tools. PR author jcwalker3; reviewer sysadmin; author and reviewer are distinct accounts. Runtime parity: in_parity true, live_stale false, restart_required false, mutation_safe true, at 8eada1fbe45289d6b92d291b3d5e5e56ae64ac5d. gitea_resolve_task_capability(task='review_pr') returned allowed_in_current_session true, stop_required false. Reviewer lease session 82756-515cd5c58ac7, worktree branches/review-pr968-fa510dd2 pinned to the head above. Live PR state and native review state were read through gitea_view_pr and gitea_get_pr_review_feedback before this verdict. Substantive basis for approval: - The root cause is correctly identified and correctly scoped. SAFE_ENV_KEYS is an echo allowlist, and classify_namespace_probe read provenance through it, so that surface could never return client_managed for any process. Routing both surfaces through one authority removes the disagreement at its source. - Splitting launch_provenance from session_ownership is the right decomposition. Keeping is_client_managed bound to the launch dimension preserves the existing wall, so unifying the implementations changes which code decides rather than what is decided. - Ownership refusals are scoped to a worker identity or generation rather than profile-wide or fleet-wide, and fencing epochs stop a dead claimant from permanently stranding a healthy daemon. LAST_UPDATED_BY: sysadmin (prgs-reviewer), reviewer lease session 82756-515cd5c58ac7
Owner

repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #968
issue: #948
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 82756-515cd5c58ac7
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr968-fa510dd2
phase: released
candidate_head: fa510dd28d
target_branch: master
target_branch_sha: 8eada1fbe4
last_activity: 2026-07-29T08:00:09Z
expires_at: 2026-07-29T08:10:09Z
blocker: manual-release

<!-- mcp-review-lease:v1 --> repo: Scaled-Tech-Consulting/Gitea-Tools pr: #968 issue: #948 reviewer_identity: sysadmin profile: prgs-reviewer session_id: 82756-515cd5c58ac7 worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr968-fa510dd2 phase: released candidate_head: fa510dd28dd5f9ca4f81e5b3048ceab584f0b7d7 target_branch: master target_branch_sha: 8eada1fbe45289d6b92d291b3d5e5e56ae64ac5d last_activity: 2026-07-29T08:00:09Z expires_at: 2026-07-29T08:10:09Z blocker: manual-release
Owner

repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #968
issue: #948
reviewer_identity: sysadmin
profile: prgs-merger
session_id: 13686-d751649482af
worktree: /Users/jasonwalker/Development/Gitea-Tools
phase: claimed
candidate_head: fa510dd28d
target_branch: master
target_branch_sha: 8eada1fbe4
last_activity: 2026-07-29T08:28:57Z
expires_at: 2026-07-29T08:38:57Z
blocker: none

<!-- mcp-review-lease:v1 --> repo: Scaled-Tech-Consulting/Gitea-Tools pr: #968 issue: #948 reviewer_identity: sysadmin profile: prgs-merger session_id: 13686-d751649482af worktree: /Users/jasonwalker/Development/Gitea-Tools phase: claimed candidate_head: fa510dd28dd5f9ca4f81e5b3048ceab584f0b7d7 target_branch: master target_branch_sha: 8eada1fbe45289d6b92d291b3d5e5e56ae64ac5d last_activity: 2026-07-29T08:28:57Z expires_at: 2026-07-29T08:38:57Z blocker: none
sysadmin merged commit 956fa15fe3 into master 2026-07-29 03:29:47 -05:00
Owner

Stale #332 review-decision lock cleanup (#594)

Status: APPLIED

Manual deletion of session-state files is not the workflow.
This path only clears a lock when the referenced PR is merged/closed.

## Stale #332 review-decision lock cleanup (#594) Status: **APPLIED** - actor: `sysadmin` - profile: `prgs-merger` - timestamp: `2026-07-29T08:29:51.216814+00:00` - last terminal: `approve` on PR #968 - PR state: `closed` (merged=True) - merge_commit_sha: `956fa15fe3dab82c8669d5b257406e1e4c14929c` - prior live_mutations_count: `5` - prior profile_identity: `prgs-reviewer` Manual deletion of session-state files is **not** the workflow. This path only clears a lock when the referenced PR is merged/closed.
Owner

finalized_at: 2026-07-29T08:30:43Z
finalized_by_identity: sysadmin
finalized_by_profile: prgs-merger
finalized_by_session_id: 13686-d751649482af
finalization_outcome: released
finalization_reason: PR #968 merged at fa510dd2 into master as 956fa15f; merger lease finalized
finalized_lease_comment_id: 18460

repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #968
issue: #948
reviewer_identity: sysadmin
profile: prgs-merger
session_id: 13686-d751649482af
worktree: /Users/jasonwalker/Development/Gitea-Tools
phase: released
candidate_head: fa510dd28d
target_branch: master
target_branch_sha: 8eada1fbe4
last_activity: 2026-07-29T08:30:43Z
expires_at: 2026-07-29T08:40:43Z
blocker: PR #968 merged at fa510dd2 into master as 956fa15f; merger lease finalized

<!-- mcp-merger-lease-final:v1 --> finalized_at: 2026-07-29T08:30:43Z finalized_by_identity: sysadmin finalized_by_profile: prgs-merger finalized_by_session_id: 13686-d751649482af finalization_outcome: released finalization_reason: PR #968 merged at fa510dd2 into master as 956fa15f; merger lease finalized finalized_lease_comment_id: 18460 <!-- mcp-review-lease:v1 --> repo: Scaled-Tech-Consulting/Gitea-Tools pr: #968 issue: #948 reviewer_identity: sysadmin profile: prgs-merger session_id: 13686-d751649482af worktree: /Users/jasonwalker/Development/Gitea-Tools phase: released candidate_head: fa510dd28dd5f9ca4f81e5b3048ceab584f0b7d7 target_branch: master target_branch_sha: 8eada1fbe45289d6b92d291b3d5e5e56ae64ac5d last_activity: 2026-07-29T08:30:43Z expires_at: 2026-07-29T08:40:43Z blocker: PR #968 merged at fa510dd2 into master as 956fa15f; merger lease finalized
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#968