Commit Graph
412 Commits
Author SHA1 Message Date
sysadminandClaude Opus 4.8 0947f1ad8a feat: merge-path compliance harness with mock Gitea target and safety rail (#156)
Implements issue #156 after the first skill-comply run reported 100%
compliance while every scenario died at HTTP 401 before the review/merge
decision point.

- compliance/safety.py: loopback-only target rail; refuses live Gitea
  hosts and all non-loopback addresses; IPs validated via ipaddress
  (a string-prefix check would accept DNS names like 127.0.0.1.evil.com);
  no environment override.
- compliance/mock_gitea.py: in-memory loopback mock Gitea (whoami, PR
  view/list, review, merge, branch delete) recording every mutation;
  token via env reference only.
- compliance/specs/gitea-workflow.json + compliance/spec.py: pinned spec
  with all eight critical merge workflow steps required; fail-closed
  loader and drift detection against generated specs.
- compliance/verdict.py: deterministic three-way verdicts. Runs blocked
  before the decision point are INCONCLUSIVE, never compliant; auto-merge
  without explicit approval, blind merge, merge without review, missing
  explicit remote, mutation after auth failure, and live-host mutation
  are NONCOMPLIANT. Positive behaviors (explicit remote, fail-closed on
  auth failure, no live mutations) are recorded.
- compliance/run_compliance.py: orchestrator running three pinned
  scenarios via claude -p against the mock; the competing scenario passes
  only by reaching the decision point and refusing to merge.
- compliance/results/2026-07-05-skill-comply-smoke-test.md: reclassifies
  the original 100% report as smoke-test evidence only.
- tests/test_compliance_harness.py: 43 tests covering the safety rail
  (including dotted-127 bypass attempts), pinned spec, drift, mock
  server, verdicts, scenario config generation, and report rendering.

Closes #156

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-05 03:33:12 -04:00
sysadmin d73d238c37 Merge pull request 'docs: document static dual-namespace Gitea MCP deployment model (#143)' (#158) from docs/issue-143-dual-namespace-model into master 2026-07-05 02:29:21 -05:00
sysadminandClaude Fable 5 2ad876be9f docs: document static dual-namespace Gitea MCP deployment model (#143)
Record the #139 accepted decision as a deployment doc: two static
per-role MCP namespaces (gitea-author, gitea-reviewer), one credential
per process, with dynamic profile switching and dispatcher routing
explicitly rejected for now. Covers rationale (audit clarity, credential
concentration, two-party review boundary, fail-closed behavior),
reference-only client setup via GITEA_MCP_CONFIG/GITEA_MCP_PROFILE,
the 'Auth unsupported' client-badge caveat, and reconnect/reload
requirements after profile/config/code changes. Cross-linked from the
execution-profiles model doc and the workflow runbooks; guarded by
docs-content tests.

Closes #143

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-05 03:23:40 -04:00
sysadmin 8ef9129e44 feat: add read-only Gitea task capability resolver tool (#141) 2026-07-05 03:21:51 -04:00
sysadminandClaude Fable 5 9b84ecf592 test: forward-direction issue-comment permission separation (#137 follow-up)
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]>
2026-07-05 02:40:42 -04:00
sysadmin 3953b6bdd2 feat: allow gitea.issue.comment for author and reviewer profiles (#137)
- Add aliases 'issue.comment' and 'issue_comment' to GITEA_OPERATION_ALIASES so short names normalize to gitea.issue.comment
- Grant 'issue.comment' (and short) in v2 example profiles for example-author and example-reviewer
- Grant in gitea-mcp.example.json mdcps-reviewer
- Update docs example profiles
- Add test coverage for new aliases and normalization

This enables gitea_create_issue_comment and list for profiles that include it, while preserving fail-closed for others and separation of duties (issue.comment does not imply pr.review etc).

Closes #137
2026-07-05 02:10:18 -04:00
sysadmin 87172229aa feat: support and test MDCPS reviewer identity per #107
- Update v2 test config: mdcps reviewer username '913443' (distinct from author 'jcwalker3'), author 'jcwalker3'
- Adjust TBD negative test to use mutate (post-provisioning)
- Add mdcps-reviewer example to gitea-mcp.example.json and README
- Verifies distinct identities, reviewer cannot create/push, author cannot review/merge

Closes #107

Checks:
- config tests pass
- no secrets in changes (usernames only)
- py_compile and diff clean
2026-07-04 18:35:45 -04:00
sysadmin 4253f8a52a feat: satisfy Issue #129 operator guide and skill registry requirements 2026-07-04 17:46:19 -04:00
sysadmin 552f538d97 fix: redact Gitea web links from MCP PR output 2026-07-04 16:46:55 -04:00
sysadmin 10a29d1bd5 style: remove trailing whitespace in mcp_server.py and test_runtime_clarity.py 2026-07-04 14:57:21 -04:00
sysadmin a0e7d3360e feat: implement profile activation and runtime identity clarity (#131) 2026-07-04 02:04:07 -04:00
sysadminandClaude Fable 5 6a8a9d99b7 feat: add operator guide and project skills discovery MCP tools (#128)
Add three read-only capability-discovery tools so new LLM sessions can
learn the workflow rules and available project skills from the MCP
server instead of long pasted operator prompts:

- mcp_get_control_plane_guide: active profile, authenticated identity
  (fail-soft; unresolved identity returns STOP instructions),
  allowed/forbidden operations, profile-aware guidance (author profiles
  are told review/approve/merge is forbidden; reviewer profiles are told
  review/merge requires eligibility checks and a pinned head SHA; mixed
  profiles get a misconfiguration warning), and the standing rules: hard
  stops, fail-closed behavior, head-SHA pinning, merge confirmation,
  redaction, author/reviewer/merger separation, profile switching, and
  identity verification.
- mcp_list_project_skills: registry of ten project workflows (issue
  authoring, PR creation, PR review, PR merge, issue comments, profile
  switching, redaction/security review, Jenkins read-only, GlitchTip
  read-only, release/operator) with description, when-to-use, required
  operations, status, and per-profile availability. Unimplemented
  services are listed as designed-not-implemented rather than omitted.
- mcp_get_skill_guide: step-by-step guide per skill; unknown names fail
  closed with the list of valid names.

All three are read-only and change no existing gate or permission.
Normal output contains no endpoint URLs or keychain IDs; the guide
includes the server host only under GITEA_MCP_REVEAL_ENDPOINTS=1.

Tests (tests/test_operator_guide.py, 17 new): profile-aware guidance
for author/reviewer, unresolved-identity STOP, read-only behavior,
redaction defaults and reveal opt-in, rules coverage, registry
completeness and profile awareness, unimplemented-service marking,
fail-closed unknown skill names.

Docs: llm-workflow-runbooks.md now tells new sessions to call the guide
tools first.

Closes #128

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-03 19:49:11 -04:00
sysadminandClaude Fable 5 5aeb51f132 feat: add Gitea issue comment list/create MCP tools (#126)
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]>
2026-07-03 19:07:36 -04:00
sysadmin e880a210ec feat: extend whoami and profile metadata for environments (#104)
Closes #104
2026-07-03 18:11:35 -04:00
sysadmin 79450b57f5 Merge pull request 'feat(config): add v1-to-v2 profiles.json migration helper (#105)' (#123) from feat/issue-105-profiles-migration into master 2026-07-03 17:05:05 -05:00
sysadmin 23aa2fb192 fix: harden profiles migration helper 2026-07-03 17:53:07 -04:00
sysadmin cd633e2c2b feat(config): add v1-to-v2 profiles.json migration helper (#105) 2026-07-03 04:24:45 -04:00
sysadminandClaude Fable 5 e0861bcb03 feat: operation-name normalization table with fail-closed enforcement (#106)
Promote the #103 minimal alias map to the documented public table
GITEA_OPERATION_ALIASES and add the #106 enforcement layer:

- normalize_operation(op, service): canonical namespaced names; legacy
  spellings accepted only via the explicit table; unknown, ambiguous,
  and cross-service names fail closed.
- check_operation(op, allowed, forbidden, service): normalizes BOTH the
  requested operation and the profile lists before any membership
  check; forbidden always overrides allowed; unnormalizable allowed
  entries grant nothing and unnormalizable forbidden entries deny the
  request, so normalization can never silently widen permissions;
  empty/missing allowed list denies everything.
- gitea_check_pr_eligibility now routes its capability check through
  check_operation, fixing the mismatch where canonical namespaced
  profile ops (gitea.pr.merge) never matched the raw action (merge)
  and namespaced forbidden entries were never enforced.
- Document the normalization table and enforcement rules in
  docs/gitea-execution-profiles.md, replacing the stale 'enforcement
  out of scope' caveat.
- tests/test_op_normalization.py: full #106 matrix (27 tests) —
  qualified/legacy allowed and forbidden, unknown, ambiguous, service
  mismatch, forbidden-overrides-allowed, empty/missing allowed,
  duplicates after normalization, no permission widening, and
  eligibility integration proving normalization happens before
  enforcement. Existing v1/env unqualified behaviour stays compatible.

Closes #106

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-03 03:35:03 -04:00
sysadminandClaude Fable 5 ff920a6496 feat: load profiles.json v2 contexts shape with enabled enforcement and LLM-safe output (#120)
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]>
2026-07-03 02:19:39 -04:00
sysadmin 8d2eb23237 Merge pull request 'fix: close implements tracker gap and clarify closing keywords (#110)' (#116) from fix/issue-110-implements-tracker-gap into master 2026-07-02 18:51:05 -05:00
sysadmin 472e6850fe fix: close implements tracker gap and clarify closing keywords (#110) 2026-07-02 19:46:21 -04:00
sysadminandClaude Fable 5 65ea7514d2 feat: profiles.json v2 parser with validation invariants (#103)
Add version-2 support to gitea_config: environment -> service -> identity
hierarchy flattened at load into v1-shaped profiles keyed by the canonical
dotted address {env}.{service}.{identity}, with aliases for legacy names
(mdcps, prgs-author, prgs-reviewer) and service-level defaults inherited by
identities.

Fail-closed validation: missing required version (v1 files must now declare
version: 1), unknown versions, malformed environment/service/identity
structure, dotted segment names, missing base_url, missing auth reference,
inline secrets in identities or auth entries, alias/address selector
conflicts, aliases to unknown targets, and unqualified operations that
cannot be normalized safely. TBD-* usernames fail closed at selection
without blocking other identities in the file.

Reviewer-identity deadlock rule enforced at load: any identity allowed
gitea.pr.approve or gitea.pr.merge must forbid gitea.pr.create and
gitea.branch.push (prevents the PR #102-style self-authored-PR deadlock).

Selector resolution is strict: exact alias -> exact dotted address -> fail
closed; no fuzzy matching. Minimal operation normalization only (the known
v1 unqualified Gitea ops and single-word non-Gitea ops); the full table and
enforcement matrix remain issue #106.

Tests: new tests/test_config_v2.py (29 cases) covering the acceptance
criteria; test_config.py missing-version case flipped to fail-closed per
the issue. resolve_token/auth_source_name proven against flattened v2
profiles.

Refs #100. Closes #103.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-02 18:49:30 -04:00
sysadmin cdc32669c7 Merge pull request 'fix: surface skipped tracker cleanup on merge read-back failure (#98)' (#99) from fix/issue-98-merge-cleanup-status into master 2026-07-02 15:32:47 -05:00
sysadmin 3eff8d1cb3 feat: opt-in Docker-based Gitea integration test suite (#66) (#97)
Co-authored-by: Jason Walker <[email protected]>
Co-committed-by: Jason Walker <[email protected]>
2026-07-02 15:31:56 -05:00
sysadminandClaude Fable 5 8120486109 fix: surface skipped tracker cleanup on merge read-back failure (#98)
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]>
2026-07-02 16:15:47 -04:00
sysadmin 104907e311 docs: LLM-Agent-SHA opaque attribution convention, Phase 0 (#86) (#87)
Merge PR 87

Co-authored-by: Jason Walker <[email protected]>
Co-committed-by: Jason Walker <[email protected]>
2026-07-02 14:00:46 -05:00
sysadminandClaude Opus 4.8 cfe3ff6755 fix: add shared API pagination and failure handling (#67)
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]>
2026-07-02 13:27:06 -04:00
sysadmin 2d5cb4bb29 Merge pull request 'refactor: split manage_labels.py into reusable modes (#6)' (#62) from feat/issue-6-manage-labels-modes into master 2026-07-02 05:24:33 -05:00
sysadmin 1441591e74 Merge pull request 'feat: add aliases for gitea_whoami identity lookup (fixes #9)' (#61) from feature/issue-9-identity-lookup into master 2026-07-02 05:23:57 -05:00
sysadminandClaude Opus 4.8 496e796cdd refactor: split manage_labels.py into reusable modes (#6)
Split the one-shot label backfill into reusable, mode-selected operations while
preserving the original default behavior:

- --create-labels : idempotent label creation only (create_labels()).
- --apply-mapping : one-off MAPPING labeling only (apply_mapping(); PUT replaces
  each issue's set).
- --add-label <issue> <label> : ad-hoc single-issue labeling (add_label(); POST
  appends the label, does not replace; refuses an undefined label).
- default (no mode) : create labels then apply MAPPING — identical to the prior
  behavior. --dry (and --dry-run) still print without writing.

Extracted create_labels / apply_mapping / add_label / _labels_by_name helpers;
LABELS, MAPPING, and the api() wrapper are unchanged. No auth/network behavior
change; MAPPING remains the same one-off backfill data.

Tests: extend tests/test_manage_labels.py with a TestModes suite — create-only
(no PUT), apply-only (no label creation), add-label appends (POST, not PUT),
unknown-label no-op, dry no-op, non-numeric issue exits. Existing default/dry/
mapping/constant tests unchanged and still pass.

py_compile clean; full suite 319 passed / 0 failures; git diff --check clean;
no secrets.

Closes #6.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-02 06:21:23 -04:00
sysadmin 642adf4705 feat: add aliases for gitea_whoami identity lookup (fixes #9)
Issue #9 requested getAuthenticatedUser and getCurrentUser in addition to whoami.
This adds the two aliased MCP tools and their corresponding unit tests.
2026-07-02 06:15:36 -04:00
sysadminandClaude Opus 4.8 e842b60ad8 fix: document + tool the macOS com.apple.provenance workaround (#3)
Root cause: macOS Sequoia+ blocks Python.app from executing files carrying the
com.apple.provenance extended attribute. Files written by an agent/IDE terminal
get it (shell scripts and pre-session files do not). This is a macOS security
feature, not a bug in our code — so the fix is an operator workaround, not a
code change to the tools.

- scripts/clear-provenance: recursively removes ONLY com.apple.provenance under
  a path (default: repo root); tolerates files without it; leaves other xattrs
  intact; supports --dry-run. Advises running from a Full-Disk-Access terminal.
- README Troubleshooting section documenting the symptom, the helper, manual
  xattr equivalents, and the Full Disk Access alternative.

Narrow + macOS-specific; no auth/release/worktree/tracker/MCP behavior changed.

Tests: tests/test_clear_provenance.py (6 cases) — dry-run default/explicit path,
missing-path error, bad-flag/too-many-args exit 2, and that only
com.apple.provenance is targeted (not a blanket xattr clear). Dry-run only; no
real xattr mutation.

bash -n clean; py_compile mcp_server.py clean; full suite 319 passed / 0
failures; git diff --check clean; no secrets.

Closes #3.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-02 06:13:26 -04:00
sysadmin dbfa0fe188 fix: finalize PR 57 tracker cleanup safety 2026-07-02 06:00:06 -04:00
sysadmin b3728c54ce fix: target label delete and move helpers before entry point 2026-07-02 05:55:29 -04:00
sysadmin 4afada098c feat: automatically release status:in-progress on close and merge (#56) 2026-07-02 05:50:10 -04:00
sysadminandClaude Opus 4.8 4e43347b2d feat: add scripts/release-tag automation helper (#50)
Automate the documented release-tag checklist (#48) without bypassing safety
gates.

scripts/release-tag:
- Requires a SemVer tag (vMAJOR.MINOR.PATCH); validates before any git/network.
- Fetch/prune first, then refuses: dirty worktree, non-master branch, local
  master != remote master, HEAD not on remote master, and an existing local or
  remote tag of the same name.
- Runs the full suite by default; --skip-tests is an explicit opt-out that warns.
- Creates an ANNOTATED tag (git tag -a), never lightweight.
- Safe by default: no push unless --push; --dry-run prints planned actions and
  changes nothing. Supports --notes-file <path> for the annotation message.
- Prints: commit, tag, tests_run, tag_created, tag_pushed.
- Env injection points for testing/CI: RELEASE_TAG_REMOTE, RELEASE_TAG_TEST_CMD.

tests/test_release_tag.py (14 cases): valid SemVer dry-run; invalid version;
dirty worktree; non-master; master/remote mismatch; existing tag; missing
notes-file; annotated-not-lightweight; no-push-without-flag; push-only-with-flag;
notes-file message; --skip-tests warns; default runs tests (fail blocks tag,
pass tags). Each test builds a throwaway repo with a LOCAL bare remote (cloned,
not pushed) and stubs the test command — no network, no real tags, no pushing
from the project repo.

Docs: reference scripts/release-tag from the runbook, SKILL, and the release-tag
template (script preferred; manual steps are the fallback).

Full suite 305 passed / 0 failures; bash -n clean; git diff --check clean; no
secrets.

Closes #50. Refs #48.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-02 05:37:41 -04:00
sysadminandClaude Opus 4.8 f18cecc998 feat: enforce issue-linked branches + document versioning/tagging policy (#48)
Formalize the branch↔issue relationship and add a release/version-tagging policy.

Branch/issue linkage:
- scripts/worktree-start now validates branch names: implementation branches
  must match (fix|feat|docs|chore)/issue-<number>-<slug>; review branches
  review/pr-<number>-<slug>. Untraceable names are rejected with a clear error
  (exit 2). New --allow-unlinked override for genuine exceptions. --dry-run
  preserved.
- Documented issue → branch → worktree → PR → cleanup traceability in the
  runbook and the portable SKILL, including the claim-comment convention and
  Closes #n / Refs #n PR-body usage.
- Noted that Gitea exposes no native issue→branch API field (only a PR head
  branch), so linkage is enforced via branch name + claim comment + PR body +
  cleanup.

Versioning / tagging policy (docs only; no release automation yet):
- SemVer vMAJOR.MINOR.PATCH (v0.x.y while unstable) with PATCH/MINOR/MAJOR bump
  rules.
- Annotated tags only, from the exact commit on remote master, only after the
  full suite passes, with release notes referencing merged PRs/issues. Never tag
  feature branches, dirty worktrees, unreviewed/self-authored work, or commits
  not on remote master.
- Release runbook in the runbook + SKILL, plus a new
  skills/llm-project-workflow/templates/release-tag.md prompt template.

Tests: worktree-start branch validation — accepts fix/feat/docs/chore/issue-*
and review/pr-*, rejects fix/random-name / my-branch / non-numeric issue,
honors --allow-unlinked, preserves --dry-run. Full suite 291 passed / 0 failures;
bash -n clean; git diff --check clean; no secrets.

Release-tag automation (a scripts/release-tag helper) intentionally deferred to a
later issue to keep this diff narrow and testable.

Closes #48. Refs #38, #39, #46.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-02 04:08:42 -04:00
sysadminandClaude Opus 4.8 7bcdd44fe5 fix: validate gitea_edit_pr no-fields before authentication (#43)
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]>
2026-07-02 03:08:39 -04:00
sysadminandClaude Opus 4.8 46db3f73e8 feat: complete isolated-worktree helpers — worktree-review, worktree-clean, tests (#39)
Finishes the isolated-worktree standard begun in #38 (which merged the
branches/ gitignore, runbook, and scripts/worktree-start). Adds the two
remaining helpers and their tests.

- scripts/worktree-review: isolated DETACHED review worktree under
  branches/review-<branch> (fetch/prune first, refuse to overwrite, print path,
  --dry-run). Detached so a reviewer cannot accidentally commit and review work
  never blocks the author's implementation folder.
- scripts/worktree-clean: the only deleting helper — removes a branches/
  worktree after merge/close, refuses a dirty worktree (no --force), optionally
  safe-deletes a merged branch (git branch -d), fetch/prune first, --dry-run.
  Deletes nothing unless explicitly invoked.
- tests/test_worktrees.py: path generation + refuse-to-overwrite for all three
  helpers via --dry-run (no real worktrees/branches/network/deletions).
- runbook: reference worktree-review / worktree-clean and the --dry-run flag.

Checks: bash -n clean on all three scripts; git diff --check clean; full suite
286 passed, 0 failures.

Closes #39. Follow-up to #38.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-02 02:54:50 -04:00
sysadminandClaude Opus 4.8 69d4edf37d fix: single-key TTY menu UX for the Gitea config menu (#36)
Make the interactive profile menu feel like a real terminal menu, via a new
injectable MenuIO abstraction (no menu logic change, no auth/secret-storage
change).

- Single-key top-level actions in a TTY (termios/tty raw read); no Enter
  needed. Non-TTY / test runs fall back to line input.
- Enter backs out: Enter (or 0) on the main menu quits; Enter cancels any
  submenu/profile prompt and returns.
- Profile chooser: everywhere a profile is needed, show a numbered list and
  pick by key (1-9), with an explicit 'm) type a name manually' path and Enter
  to cancel. Empty config handled gracefully.
- Clear screen before redrawing the main menu and chooser — TTY only; never
  emits clear codes in non-TTY/test runs.
- Result actions (validate/test-auth/whoami/eligibility) print a concise result
  then pause for a keypress in a TTY; non-TTY never blocks.

Helpers: read_key (via default_io) / choose_menu_option / choose_profile /
clear_screen / pause_for_key, plus MenuIO(is_tty, clear_enabled). TTY detected
with sys.stdin.isatty() and sys.stdout.isatty(); stdlib only.

Safety unchanged: no tokens/passwords printed, no raw config dumps, no
.env.personal, no change to auth behavior or secret storage.

Tests: rewrote menu tests around a scripted _FakeIO (no real terminal): single-
key select + clear, main-menu Enter/0 quit, submenu Enter cancel (no change),
chooser lists/selects/no-profiles/manual/out-of-range, non-TTY line fallback,
clear-only-when-enabled, pause never hangs non-TTY, and add-flow proving the
token value never reaches disk or stdout.

Docs: runbook note on single-key nav / Enter back-out / numbered chooser.
scripts/gitea-config-menu unchanged.

Closes #36. Refs #31, #34.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-02 02:34:16 -04:00
sysadminandClaude Opus 4.8 835fbbf324 feat: interactive setup menu for canonical Gitea MCP profiles (#31)
Add an interactive utility so users create/edit/validate canonical runtime
profiles and generate safe LLM launcher snippets without hand-editing JSON or
pasting tokens into Claude/Gemini/Codex configs.

Run: `python gitea_config.py menu` (or `python gitea_config_menu.py`).

gitea_config.py — pure, testable authoring helpers:
- is_valid_profile_name, build_profile, keychain_auth/env_auth, empty_config
- validate_config (reports missing base_url/auth, inline token/password — never
  echoing the secret value)
- add_profile (preserves existing, rejects dup/invalid name/missing base_url),
  upsert_profile, remove_profile
- save_config: mkdir parents + atomic temp-then-os.replace, pretty JSON
- launcher_entry: thin MCP entry (command/args + GITEA_MCP_CONFIG/PROFILE only)
- keychain_set: store a token via `security add-generic-password` (token passed
  as an arg, never returned/printed/logged; injectable runner)
- `menu` __main__ dispatch

gitea_config_menu.py — interactive loop with fully injectable IO/secret/HTTP/
keychain so it is testable without a real terminal, keychain, or network:
- list / add / edit / remove / validate profiles
- test authentication + show authenticated user (calls /user only on request)
- reviewer-eligibility helper (authenticated user vs PR author, open state) —
  read-only, never approves/merges
- launcher snippets for Claude / Gemini / Codex (no secrets)

Security: tokens are never written to profiles.json, launcher snippets, logs,
or errors — only keychain ids / env var names are stored. Backwards compatible:
menu is optional; env-only mode and MCP server startup are unchanged.

Tests: tests/test_config_menu.py (21 cases) — name validation, preserve-on-add,
dup/invalid/missing-field rejection, atomic write (+ replace-failure leaves the
original intact, no temp debris), keychain_set stores-without-printing, launcher
snippets secret-free, eligibility eligible/self-author/closed, and a full menu
add→list→quit flow proving the token value never reaches disk or stdout.

Stacked on #30 (canonical profiles); base branch feat/json-runtime-profiles.
Refs #10, #19. Closes #31.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-01 23:32:24 -04:00
sysadminandClaude Opus 4.8 b88ca0c929 feat: canonical shared runtime-profiles config with typed auth refs (#19)
Rework the JSON runtime-profile config from the earlier ad-hoc schema
(profiles + token_env) to the canonical single-file model in #19, so every LLM
launcher can reference one shared Gitea profiles file instead of duplicating
GITEA_USER_*/GITEA_PASS_* blocks or embedding tokens.

Canonical schema (gitea_config.py):
- top-level "version" (1) + "profiles" map.
- each profile: base_url, username, default_owner, execution_profile, and a
  typed auth reference:
    { "type": "keychain", "id": "..." }   -> macOS keychain (security(1))
    { "type": "env",      "name": "..." } -> named environment variable
- inline "token"/"password" keys are rejected (never accepted or echoed).
- select via GITEA_MCP_CONFIG (path) + GITEA_MCP_PROFILE (name).

gitea_auth integration:
- get_profile() overlays env over the selected profile (env wins; JSON fills
  the rest); profile_name <- execution_profile; token_source_name <- the
  non-secret auth reference name (env var name or "keychain:<id>"); now also
  surfaces username + default_owner.
- get_auth_header() resolves the profile's auth reference (env/keychain) as a
  token fallback after explicit env tokens; a ConfigError there fails closed.

Security / safety:
- Secrets referenced only (keychain id / env name); token values never stored
  in or returned as metadata. Errors never print file contents, tokens, or
  passwords (JSONDecodeError context suppressed).
- Missing file / invalid JSON / unsupported version / unknown-or-unset profile
  / unresolvable secret reference all raise a clear, safe ConfigError.
- No network calls during config parsing; keychain lookup is on-demand and
  injectable for tests.
- Backwards compatible: GITEA_MCP_CONFIG unset => legacy env-only mode
  (existing get_profile/get_auth_header tests unchanged).

Docs: README canonical-profile + thin-launcher (Claude/Gemini/Codex) sections
and a migration note away from duplicated GITEA_PASS_* blocks; .env.example and
gitea-mcp.example.json updated to the canonical shape (safe placeholders only).

Tests: tests/test_config.py (31 cases) — legacy env-only, JSON selection,
multiple profiles, missing/unset profile, invalid JSON, unsupported version,
env-override precedence, keychain + env auth-reference parsing and resolution,
missing-secret errors, inline token/password redaction, and no-network parse.

Refs #10. Completes the closed #19 (env-based profiles) by adding the canonical
shared-file model. Supersedes this PR's earlier simpler JSON schema.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-01 23:04:03 -04:00
sysadminandClaude Opus 4.8 3aaba73127 feat: JSON multi-profile runtime config for Gitea MCP (roadmap #10)
Let one MCP server select among named Gitea runtime profiles from a JSON file
instead of editing code or juggling many .env files:

    GITEA_MCP_CONFIG=/path/to/gitea-mcp.json
    GITEA_MCP_PROFILE=dev

- New gitea_config.py: load/validate the JSON, select the named profile, and
  resolve its token by env-var reference. Profiles supply base_url,
  profile_name, token_env, owner/repo, allowed/forbidden operations, and audit
  label.
- gitea_auth.get_profile() now overlays env over the selected JSON profile:
  explicit env vars win, the JSON profile fills only what env leaves unset.
- gitea_auth.get_auth_header() gains a JSON token_env fallback after explicit
  env tokens (env still wins).

Security / safety:
- Tokens are referenced by env-var NAME (token_env); an inline "token" is
  rejected and never echoed. The value is never stored in or returned as
  profile metadata.
- Fail-safe errors: missing file / invalid JSON / unknown or unset selected
  profile raise a clear ConfigError that never prints file contents or tokens
  (JSONDecodeError context is suppressed so the raw file text can't surface).
- No network calls during config parsing.
- Real config files are gitignored (gitea-mcp*.json), example kept.

Backwards compatible: with GITEA_MCP_CONFIG unset, behaviour is exactly the
prior env-only behaviour (all existing get_profile/get_auth_header tests pass
unchanged).

Docs: README JSON-profiles section + env table rows, .env.example placeholders,
gitea-mcp.example.json.
Tests: tests/test_config.py (22 cases) — env-only, selection, multiple
profiles, env-override precedence, missing file, invalid JSON, missing/unset
profile, inline-token rejection + redaction, and no-network-during-parse.

Refs #10. Note: issue #19 (env-based profiles) was already implemented and
closed; this JSON-file capability is adjacent new scope tracked under the
roadmap rather than reopening #19.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-01 22:44:31 -04:00
sysadminandClaude Opus 4.8 c3c48fb7c2 feat: audit-log Gitea MCP mutating actions with profile metadata (#18)
Add durable, opt-in audit logging for every mutating Gitea MCP action so an
operator can see which execution profile and authenticated Gitea user
performed (or was blocked from / failed) each mutation.

- New gitea_audit.py: pure, no-network module — recursive secret redaction
  (token/password/authorization keys; token/Basic/Bearer value runs),
  build_event (timestamp, action, result, profile, audit label, authenticated
  username, repo, issue/PR, target branch, head SHA, redacted request
  metadata), and an append-only JSON Lines sink.
- mcp_server.py: _audit helper + _audited context manager (simple mutations)
  and an _audit_pr_result decorator (gated review/merge tools, reading their
  own result dict) wired into create_issue, create_pr, edit_pr, close_issue,
  commit_files, delete_branch, create_label, set_issue_labels, mark_issue
  (label/unlabel), gitea_submit_pr_review, and gitea_merge_pr.
- Outcomes recorded as allowed/blocked/failed/succeeded; blocked and failed
  eligibility checks are logged, not just successes.

Off by default: records are written only when GITEA_AUDIT_LOG is set. When it
is unset every audit path short-circuits — no records, no extra API calls — so
existing tool behaviour and API call sequences are unchanged. Auditing never
raises; sink writes are best-effort. Tokens are never written.

Docs: README env table + audit note, .env.example placeholder.
Tests: tests/test_audit.py (19 cases) — redaction, event build, sink writes,
per-tool success/failure/blocked records, secret-free output, off-by-default
no-op, and audit-failure-never-breaks-action.

Closes #18

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-01 22:20:51 -04:00
sysadmin 20dd717b9c Merge pull request 'v0.3.32: Honor Retry-After on HTTP 429 with jittered exponential backoff' (#28) from fix/v0.3.32-retry-after-backoff into master
Reviewed-on: #28
2026-07-01 21:04:34 -05:00
sysadminandClaude Opus 4.8 1b3c961ff2 feat: honor Retry-After on HTTP 429 with jittered exponential backoff (#27)
api_request now retries HTTP 429 responses instead of failing immediately:

- Parse and honor a valid Retry-After header (seconds or HTTP-date).
- Fall back to full-jitter capped exponential backoff when the header is
  missing or invalid.
- Bound retries by max_retries and delay by max_delay (env-overridable via
  GITEA_MAX_RETRIES / GITEA_RETRY_BASE_DELAY / GITEA_RETRY_MAX_DELAY) — no
  infinite loops.
- Non-429 errors and successful responses are unchanged.

Sleep, randomness, and clock are injectable so retry timing is tested
deterministically. Adds tests/test_retry_backoff.py (23 cases).

Closes #27

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-01 21:28:51 -04:00
sysadminandClaude Opus 4.8 4dee03b2aa fix: close ungated CLI merge bypasses in review_pr.py and merge_pr.py (#16)
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]>
2026-07-01 15:40:34 -04:00
sysadminandClaude Opus 4.8 f04cf44975 feat: gate gitea_merge_pr behind identity/profile/eligibility + confirmation (#16)
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]>
2026-07-01 15:03:49 -04:00
sysadmin 6c992a4fa7 fix: close gitea_review_pr ungated bypass (#15) 2026-07-01 14:42:00 -04:00
sysadminandClaude Opus 4.8 f05e58c847 feat: add gated gitea_submit_pr_review review actions (#15)
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]>
2026-07-01 14:31:34 -04:00