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>
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) <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>