feat: thread worktree_path through gitea_create_issue (#450)

Hardens #274 branches-only guard for gitea_create_issue:

- verify_preflight_purity: validate resolved worktree exists, is a directory,
  and belongs to the target repository before author mutations.
- gitea_create_issue: add worktree_path threaded into preflight guard.
- tests/test_create_issue_workspace_guard.py: stable-control-checkout rejection
  and invalid-path fail-closed cases (PROJECT_ROOT simulated as control checkout).

Preserves WIP commit 8530109 implementation; test fix completes stable-checkout path.

Closes #450
This commit is contained in:
2026-07-07 14:41:06 -04:00
parent 1a1e679246
commit 81fcdb09fd
2 changed files with 189 additions and 4 deletions
+44 -4
View File
@@ -440,10 +440,48 @@ def verify_preflight_purity(remote: str | None = None, worktree_path: str | None
"Pre-flight order violation: Task capability (gitea_resolve_task_capability) has not been resolved (fail closed)"
)
if worktree_path:
dirty_files = sorted(_parse_porcelain_entries(_get_workspace_porcelain(worktree_path)))
workspace = author_mutation_worktree.resolve_mutation_workspace(
worktree_path,
PROJECT_ROOT,
active_worktree_env=os.environ.get(ACTIVE_WORKTREE_ENV),
author_worktree_env=os.environ.get(AUTHOR_WORKTREE_ENV),
)
real_workspace = os.path.realpath(workspace)
real_root = os.path.realpath(PROJECT_ROOT)
if real_workspace != real_root:
if not _preflight_in_test_mode():
if not os.path.exists(real_workspace):
raise RuntimeError(
f"Branches-only mutation guard (#274): worktree path '{workspace}' does not exist (fail closed)"
)
if not os.path.isdir(real_workspace):
raise RuntimeError(
f"Branches-only mutation guard (#274): worktree path '{workspace}' is not a directory (fail closed)"
)
try:
res = subprocess.run(
["git", "-C", real_workspace, "rev-parse", "--git-common-dir"],
capture_output=True,
text=True,
check=True,
)
common_dir = os.path.realpath(res.stdout.strip())
expected_dir = os.path.realpath(os.path.join(real_root, ".git"))
if common_dir != expected_dir:
raise RuntimeError(
f"Branches-only mutation guard (#274): worktree '{workspace}' does not belong to the target repository '{PROJECT_ROOT}' (fail closed)"
)
except Exception as e:
if isinstance(e, RuntimeError):
raise e
raise RuntimeError(
f"Branches-only mutation guard (#274): worktree '{workspace}' is not a valid git repository (fail closed)"
)
dirty_files = sorted(_parse_porcelain_entries(_get_workspace_porcelain(workspace)))
if dirty_files:
details = _preflight_workspace_details(worktree_path, dirty_files)
details = _preflight_workspace_details(workspace, dirty_files)
raise RuntimeError(
"Pre-flight order violation: Active task workspace has tracked "
"file edits before mutation (fail closed). "
@@ -972,6 +1010,7 @@ def gitea_create_issue(
repo: str | None = None,
allow_duplicate_override: bool = False,
split_from_issue: int | None = None,
worktree_path: str | None = None,
) -> dict:
"""Create a new issue on a Gitea repository.
@@ -984,6 +1023,7 @@ def gitea_create_issue(
repo: Override the repository name.
allow_duplicate_override: Operator-approved split after duplicate found.
split_from_issue: Existing duplicate issue number when overriding.
worktree_path: Optional path to verify branches-only guard.
Returns:
dict with 'number' of the created issue ('url' only with the reveal opt-in).
@@ -1010,7 +1050,7 @@ def gitea_create_issue(
)
if blocked:
return blocked
verify_preflight_purity(remote)
verify_preflight_purity(remote, worktree_path=worktree_path)
base = repo_api_url(h, o, r)
open_issues = api_get_all(f"{base}/issues?state=open&type=issues", auth)
closed_issues = api_get_all(