feat: canonical shared runtime-profiles config with typed auth refs (#19) #30

Closed
jcwalker3 wants to merge 0 commits from feat/json-runtime-profiles into master
Owner

Completes issue #19. Refs roadmap #10.

Supersedes this PR's earlier simpler JSON schema. The first commit added an ad-hoc {profiles: {name: {token_env, ...}}} layer. This PR now implements the canonical shared runtime-profiles model from #19: one canonical file every LLM launcher references, with a version, richer profile fields, and typed auth references. Review the branch as a whole (2 commits) or the net diff.

Problem

LLM MCP configs were drifting — each client's mcp_config.json duplicating GITEA_USER_* / GITEA_PASS_* / GITEA_SITE_* blocks and sometimes raw tokens. That duplicates runtime profiles, exposes secrets, and makes identity/merge safety hard to reason about.

Solution

One canonical profiles file; each LLM launcher is a thin pointer.

Canonical file:

{
  "version": 1,
  "profiles": {
    "prgs": {
      "base_url": "https://gitea.prgs.cc",
      "username": "jcwalker3",
      "auth": { "type": "keychain", "id": "prgs-gitea-token" },
      "default_owner": "Scaled-Tech-Consulting",
      "execution_profile": "personal-prgs"
    }
  }
}

Thin launcher (Claude/Gemini/Codex) — two env vars, no secrets:

"gitea-tools": {
  "command": ".../venv/bin/python3",
  "args": [".../mcp_server.py"],
  "env": {
    "GITEA_MCP_CONFIG": "/Users/jasonwalker/.config/gitea-tools/profiles.json",
    "GITEA_MCP_PROFILE": "prgs"
  }
}

Implemented format

  • Top-level version (must be 1) + profiles map.
  • Per profile: base_url, username, default_owner, execution_profile, and a typed auth reference:
    • { "type": "keychain", "id": "..." } → macOS keychain (security find-generic-password, on-demand).
    • { "type": "env", "name": "..." } → named env var.
  • Inline token/password keys are rejected (never accepted or echoed).
  • Select via GITEA_MCP_CONFIG (path) + GITEA_MCP_PROFILE (name).

Precedence

Explicit process env vars (GITEA_PROFILE_NAME, GITEA_BASE_URL, GITEA_TOKEN, …) override the JSON profile; the JSON profile fills only what env leaves unset. GITEA_MCP_CONFIG unset ⇒ legacy env-only mode (unchanged).

Safety

  • Secrets by reference only; token values never stored in / returned as / logged in metadata.
  • Missing file / invalid JSON / unsupported version / unknown-or-unset profile / unresolvable secret ref ⇒ clear ConfigError that never prints file contents, tokens, or passwords.
  • No network during config parsing.

Tests / checks

  • tests/test_config.py — 31 cases: legacy env-only, selection, multiple profiles, missing/unset profile, invalid JSON (no leak), unsupported version, env-override precedence, keychain + env auth parsing & resolution, missing-secret errors, inline token/password redaction, no-network parse.
  • Full suite: 243 passed, 0 failures, 0 errors (existing get_profile/get_auth_header/mcp tests unchanged → backwards compat). JUnit XML (harness swallows pytest stdout on multi-file runs).
  • py_compile clean; example JSON validated; real configs gitignored (gitea-mcp*.json).

Files changed

gitea_config.py (canonical loader/selector/auth-resolver), gitea_auth.py (get_profile overlay + get_auth_header fallback), tests/test_config.py, gitea-mcp.example.json, README.md, .env.example.


⚠️ Authored by me — do not self-merge. Needs review by another author.

Completes issue **#19**. Refs roadmap #10. > **Supersedes this PR's earlier simpler JSON schema.** The first commit added an ad-hoc `{profiles: {name: {token_env, ...}}}` layer. This PR now implements the **canonical shared runtime-profiles** model from #19: one canonical file every LLM launcher references, with a `version`, richer profile fields, and typed auth references. Review the branch as a whole (2 commits) or the net diff. ## Problem LLM MCP configs were drifting — each client's `mcp_config.json` duplicating `GITEA_USER_*` / `GITEA_PASS_*` / `GITEA_SITE_*` blocks and sometimes raw tokens. That duplicates runtime profiles, exposes secrets, and makes identity/merge safety hard to reason about. ## Solution **One canonical profiles file**; each LLM launcher is a thin pointer. Canonical file: ```json { "version": 1, "profiles": { "prgs": { "base_url": "https://gitea.prgs.cc", "username": "jcwalker3", "auth": { "type": "keychain", "id": "prgs-gitea-token" }, "default_owner": "Scaled-Tech-Consulting", "execution_profile": "personal-prgs" } } } ``` Thin launcher (Claude/Gemini/Codex) — two env vars, no secrets: ```json "gitea-tools": { "command": ".../venv/bin/python3", "args": [".../mcp_server.py"], "env": { "GITEA_MCP_CONFIG": "/Users/jasonwalker/.config/gitea-tools/profiles.json", "GITEA_MCP_PROFILE": "prgs" } } ``` ## Implemented format - Top-level `version` (must be `1`) + `profiles` map. - Per profile: `base_url`, `username`, `default_owner`, `execution_profile`, and a typed `auth` reference: - `{ "type": "keychain", "id": "..." }` → macOS keychain (`security find-generic-password`, on-demand). - `{ "type": "env", "name": "..." }` → named env var. - Inline `token`/`password` keys are **rejected** (never accepted or echoed). - Select via `GITEA_MCP_CONFIG` (path) + `GITEA_MCP_PROFILE` (name). ## Precedence Explicit process env vars (`GITEA_PROFILE_NAME`, `GITEA_BASE_URL`, `GITEA_TOKEN`, …) **override** the JSON profile; the JSON profile fills only what env leaves unset. `GITEA_MCP_CONFIG` unset ⇒ **legacy env-only mode** (unchanged). ## Safety - Secrets by reference only; token values never stored in / returned as / logged in metadata. - Missing file / invalid JSON / unsupported version / unknown-or-unset profile / unresolvable secret ref ⇒ clear `ConfigError` that never prints file contents, tokens, or passwords. - **No network** during config parsing. ## Tests / checks - `tests/test_config.py` — 31 cases: legacy env-only, selection, multiple profiles, missing/unset profile, invalid JSON (no leak), unsupported version, env-override precedence, keychain + env auth parsing & resolution, missing-secret errors, inline token/password redaction, no-network parse. - Full suite: **243 passed, 0 failures, 0 errors** (existing `get_profile`/`get_auth_header`/mcp tests unchanged → backwards compat). JUnit XML (harness swallows pytest stdout on multi-file runs). - `py_compile` clean; example JSON validated; real configs gitignored (`gitea-mcp*.json`). ## Files changed `gitea_config.py` (canonical loader/selector/auth-resolver), `gitea_auth.py` (`get_profile` overlay + `get_auth_header` fallback), `tests/test_config.py`, `gitea-mcp.example.json`, `README.md`, `.env.example`. --- ⚠️ Authored by me — do **not** self-merge. Needs review by another author.
jcwalker3 added 1 commit 2026-07-01 21:45:04 -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>
jcwalker3 closed this pull request 2026-07-01 21:57:12 -05:00
Author
Owner

Superseded by the canonical runtime-profiles work under #19 (branch feat/issue-19-canonical-runtime-profiles). That schema is a refinement of this one: adds version, richer profile fields (username, default_owner, execution_profile), and typed auth references ({type: keychain|env, ...}). Closing this PR in favor of #19.

Superseded by the canonical runtime-profiles work under #19 (branch `feat/issue-19-canonical-runtime-profiles`). That schema is a refinement of this one: adds `version`, richer profile fields (`username`, `default_owner`, `execution_profile`), and typed auth references (`{type: keychain|env, ...}`). Closing this PR in favor of #19.
jcwalker3 changed title from feat: JSON multi-profile runtime config for Gitea MCP (roadmap #10) to feat: canonical shared runtime-profiles config with typed auth refs (#19) 2026-07-01 22:04:42 -05:00
jcwalker3 reopened this pull request 2026-07-01 22:04:42 -05:00
Author
Owner

Correction to my earlier comment: this PR is not being closed. Per direction (reuse #30, no competing PR), it was reopened and reworked in place to the canonical shared runtime-profiles schema (version + typed keychain/env auth refs). It now completes #19. Title/body updated.

Correction to my earlier comment: this PR is **not** being closed. Per direction (reuse #30, no competing PR), it was reopened and reworked in place to the canonical shared runtime-profiles schema (version + typed keychain/env auth refs). It now completes #19. Title/body updated.
jcwalker3 closed this pull request 2026-07-01 22:57:57 -05:00
jcwalker3 deleted branch feat/json-runtime-profiles 2026-07-01 22:57:57 -05:00
jcwalker3 reopened this pull request 2026-07-02 00:08:20 -05:00
Author
Owner

Reopened. This PR was found closed without mergingmaster is still at the #29 merge and does not contain this branch's commits (3aaba73, b88ca0c). Branch feat/json-runtime-profiles re-pushed and PR reopened so an eligible (non-author) reviewer can merge it. Merge this before #32 (which stacks on it).

Reopened. This PR was found **closed without merging** — `master` is still at the #29 merge and does not contain this branch's commits (`3aaba73`, `b88ca0c`). Branch `feat/json-runtime-profiles` re-pushed and PR reopened so an eligible (non-author) reviewer can merge it. Merge this before #32 (which stacks on it).
jcwalker3 closed this pull request 2026-07-02 00:17:42 -05:00
jcwalker3 deleted branch feat/json-runtime-profiles 2026-07-02 00:17:42 -05:00

Pull request closed

Sign in to join this conversation.