gitea_publish_unpublished_issue_branch fails closed for every caller: its own preflight discards the worktree_path it requires #816

Open
opened 2026-07-22 17:07:02 -05:00 by jcwalker3 · 0 comments
Owner

Problem statement

gitea_publish_unpublished_issue_branch (#812, landed in PR #814 at 99fda93) cannot succeed for any caller. Its preflight resolves the author worktree from the MCP process root instead of from the worktree_path argument the tool declares as required, so the #618 author-worktree gate blocks it against the stable control checkout before any of the tool's own ownership evidence is read.

Live invocation, gitea-author / prgs-author, parity clean at 910b6ed, mutation_safe: true, correct preflight order (gitea_whoamigitea_resolve_task_capability task publish_unpublished_branch, allowed_in_current_session: true):

reason_code: internal_error
exception_class: builtins.RuntimeError
detail: Durable author worktree resolution blocked (#618): author mutation blocked:
        workspace is the stable control checkout; author mutation requires an explicit
        validated worktree_path or a worktree derived …

dry_run=true fails identically, so there is no read-only path to the decision either.

Root cause

gitea_mcp_server.py:9176:

verify_preflight_purity(remote, task=task, org=org, repo=repo)

verify_preflight_purity accepts worktree_path (signature at gitea_mcp_server.py:1342) and passes it to _resolve_namespace_mutation_context(worktree_path). With the argument omitted it resolves None → the process root → the control checkout, and for role == "author" namespace_workspace_binding routes to author_mutation_worktree.resolve_durable_author_worktree, which by design refuses a control-checkout fallback and raises.

The tool's own worktree_path is not consulted until gitea_mcp_server.py:9180, four lines after the raise. Sibling worktree-scoped author tools forward it — edit_issue at 12302, the shared resolver at 1804, post_heartbeat's neighbour at 17505 all pass worktree_path=. This call site is the outlier.

Fix shape: forward the argument.

verify_preflight_purity(remote, worktree_path=worktree_path, task=task, org=org, repo=repo)

Why the test suite did not catch it

tests/test_issue_812_publish_unpublished_commit.py drives the real tool, but every case runs under pytest and the file sets neither GITEA_TEST_FORCE_ANTI_STOMP nor any purity_order_forced() signal. skip_purity_order (gitea_mcp_server.py:1381) is therefore true and the entire if not skip_purity_order: block — which contains the #618 resolution at 1417 — never executes. The suite exercises the tool's evidence model thoroughly and its preflight not at all.

The harness also applies patch.object(mcp_server, "PROJECT_ROOT", self.repo), so even were the gate live, the synthetic fixture would not reproduce the control-checkout collision that every real caller hits.

User and operational impact

#812 exists to break the entry-point-B deadlock: work already committed on a registered worktree, unpublished, where publication is lock-derived under #618 and the lock is withheld until a remote head exists. That deadlock is still live.

Issue #635 is the concrete casualty and is now on its sixth blocked cycle. Its state:

  • commit b2f6e9a6dc40e9651ef876f322dd0a68bddebfd8 on feat/issue-635-project-registry-api, tree clean, no remote head;
  • durable lock prgs-Scaled-Tech-Consulting-Gitea-Tools-635.json names claimant jcwalker3 / prgs-author and the correct worktree — exactly the ownership record this tool reads;
  • lock expired 2026-07-22T15:46:17Z while recorded pid 36856 is a live mcp_server.py daemon, so #601 reclaim (needs a dead pid) and #760 exact-owner renewal (needs a published head) both stay unreachable.

Every documented escape from that state routes through this tool.

Required implementation investigation

gitea_mcp_server.gitea_publish_unpublished_issue_branch (9105–9200), verify_preflight_purity (1340–1470), namespace_workspace_binding.resolve_namespace_workspace (60–120), author_mutation_worktree.resolve_durable_author_worktree (535+), tests/test_issue_812_publish_unpublished_commit.py.

Confirm no other verify_preflight_purity call site in a worktree-scoped mutation tool has the same omission — close_pr (8792), edit_pr (8794), commit_files (9056), delete_branch (10409), close_issue (12155) all pass no worktree_path and should each be classified as correct-by-lock-derivation or defective, rather than assumed.

Proposed direction

  1. Forward worktree_path at 9176.
  2. Add a regression case that runs the tool with purity-order enforcement on, from a process root that is the control checkout and a worktree_path that is a registered non-control worktree, asserting the call is not blocked by #618. This is the assertion whose absence let the defect ship.
  3. Keep every existing refusal intact: ownership still read from the lock file, never asserted by the caller; dirty/untracked, unregistered worktree, changed HEAD, non-ancestor remote head, competing open PR, and declared-hash mismatch all still refuse.

Security and workflow-safety requirements

Forwarding a caller-supplied path must not weaken #618. The explicit-worktree_path branch already validates registration and membership against the canonical root; the argument becomes a candidate, not a bypass. The lock file stays the sole source of ownership, and #812 AC23/AC24 hold — no lease renewal, reclaim, or clearing, and no consultation of the recorded owner pid's liveness.

Acceptance criteria

  1. gitea_publish_unpublished_issue_branch completes preflight when called with a valid registered worktree_path from a control-checkout process root.
  2. dry_run=true returns its decision and evidence rather than raising.
  3. Every pre-existing refusal is preserved, each with a test.
  4. A regression test exercises the tool with purity-order enforcement active, and fails against the current code.
  5. The audit of sibling call sites is recorded, with any further omission either fixed here or filed separately.

Required tests

Extend tests/test_issue_812_publish_unpublished_commit.py with a purity-order-enforced class; re-run the existing #812 suite unchanged; run the affected lock/worktree/preflight suites.

Observability and audit requirements

The block surfaces as internal_error with exception_class: builtins.RuntimeError, which reads as a server fault rather than a gate decision. Prefer a typed precondition refusal carrying blocker_kind and exact_safe_next_action if that can be done without broadening the gate.

Dependencies and linkage

Canonical issue state

STATE: ready-for-author
WHO_IS_NEXT: author
NEXT_ACTION: Forward worktree_path into verify_preflight_purity at gitea_mcp_server.py:9176 and add the purity-order-enforced regression test
NEXT_PROMPT: Author implements the preflight forwarding fix and its regression coverage only; PR; stop

Required final evidence

PR with the fix, the regression test that fails on current master, the sibling call-site audit, and full-suite results classified against a clean master baseline.

Required final response and handoff expectations

Brief PR handoff to reviewer, naming #635 as the unblocked consumer.

## Problem statement `gitea_publish_unpublished_issue_branch` (#812, landed in PR #814 at `99fda93`) cannot succeed for any caller. Its preflight resolves the author worktree from the MCP process root instead of from the `worktree_path` argument the tool declares as **required**, so the #618 author-worktree gate blocks it against the stable control checkout before any of the tool's own ownership evidence is read. Live invocation, `gitea-author` / `prgs-author`, parity clean at `910b6ed`, `mutation_safe: true`, correct preflight order (`gitea_whoami` → `gitea_resolve_task_capability` task `publish_unpublished_branch`, `allowed_in_current_session: true`): ```text reason_code: internal_error exception_class: builtins.RuntimeError detail: Durable author worktree resolution blocked (#618): author mutation blocked: workspace is the stable control checkout; author mutation requires an explicit validated worktree_path or a worktree derived … ``` `dry_run=true` fails identically, so there is no read-only path to the decision either. ## Root cause `gitea_mcp_server.py:9176`: ```python verify_preflight_purity(remote, task=task, org=org, repo=repo) ``` `verify_preflight_purity` accepts `worktree_path` (signature at `gitea_mcp_server.py:1342`) and passes it to `_resolve_namespace_mutation_context(worktree_path)`. With the argument omitted it resolves `None` → the process root → the control checkout, and for `role == "author"` `namespace_workspace_binding` routes to `author_mutation_worktree.resolve_durable_author_worktree`, which by design refuses a control-checkout fallback and raises. The tool's own `worktree_path` is not consulted until `gitea_mcp_server.py:9180`, four lines after the raise. Sibling worktree-scoped author tools forward it — `edit_issue` at `12302`, the shared resolver at `1804`, `post_heartbeat`'s neighbour at `17505` all pass `worktree_path=`. This call site is the outlier. Fix shape: forward the argument. ```python verify_preflight_purity(remote, worktree_path=worktree_path, task=task, org=org, repo=repo) ``` ## Why the test suite did not catch it `tests/test_issue_812_publish_unpublished_commit.py` drives the real tool, but every case runs under pytest and the file sets neither `GITEA_TEST_FORCE_ANTI_STOMP` nor any `purity_order_forced()` signal. `skip_purity_order` (`gitea_mcp_server.py:1381`) is therefore true and the entire `if not skip_purity_order:` block — which contains the #618 resolution at `1417` — never executes. The suite exercises the tool's evidence model thoroughly and its preflight not at all. The harness also applies `patch.object(mcp_server, "PROJECT_ROOT", self.repo)`, so even were the gate live, the synthetic fixture would not reproduce the control-checkout collision that every real caller hits. ## User and operational impact #812 exists to break the entry-point-B deadlock: work already committed on a registered worktree, unpublished, where publication is lock-derived under #618 and the lock is withheld until a remote head exists. That deadlock is still live. Issue #635 is the concrete casualty and is now on its sixth blocked cycle. Its state: * commit `b2f6e9a6dc40e9651ef876f322dd0a68bddebfd8` on `feat/issue-635-project-registry-api`, tree clean, no remote head; * durable lock `prgs-Scaled-Tech-Consulting-Gitea-Tools-635.json` names claimant `jcwalker3` / `prgs-author` and the correct worktree — exactly the ownership record this tool reads; * lock expired `2026-07-22T15:46:17Z` while recorded pid `36856` is a live `mcp_server.py` daemon, so #601 reclaim (needs a dead pid) and #760 exact-owner renewal (needs a published head) both stay unreachable. Every documented escape from that state routes through this tool. ## Required implementation investigation `gitea_mcp_server.gitea_publish_unpublished_issue_branch` (9105–9200), `verify_preflight_purity` (1340–1470), `namespace_workspace_binding.resolve_namespace_workspace` (60–120), `author_mutation_worktree.resolve_durable_author_worktree` (535+), `tests/test_issue_812_publish_unpublished_commit.py`. Confirm no other `verify_preflight_purity` call site in a worktree-scoped mutation tool has the same omission — `close_pr` (8792), `edit_pr` (8794), `commit_files` (9056), `delete_branch` (10409), `close_issue` (12155) all pass no `worktree_path` and should each be classified as correct-by-lock-derivation or defective, rather than assumed. ## Proposed direction 1. Forward `worktree_path` at `9176`. 2. Add a regression case that runs the tool with purity-order enforcement **on**, from a process root that is the control checkout and a `worktree_path` that is a registered non-control worktree, asserting the call is not blocked by #618. This is the assertion whose absence let the defect ship. 3. Keep every existing refusal intact: ownership still read from the lock file, never asserted by the caller; dirty/untracked, unregistered worktree, changed HEAD, non-ancestor remote head, competing open PR, and declared-hash mismatch all still refuse. ## Security and workflow-safety requirements Forwarding a caller-supplied path must not weaken #618. The explicit-`worktree_path` branch already validates registration and membership against the canonical root; the argument becomes a *candidate*, not a bypass. The lock file stays the sole source of ownership, and #812 AC23/AC24 hold — no lease renewal, reclaim, or clearing, and no consultation of the recorded owner pid's liveness. ## Acceptance criteria 1. `gitea_publish_unpublished_issue_branch` completes preflight when called with a valid registered `worktree_path` from a control-checkout process root. 2. `dry_run=true` returns its decision and evidence rather than raising. 3. Every pre-existing refusal is preserved, each with a test. 4. A regression test exercises the tool with purity-order enforcement active, and fails against the current code. 5. The audit of sibling call sites is recorded, with any further omission either fixed here or filed separately. ## Required tests Extend `tests/test_issue_812_publish_unpublished_commit.py` with a purity-order-enforced class; re-run the existing #812 suite unchanged; run the affected lock/worktree/preflight suites. ## Observability and audit requirements The block surfaces as `internal_error` with `exception_class: builtins.RuntimeError`, which reads as a server fault rather than a gate decision. Prefer a typed `precondition` refusal carrying `blocker_kind` and `exact_safe_next_action` if that can be done without broadening the gate. ## Dependencies and linkage * Regressed: #812 (PR #814, `99fda93`) · Blocks: #635 · Related: #618, #601, #760, #772 ## Canonical issue state ```text STATE: ready-for-author WHO_IS_NEXT: author NEXT_ACTION: Forward worktree_path into verify_preflight_purity at gitea_mcp_server.py:9176 and add the purity-order-enforced regression test NEXT_PROMPT: Author implements the preflight forwarding fix and its regression coverage only; PR; stop ``` ## Required final evidence PR with the fix, the regression test that fails on current master, the sibling call-site audit, and full-suite results classified against a clean master baseline. ## Required final response and handoff expectations Brief PR handoff to reviewer, naming #635 as the unblocked consumer.
jcwalker3 added the status:readyworkflow-hardeningtype:bug labels 2026-07-22 17:07:02 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

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