feat: interactive setup menu for canonical Gitea MCP profiles (#31) #32

Closed
jcwalker3 wants to merge 0 commits from feat/gitea-config-menu into master
Owner

Closes #31. Refs #10, #19.

⚠️ Stacked PR

Base branch is feat/json-runtime-profiles (PR #30), not master — this builds on the canonical runtime-profiles code that isn't on master yet. Merge #30 first, then this PR's diff reduces to just the menu additions. (If Gitea shows #30's commits here, that's the stack; the net-new files are gitea_config_menu.py, tests/test_config_menu.py, plus additions to gitea_config.py and README.md.)

What

Interactive utility so users create/edit/validate canonical profiles and generate safe LLM launcher snippets without hand-editing JSON or pasting tokens into Claude/Gemini/Codex configs.

python gitea_config.py menu

Menu: list / add / edit / remove profiles · validate config · test profile auth · show authenticated user · generate launcher snippets (Claude/Gemini/Codex) · check reviewer eligibility for a PR.

Profile creation

Prompts for name, base URL, username, default owner/repo, execution profile, and auth type:

  • keychain — stores the token via security add-generic-password under an id like prgs-reviewer-token; profile records only { "type": "keychain", "id": ... }.
  • env — records { "type": "env", "name": "GITEA_TOKEN_PRGS_REVIEWER" }; user sets the env var themselves.

Token safety (core requirement)

  • Tokens are never written to profiles.json, launcher snippets, logs, or errors — only keychain ids / env var names are stored.
  • Token entry uses a hidden secret prompt; passed to security as an argument only.
  • Launcher snippets contain only command, args, GITEA_MCP_CONFIG, GITEA_MCP_PROFILE.

Config output

Writes one canonical file (default ~/.config/gitea-tools/profiles.json): creates parent dirs, atomic temp-then-os.replace, pretty JSON, preserves existing profiles.

Auth test / eligibility (read-only, on demand)

  • Test auth: resolves the profile's token, calls /api/v1/user, prints the username.
  • Eligibility: prints authenticated user, PR author, and ELIGIBLE/INELIGIBLE (open PR AND reviewer ≠ author). Never approves or merges.

Backwards compatibility

Menu is optional; env-only mode and MCP server startup are unchanged.

Tests / checks

  • tests/test_config_menu.py — 21 cases: name validation, preserve-on-add, dup/invalid/missing-field rejection, atomic write (+ replace-failure leaves 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 asserting the token value never reaches disk or stdout.
  • Full suite: 264 passed, 0 failures, 0 errors (JUnit XML; harness swallows pytest stdout on multi-file runs).
  • py_compile clean; python gitea_config.py menu smoke-tested (menu renders, quits cleanly; no-arg prints usage). Every side effect (stdin/secret/stdout/keychain/HTTP) is injectable — tests use no real keychain or network.

Files changed

gitea_config.py (+authoring helpers, menu dispatch), gitea_config_menu.py (new), tests/test_config_menu.py (new), README.md.


⚠️ Authored by me — do not self-merge. Needs review by another author, after PR #30 merges.

Closes #31. Refs #10, #19. ## ⚠️ Stacked PR Base branch is **`feat/json-runtime-profiles`** (PR #30), **not** `master` — this builds on the canonical runtime-profiles code that isn't on master yet. **Merge #30 first**, then this PR's diff reduces to just the menu additions. (If Gitea shows #30's commits here, that's the stack; the net-new files are `gitea_config_menu.py`, `tests/test_config_menu.py`, plus additions to `gitea_config.py` and `README.md`.) ## What Interactive utility so users create/edit/validate canonical profiles and generate safe LLM launcher snippets **without hand-editing JSON or pasting tokens** into Claude/Gemini/Codex configs. ```bash python gitea_config.py menu ``` Menu: list / add / edit / remove profiles · validate config · test profile auth · show authenticated user · generate launcher snippets (Claude/Gemini/Codex) · check reviewer eligibility for a PR. ## Profile creation Prompts for name, base URL, username, default owner/repo, execution profile, and auth type: - **keychain** — stores the token via `security add-generic-password` under an id like `prgs-reviewer-token`; profile records only `{ "type": "keychain", "id": ... }`. - **env** — records `{ "type": "env", "name": "GITEA_TOKEN_PRGS_REVIEWER" }`; user sets the env var themselves. ## Token safety (core requirement) - Tokens are **never** written to `profiles.json`, launcher snippets, logs, or errors — only keychain ids / env var names are stored. - Token entry uses a hidden secret prompt; passed to `security` as an argument only. - Launcher snippets contain only `command`, `args`, `GITEA_MCP_CONFIG`, `GITEA_MCP_PROFILE`. ## Config output Writes one canonical file (default `~/.config/gitea-tools/profiles.json`): creates parent dirs, **atomic** temp-then-`os.replace`, pretty JSON, **preserves existing profiles**. ## Auth test / eligibility (read-only, on demand) - Test auth: resolves the profile's token, calls `/api/v1/user`, prints the username. - Eligibility: prints authenticated user, PR author, and `ELIGIBLE`/`INELIGIBLE` (open PR AND reviewer ≠ author). Never approves or merges. ## Backwards compatibility Menu is optional; env-only mode and MCP server startup are unchanged. ## Tests / checks - `tests/test_config_menu.py` — 21 cases: name validation, preserve-on-add, dup/invalid/missing-field rejection, atomic write (+ replace-failure leaves 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 asserting the token value never reaches disk or stdout. - Full suite: **264 passed, 0 failures, 0 errors** (JUnit XML; harness swallows pytest stdout on multi-file runs). - `py_compile` clean; `python gitea_config.py menu` smoke-tested (menu renders, quits cleanly; no-arg prints usage). Every side effect (stdin/secret/stdout/keychain/HTTP) is injectable — tests use no real keychain or network. ## Files changed `gitea_config.py` (+authoring helpers, `menu` dispatch), `gitea_config_menu.py` (new), `tests/test_config_menu.py` (new), `README.md`. --- ⚠️ Authored by me — do **not** self-merge. Needs review by another author, **after PR #30 merges**.
jcwalker3 changed target branch from feat/json-runtime-profiles to master 2026-07-01 22:57:57 -05:00
jcwalker3 added 3 commits 2026-07-01 22:57:57 -05:00
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
jcwalker3 closed this pull request 2026-07-01 22:58:32 -05:00
jcwalker3 deleted branch feat/gitea-config-menu 2026-07-01 22:58:32 -05:00
jcwalker3 reopened this pull request 2026-07-02 00:08:23 -05:00
Author
Owner

Reopened. Was auto-closed when its base branch feat/json-runtime-profiles was deleted; the menu work never reached master. Base branch re-pushed and PR reopened. Merge after #30, then re-point this base to master (or rebase) if needed.

Reopened. Was auto-closed when its base branch `feat/json-runtime-profiles` was deleted; the menu work never reached `master`. Base branch re-pushed and PR reopened. Merge **after** #30, then re-point this base to `master` (or rebase) if needed.
jcwalker3 closed this pull request 2026-07-02 00:17:42 -05:00
jcwalker3 deleted branch feat/gitea-config-menu 2026-07-02 00:17:42 -05:00

Pull request closed

Sign in to join this conversation.