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) <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>
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>
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) <noreply@anthropic.com>
Add a read-only MCP tool that reports the active runtime execution
profile so an LLM can inspect what the current process is configured to
do before deciding whether to attempt an action later.
- gitea_get_profile: returns profile_name, allowed/forbidden operation
categories, audit_label, token_source_name (a NAME, never a value),
base_url, remote, resolved server, and — optionally — the verified
authenticated username. Identity resolution fails soft and marks
identity_status (verified/unknown/unavailable/not_resolved); the
profile config is always returned. Never mutates Gitea.
- gitea_auth.get_profile(): extended with forbidden_operations,
audit_label, token_source_name from env (non-secret metadata).
- .env.example / README: document the new optional metadata vars and
the discovery tool.
- tests: metadata parsing, verified/unavailable/unknown identity paths,
skip-identity, and secret-redaction.
Read-only. No token exposure. No multi-token switching. No PR
eligibility, review, or merge workflow. No Jenkins/Ops/GlitchTip/
Release/deploy behavior.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Allow the same MCP server to run as separate MCP entries, each with its
own token and profile name, so roles stay task-scoped (the profile is
the role, not the LLM).
- gitea_auth.get_profile(): reads GITEA_PROFILE_NAME,
GITEA_ALLOWED_OPERATIONS, GITEA_BASE_URL as non-secret metadata.
Never reads/returns/logs the token.
- gitea_whoami now surfaces the safe profile metadata (name + allowed
operations) alongside identity; token still never exposed.
- .env.example: placeholder-only template for a runtime profile.
- .gitignore: track .env.example while keeping real .env* ignored.
- README: document multiple env-configured MCP entries.
- tests: profile defaults/parsing, token-never-included, whoami surfaces
profile without leaking token.
One token + one profile per process. No multi-token switching in a
single runtime. No approve/merge/eligibility workflow. No
Jenkins/Ops/GlitchTip/Release/deploy behavior. No real secrets.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>