Resolve task_capability_map.py conflict by keeping all three capability
entries: commit_files and gitea_commit_files (gitea.repo.commit, #262,
from master) and reconcile_merged_cleanups (gitea.read, #269, this
branch). Capability boundaries unchanged — reconciliation reporting stays
read-only; no close/merge capability broadened.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Extends the single-terminal-decision machinery (#211) to session-level
hard-stop semantics:
- terminal_review_hard_stop_reasons: after a live terminal verdict, only
the merge sequence for the same approved PR may continue; REQUEST_CHANGES
stops the run; operator-approved corrections re-open the review path
only, never a cross-PR merge.
- gitea_merge_pr: new Gate 2b consults the hard-stop before any API call,
so a run can never verdict one PR and merge another.
- gitea_mark_final_review_decision: hard-stop check with explicit stop
guidance naming the consumed terminal mutation; plus duplicate
REQUEST_CHANGES suppression — an unresolved request-changes at the
current head SHA refuses a second request-changes (fail closed when
feedback cannot be verified).
- tests/test_terminal_review_hard_stop.py: 13 tests covering the
acceptance criteria; existing TestSubmitPrReview setups stub the new
feedback fetch.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Return wrapped {prs, pagination} from gitea_list_prs with has_more,
inventory_complete, and page traversal via api_fetch_page. Route
gitea_review_pr inventory through gitea_list_prs so trust gates can
prove pagination finality. Add assess_list_prs_pagination_proof verifier.
Closes#340
Implement gitea_reconcile_merged_cleanups with dry-run reporting for merged
PR remote branches and local branches/ worktrees, plus confirmation-gated
execute mode. Safety gates cover protected branches, open PR references,
active issue locks, dirty worktrees, and delete_branch capability.
Closes#269
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Allow issue lock from base-equivalent branches/ worktrees instead of
requiring the literal master/main branch name. Runtime preflight and
mark_issue/lock_issue now inspect the declared active task workspace so
dirty control-checkout state does not block clean task worktrees.
Closes#275
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Add commit_files and gitea_commit_files resolver entries for
gitea.repo.commit, align gitea_commit_files tool gates with other
issue-write mutations, and add regression tests for author allow,
reviewer deny, preflight order, and resolver/tool alignment.
Refs #262
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Pre-flight order tracking now compares porcelain baselines captured at
process start, whoami, and capability resolution instead of treating any
shared-worktree dirt as a sticky violation. Fresh gitea_whoami re-evaluates
and clears violations; runtime context reports preflight_block_reasons when
mutations would fail.
Refs #252
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Gitea expects review event APPROVED, not APPROVE; the wrong event left
submissions as PENDING drafts. Verify terminal verdicts after submit,
submit pending drafts when needed, and block merge when approval_visible
is false.
gitea_lock_issue and gitea_create_pr accept worktree_path so lock
preconditions validate the caller's scratch clone instead of the shared
MCP server CWD. Lock records store the validated path; PR creation fails
closed when the declared path does not match.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Fix 8 suite failures triggered when GITEA_MCP_CONFIG/GITEA_MCP_PROFILE
are set in the operator environment.
- TestGetAuthHeader: clear MCP config env and DYNAMIC_CONFIGS in setUp
- TestCreatePR/TestIssueLocking: add CREATE_PR_ENV author fixture and
patch reviewer-stop gate so tests reach intended code paths
Closes#227
Extract TASK_CAPABILITY_MAP into task_capability_map.py as the single source
of truth shared by gitea_resolve_task_capability and issue-mutating tools.
Gate gitea_create_issue, gitea_close_issue, gitea_mark_issue, and
gitea_set_issue_labels with structured #142 permission reports. Add regression
tests proving resolver-denied tasks fail closed at the raw tool layer.
Implements mcp-control-plane #69.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
- Bind review decision state to in-process session (pid + profile lock); drop /tmp file path.
- Fail closed on review_pr.py CLI; route live reviews through gated MCP tools only.
- Require operator_authorized on review correction; allow re-mark after correction.
- Validate remote/org/repo on mark and submit; wire review mutation proof into build_final_report.
- Add security regression tests for spoofed locks, correction flow, and CLI bypass.
Refs #211
Reviewer agents could post probe APPROVE/REQUEST_CHANGES reviews while
testing lock paths, polluting PR audit trails. Add a review decision lock
seeded by gitea_resolve_task_capability(review_pr) that requires
gitea_mark_final_review_decision and final_review_decision_ready=True
before gitea_submit_pr_review performs a live mutation.
Add gitea_dry_run_pr_review for read-only submission validation,
gitea_authorize_review_correction for operator-approved fixes, and
assess_review_mutation_final_report for final-report proof. One live
review mutation per run unless correction is explicitly authorized.
Recreation of the #204 work from closed PR #205 (invalid provenance), rebuilt
cleanly on master under the prgs author identity with no PR #203 content:
- Add gitea_lock_issue MCP tool: locks exactly one issue to its branch name,
fails closed on branch/issue-number mismatch and on issues already tied to
an open PR (by head branch or Closes/Fixes reference).
- gitea_create_pr now requires the issue lock: head must match the locked
branch, title/body must contain Closes/Fixes #<locked issue> exactly, and
ambiguous references (equivalent / related / same as) are rejected.
- scripts/worktree-start refuses to create an issue-linked worktree unless the
lock file exists and matches the requested branch.
- assess_controller_handoff rejects handoffs whose selected issue / opened PR
fields carry multiple numbers or fuzzy equivalence wording.
- Tests: TestIssueLocking (lock + create_pr gates), handoff exact-reference
tests, worktree-start lock coverage.
Closes#204
Co-Authored-By: Claude Fable 5 <[email protected]>
Add server-side duplicate detection in gitea_create_issue that re-queries
open and recently closed issues at mutation time, blocks normalized title
matches unless the operator explicitly approves a split, and validates LLM
duplicate-search summaries via review_proofs.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
PR closure had no first-class capability: agents could close PRs through
gitea_edit_pr(state=closed) with no close-specific capability proof, and
gitea_resolve_task_capability(close_pr) failed as unknown, leaving the
broad edit path as an untracked close fallback.
Add close_pr to the resolver TASK_MAP (gitea.pr.close, author-side) and
gate gitea_edit_pr(state=closed) on the same operation: without it the
close fails closed before any auth or API call, with reasons and a
structured permission_report; with it the close proceeds and is audited
as a distinct close_pr action carrying the required capability. Reject
invalid state values outright so case variants cannot bypass the gate.
Generalize the profile gate helper (_profile_operation_gate) and document
the PR comment / PR edit / PR close capability split.
Co-Authored-By: Claude Fable 5 <[email protected]>
Addresses the sysadmin REQUEST_CHANGES on PR #203 (reviewed head
10d2644790):
1. Lock redesigned; /tmp file removed entirely. The mutation authority is
now an in-process record (_MUTATION_AUTHORITY) plus an environment
session lock (GITEA_SESSION_PROFILE_LOCK) exported at server launch:
- in-process record cannot be spoofed by other local processes, cannot
go stale across sessions, and cannot race concurrent agents;
- the env lock is inherited by child CLI processes, so review_pr.py can
refuse an ad-hoc GITEA_MCP_PROFILE role escalation without any shared
file; a missing env lock (direct operator CLI use) stays allowed;
- silent except-pass writes are gone; an unresolvable profile fails
closed.
2. Standard reviewer workflow unbroken: verify_mutation_authority seeds
itself from the live config-resolved context at the first mutation gate
(approved preflight path whoami -> eligibility -> review/merge), and now
runs as the final gate after eligibility, reusing the identity that
eligibility proved (no extra /user call).
3. Trailing whitespace removed from review_pr.py (git diff --check clean).
4. Module-global verify_mutation_authority no-op bypass removed from
tests/test_mcp_server.py; replaced with a tests/conftest.py autouse
fixture that only resets per-process state (_MUTATION_AUTHORITY,
_IDENTITY_CACHE, session lock env) between tests — the gate itself
stays live in every test.
5. Tests rewritten for the new design: seeding on first verify, unresolved
profile fails closed, remote/profile/identity mismatches fail closed,
session-lock env mismatch rejected, foreign-pid authority reseeded,
unauthorized author->reviewer pivot blocked, authorized pivot allowed;
CLI: mismatch blocked, match allowed, no-lock allowed.
6. Rebased onto current master (c6fd0fd).
Closes#199
Refs #194
Co-Authored-By: Claude Fable 5 <[email protected]>
PR #138 added the issue.comment alias and example grants. Add the
forward-direction separation guards the migration decision requires:
gitea.issue.comment never implies issue close, PR review/approve/merge,
or branch push / repo commit; gitea.pr.comment never implies
gitea.issue.comment; the pre-#137 live author op set still fails closed
with the exact operator-facing reason; and the shipped v1/v2 example
configs keep granting issue comments while preserving author/reviewer
role separation.
Refs #137, #139
Co-Authored-By: Claude Fable 5 <[email protected]>
Add gitea_list_issue_comments and gitea_create_issue_comment so
discussion/design workflows can read and post issue comments through
the MCP layer instead of direct API scripts.
- List requires gitea.read; create requires gitea.issue.comment —
gated separately from the gitea.pr.* review/merge family, fail closed.
- Issue comments never touch PR review endpoints.
- LLM-safe output: comment id/author/timestamps/body only; web links
appear solely under the GITEA_MCP_REVEAL_ENDPOINTS admin opt-in.
- Create operations are audit-logged (create_issue_comment) and errors
are redacted before being raised.
- Tests cover list/create success, permission blocks (including PR
review permissions not granting issue comments), forbidden-overrides,
empty body, missing issue with redacted error, endpoint separation,
and reveal opt-in.
- Document issue comments versus PR reviews in
docs/gitea-execution-profiles.md.
Closes#126
Co-Authored-By: Claude Fable 5 <[email protected]>
Support the canonical contexts-shape version 2 config (contexts / profiles /
projects / rules) alongside the existing environments shape and v1:
- Require a boolean 'enabled' on every context, profile, service, and
project. Disabled entries are surfaced in audits but fail closed at
selection/resolution — never a silent fallback to another profile,
service, or credential source.
- Resolve the active identity from GITEA_MCP_PROFILE via the existing
select_profile path; profile base_url falls back to the context's enabled
gitea block.
- Add resolve_service() and project_for_path() for context service and
project-to-context resolution (internal use; fail closed on disabled).
- get_auth_header now propagates ConfigError when GITEA_MCP_CONFIG is set
instead of silently degrading to Basic auth.
- Hide endpoint URLs and keychain ids from normal LLM-facing output:
gitea_whoami / gitea_get_profile report logical names and auth status
only; new gitea_audit_config tool reports enabled/disabled state and safe
one-line service summaries. The GITEA_MCP_REVEAL_ENDPOINTS opt-in (and
'python3 gitea_config.py audit --reveal-endpoints' locally) restores
endpoints and auth source names for admin diagnostics; token values are
never printed on any path.
- Ship gitea-mcp.v2-contexts.example.json (synthetic values) and validate
it in tests.
Implements #120
Co-Authored-By: Claude Fable 5 <[email protected]>
gitea_merge_pr ran cleanup_in_progress_for_pr inside the same try as the
post-merge read-back GET; a read-back failure silently skipped tracker
cleanup, leaving only merge_commit=null and no cleanup_status at all, so
status:in-progress could stay stuck while the merge read as full success.
Split the block: read-back failure now returns an explicit
cleanup_status='skipped (merge read-back failed)', and an unexpected
cleanup exception returns 'skipped (cleanup error: <redacted>)' instead of
masking merge_commit. Cleanup still never blocks a performed merge, the
happy-path API call sequence is unchanged, and _redact keeps credentials
out of surfaced errors.
Add regression tests: read-back failure => merge still performed, explicit
skip status, no tracker DELETE traffic; cleanup exception => surfaced and
redacted.
Fixes#98.
Co-Authored-By: Claude Fable 5 <[email protected]>
Harden gitea_auth.api_request: add a per-request timeout (env
GITEA_HTTP_TIMEOUT), convert timeouts and DNS/network failures
(URLError/TimeoutError) into clear RuntimeErrors, give 502/503/504 an
explicit 'upstream unavailable' message, convert malformed success JSON
into a clean error, and redact credential-like substrings from all error
text. Preserves the success path and existing 429 retry/backoff.
Add shared gitea_auth.api_get_all: page-based pagination that tolerates
missing/malformed metadata (relies on page length, not Link/X-Total-Count
headers), honors an optional overall limit, and caps pages. Wire it into
the read-only list tools gitea_list_issues, gitea_list_prs, and
gitea_list_labels (return shape unchanged).
Add tests/test_api_reliability.py (18 cases) and update the three list-tool
tests to the new call path. No auth/profile/merge/review/tracker behavior
changed. No modular #65 refactor.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Issue #9 requested getAuthenticatedUser and getCurrentUser in addition to whoami.
This adds the two aliased MCP tools and their corresponding unit tests.
gitea_edit_pr called _auth() (and resolved the remote) before checking whether
any editable field was provided, so a pure validation error (no fields) surfaced
as a RuntimeError "no credentials" in environments without Gitea auth — making
test_edit_pr_no_fields_raises depend on credentials/network/env.
Move the payload build + no-fields ValueError ahead of _resolve/_auth/URL setup.
Behavior is unchanged when fields are provided (same _resolve → _auth → audited
PATCH path). No change to auth, retry/backoff, audit, config profiles, or
worktree helpers.
Tests: add test_edit_pr_no_fields_validates_before_auth asserting the no-fields
path raises ValueError and calls neither get_auth_header nor api_request (even
with auth mocked to None). Existing edit-PR tests unchanged.
Full suite passes with no Gitea credentials (287 passed, 0 failures) — the
no-fields test no longer depends on the environment.
Closes#43.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Reviewer found the MCP merge surface is gated but two local CLI scripts remain
ungated merge paths that LLM automations in this project have been using.
Close them (Option B — minimal safe fix; full gated CLI merge left to a
follow-up):
- review_pr.py: `--merge` now fails closed BEFORE any API call with a clear
message directing callers to the gated `gitea_merge_pr` MCP workflow. The
review-only path is unchanged. The merge execution block was removed.
- merge_pr.py: main() is now a fail-closed no-op — reads no credentials and
makes no merge API call; prints that merge is only available via the gated
workflow.
- README: the `review_pr.py` row and Quick Examples no longer advertise a CLI
`--merge` path; added an audit-logging clarification that #16 returns
structured gate/merge results but does not add durable audit logging, which
is tracked by #18.
Tests updated/added:
- test_review_pr.py: `--merge` fails closed with no API call; message points to
the gated workflow.
- test_merge_pr.py: merge fails closed with no API call, even with
--force/--do/--title/--message; message points to the gated workflow.
- test_mcp_server.py: README no longer advertises the ungated CLI merge example.
The gated MCP `gitea_merge_pr` is unchanged and still gated; `gitea_review_pr`
still fails closed on merge=True; `gitea_submit_pr_review` still cannot merge.
No secrets, auth headers, raw env, or credential paths are exposed. No
Jenkins/Ops/GlitchTip/Release/deploy/CI behavior added. #17/#18 not started.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Replace the ungated gitea_merge_pr with a gated merge workflow. This is now
the only merge path the MCP server exposes; the merge API is called only
after every safety gate passes.
Gates (fail-closed at each step):
1. Merge method is merge | squash | rebase.
2. Explicit confirmation: confirmation must equal "MERGE PR <n>" (without it,
zero API calls are made).
3. Reuse gitea_check_pr_eligibility (#14) with action 'merge': proves the
authenticated identity, the active profile (and that it allows merge), the
PR author, blocks self-merge, requires the PR open, and fails closed when
the PR is not mergeable or mergeability is unknown.
4. Optional expected_head_sha: refuse if the PR head moved.
5. Optional expected_changed_files: refuse if the PR's changed file set differs.
6. Redundant self-merge block (auth user == PR author).
The force/ignore-checks option was removed — Gitea's own mergeable signal
(which reflects branch-protection required reviews/checks) must be positive,
so required approval/check state is honoured, never bypassed.
Output reports performed, authenticated user, profile name, PR author, PR
number, head SHA checked, merge method, gates passed/blocked, and merge
result / merge commit — never a token, auth header, or credential. Error text
is scrubbed via _redact.
Surface audit: no ungated merge path remains. The /merge endpoint appears only
inside gitea_merge_pr; gitea_review_pr fails closed on merge=True before any
API call; gitea_submit_pr_review has no merge parameter and 'merge' is not a
reviewable action. Tests assert all three.
Tests cover: merge succeeds only when all gates pass; self-author blocked;
unknown identity/profile blocked; profile without merge permission blocked;
missing/wrong confirmation blocked (no API call); head-SHA mismatch blocked;
changed-files mismatch blocked; closed PR blocked; non-mergeable blocked;
unknown mergeability fail-closed; no merge call when gates fail; invalid merge
method rejected; output and error redaction; and the no-ungated-merge-path audit.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Add gitea_submit_pr_review, the only tool that submits a Gitea PR review.
It performs a review mutation (comment / approve / request_changes) only
after every safety gate passes, and never merges.
Gates (fail-closed at each step):
1. Validate action is comment | approve | request_changes.
2. Reuse gitea_check_pr_eligibility (#14) for authenticated-user lookup,
active-profile lookup, PR-author lookup, self-approval block, and the
profile-allowed-operation check. approve requires 'approve' eligibility,
request_changes requires 'request_changes', comment requires 'review'.
3. Redundant self-approval block (auth user == PR author).
4. Optional expected_head_sha: refuse if the PR head has moved.
5. Only then POST /repos/{owner}/{repo}/pulls/{n}/reviews (formal review
endpoint, so approvals/change-requests carry real review state).
Output reports action, whether performed, authenticated user, profile name,
PR author, PR number, head SHA checked, and reasons — never a token, auth
header, or credential. Error text is scrubbed via _redact as defence in depth.
Merge is intentionally not implemented (belongs to #16).
Tests cover: self-author approve blocked, approve/request_changes/comment
succeed only when eligible, unknown identity fail-closed, disallowed profile
op blocked, head-SHA mismatch blocked, no mutation when gates fail, invalid
action rejected, and secret redaction in output and error paths.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Add two explicit eligibility tests requested in review of PR #24:
- self-author blocked from 'approve' (eligible=false, reason
"authenticated user is PR author").
- 'merge' fails closed when Gitea reports mergeable=None (eligible=false,
reason "PR mergeability unknown").
Tests only; no implementation change. Behavior already enforced by
gitea_check_pr_eligibility.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>