From 2ad876be9f145c530e3f0d806bd8918c393bcc1b Mon Sep 17 00:00:00 2001 From: Jason Walker <913443@dadeschools.net> Date: Sun, 5 Jul 2026 03:20:36 -0400 Subject: [PATCH] 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 --- docs/gitea-dual-namespace-deployment.md | 156 ++++++++++++++++++++++++ docs/gitea-execution-profiles.md | 3 + docs/llm-workflow-runbooks.md | 1 + tests/test_dual_namespace_docs.py | 70 +++++++++++ 4 files changed, 230 insertions(+) create mode 100644 docs/gitea-dual-namespace-deployment.md create mode 100644 tests/test_dual_namespace_docs.py diff --git a/docs/gitea-dual-namespace-deployment.md b/docs/gitea-dual-namespace-deployment.md new file mode 100644 index 0000000..aa4c03d --- /dev/null +++ b/docs/gitea-dual-namespace-deployment.md @@ -0,0 +1,156 @@ +# Static Dual-Namespace Gitea MCP Deployment + +## Purpose + +This document (tracked as issue #143) records the deployment model accepted +in issue #139: run the +Gitea MCP server as **two static, per-role namespaces** — one authoring, one +reviewing — instead of switching profiles inside a running server or routing +through a dispatcher. It explains what to configure, why this model was +chosen, and what to expect from MCP clients. + +This is the deployment companion to +[`gitea-execution-profiles.md`](gitea-execution-profiles.md) (the profile +*model*) and [`llm-workflow-runbooks.md`](llm-workflow-runbooks.md) (the +workflows run on top of it). + +## The model + +Run two independent MCP server instances of the same `gitea-mcp` code, each +launched with exactly one static execution profile: + +| Namespace (MCP server name) | Profile (role) | Typical use | +|-----------------------------|----------------|-------------| +| `gitea-author` | an author profile | implement issues, push branches, open PRs, comment | +| `gitea-reviewer` | a reviewer profile | review, approve/request changes, merge | + +Properties: + +- **One process, one credential.** Each namespace authenticates as exactly + one Gitea identity for its entire lifetime. A session connected to + `gitea-author` can never approve or merge; a session connected to + `gitea-reviewer` cannot push branches or commit unless explicitly + configured. +- **`runtime_switching_supported: false`.** The running server never changes + identity. Choosing a role means choosing which namespace to connect to, + not asking the server to become someone else. +- **Roles are profiles, not LLMs.** Per the profile model, the LLM is not + the role — the profile is. The same LLM session may author under one + namespace and (in a *separate* session) review under the other. + +## Rejected alternatives — and why + +Both alternatives below were considered in the #139 discussion and are +**rejected for now**; dynamic in-process profile switching is +**not enabled in this deployment model**. (The runtime *can* support it +behind an explicit `allow_runtime_switching: true` config opt-in — see +[`gitea-execution-profiles.md`](gitea-execution-profiles.md) — but this +model deliberately leaves it off, so namespaces report +`runtime_switching_supported: false`.) + +- **Dynamic profile switching** (one server, `gitea_activate_profile`-style + role changes at runtime): rejected because a single process would hold, or + be able to obtain, both credentials; "which identity am I?" becomes + mutable state that injected instructions could target; and audit + attribution blurs when one process acts as multiple identities. +- **Dispatcher / router front door** (one entry point that forwards each + call to a role-appropriate backend): rejected because it concentrates + every credential behind one surface and re-creates the same escalation + problem with extra moving parts. + +Why the static dual-namespace model wins: + +- **Clearer audit.** Every audit record from a namespace maps to one + identity and one `audit_label`; there is no in-process identity history + to reconstruct. +- **Less credential concentration.** No process ever holds more than one + token. Compromise or prompt-injection of one session bounds the blast + radius to that role's allowed operations. +- **Simpler two-party review boundary.** Author and reviewer are different + authenticated identities in different processes; self-review/self-merge + checks stay structural, not behavioral. Note that namespaces alone do not + provide two-party review — one agent driving both namespaces in one + session still defeats it. Keep authoring and reviewing in separate + sessions. +- **Safer fail-closed behavior.** Each server validates its single profile + at startup and on every gated call; anything unknown, ambiguous, or + unresolved refuses. There is no "switch succeeded but half-applied" + state to reason about. + +## Client setup + +Each namespace is the same server binary launched with its own environment. +Configuration is by *reference only*: environment variables name a config +file and a profile entry; tokens stay in the operator's keychain/secret +store and never appear in client config, tool output, or this document. + +Conceptual client registration (names and variables only — adapt the launch +syntax to the client): + +```jsonc +{ + "mcpServers": { + "gitea-author": { + "command": "/venv/bin/python3", + "args": ["/mcp_server.py"], + "env": { + "GITEA_MCP_CONFIG": "", + "GITEA_MCP_PROFILE": "" + } + }, + "gitea-reviewer": { + "command": "/venv/bin/python3", + "args": ["/mcp_server.py"], + "env": { + "GITEA_MCP_CONFIG": "", + "GITEA_MCP_PROFILE": "" + } + } + } +} +``` + +- `GITEA_MCP_CONFIG` — path to the operator-owned profiles config (see + [`gitea-execution-profiles.md`](gitea-execution-profiles.md)). The file is + operator-owned; LLM sessions must never rewrite it. +- `GITEA_MCP_PROFILE` — the profile entry this namespace runs as. Exactly + one per namespace; never both. +- Verify after connecting: call `gitea_whoami` / `gitea_get_runtime_context` + and confirm the authenticated identity and allowed operations match the + namespace's role before doing any work. + +### "Auth unsupported" in some clients is normal + +Some MCP clients display an "Auth unsupported" (or similar) status for +custom/local stdio servers. That message refers to the client↔server MCP +authentication handshake, which local servers do not use — it does **not** +mean Gitea authentication failed. Gitea credentials are resolved by the +server itself from the configured profile. Trust `gitea_whoami`, not the +client's connection badge. + +## Reconnect / reload after changes + +The server reads its code and profile config **once, at process start**. A +long-running namespace does not see later changes, so after any of: + +- editing the profiles config (e.g. granting/removing an operation), +- merging server code that changes operation gating or tool surfaces, +- rotating the credential a profile references, + +the operator must **reload** the affected namespace — restart the server or +use the client's MCP reconnect action (e.g. `/mcp` in Claude Code) — before +the change takes effect. Symptoms of a stale namespace include gated calls +failing closed with operation-normalization errors even though the live +config is correct. Fail-closed is the intended behavior here: a stale +server refuses rather than guesses. Reconnect and re-verify with +`gitea_whoami`. + +## Related documents + +- [`gitea-execution-profiles.md`](gitea-execution-profiles.md) — the profile + model, reference profiles (`gitea-author`, `gitea-reviewer`), operation + naming, and safety rules. +- [`llm-workflow-runbooks.md`](llm-workflow-runbooks.md) — the author and + reviewer workflows run on top of these namespaces. +- [`safety-model.md`](safety-model.md) — fail-closed and gating principles. +- Issue #139 — the discussion and decision this document records. diff --git a/docs/gitea-execution-profiles.md b/docs/gitea-execution-profiles.md index 92a23bb..61324bd 100644 --- a/docs/gitea-execution-profiles.md +++ b/docs/gitea-execution-profiles.md @@ -265,6 +265,9 @@ For security-sensitive or high-risk tasks, the preferred safety model uses separ - `gitea-author`: Exposes tools configured with author permissions; cannot perform approvals or merges. - `gitea-reviewer`: Exposes tools configured with reviewer permissions; used for PR reviews and merges. This layout maintains physical separation of credentials and prevents privilege escalation within a single session. +This is the model accepted in #139; deployment details, rationale, and client +setup live in +[`gitea-dual-namespace-deployment.md`](gitea-dual-namespace-deployment.md). ### 3. Verification Post-Switching When dynamic profile switching is enabled and a profile is activated via `gitea_activate_profile`, the session MUST immediately: diff --git a/docs/llm-workflow-runbooks.md b/docs/llm-workflow-runbooks.md index 9da2fbd..1ab0b14 100644 --- a/docs/llm-workflow-runbooks.md +++ b/docs/llm-workflow-runbooks.md @@ -485,6 +485,7 @@ scripts/release-tag v0.4.0 --notes-file /tmp/release-notes.md --push - [`../skills/llm-project-workflow/SKILL.md`](../skills/llm-project-workflow/SKILL.md) — portable cross-project LLM workflow skill. - [`gitea-execution-profiles.md`](gitea-execution-profiles.md) — the profile model. +- [`gitea-dual-namespace-deployment.md`](gitea-dual-namespace-deployment.md) — static author/reviewer namespace deployment (#139 decision). - [`llm-agent-sha.md`](llm-agent-sha.md) — opaque agent attribution metadata (never an eligibility input). - [`safety-model.md`](safety-model.md) — trust boundaries and audit logging. - [`tool-boundaries.md`](tool-boundaries.md) — per-tool allowed operations. diff --git a/tests/test_dual_namespace_docs.py b/tests/test_dual_namespace_docs.py new file mode 100644 index 0000000..f8f3412 --- /dev/null +++ b/tests/test_dual_namespace_docs.py @@ -0,0 +1,70 @@ +"""Docs checks for the static dual-namespace deployment model (#143). + +Issue #139's accepted decision: run two static, per-role Gitea MCP +namespaces (`gitea-author`, `gitea-reviewer`) instead of dynamic profile +switching or a dispatcher. These tests keep the deployment doc present, +accurate to that decision, cross-linked from the operator docs, and free +of secret material or raw service URLs. +""" +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +DOC = REPO_ROOT / "docs" / "gitea-dual-namespace-deployment.md" + + +def _doc_text(): + assert DOC.is_file(), "missing docs/gitea-dual-namespace-deployment.md" + return DOC.read_text(encoding="utf-8") + + +def test_doc_exists_with_title_and_content(): + text = _doc_text() + assert text.lstrip().startswith("#"), "doc lacks a title" + assert len(text.strip()) > 1000, "doc is a stub" + assert "#139" in text, "doc does not reference the #139 decision" + + +def test_doc_names_both_role_namespaces(): + text = _doc_text() + assert "gitea-author" in text + assert "gitea-reviewer" in text + + +def test_doc_rejects_dynamic_switching_and_dispatcher(): + text = _doc_text().lower() + assert "dynamic" in text and "switching" in text + assert "dispatcher" in text + for marker in ("rejected", "not enabled"): + assert marker in text, f"doc does not state {marker!r}" + + +def test_doc_explains_why(): + text = _doc_text().lower() + for reason in ("audit", "credential", "two-party", "fail-closed"): + assert reason in text, f"doc does not explain the {reason!r} rationale" + + +def test_doc_gives_client_setup_examples_without_secret_values(): + text = _doc_text() + assert "GITEA_MCP_PROFILE" in text, "no profile env var in setup examples" + assert "GITEA_MCP_CONFIG" in text, "no config env var in setup examples" + # References/names only — never values, hosts, or key material. + for marker in ("https://", "http://", "ghp_", "Authorization:", + "BEGIN PRIVATE KEY"): + assert marker not in text, f"doc contains {marker!r}" + + +def test_doc_covers_auth_unsupported_and_reload(): + text = _doc_text() + assert "Auth unsupported" in text, ( + "doc does not explain the 'Auth unsupported' client message") + lower = text.lower() + assert "reconnect" in lower and "reload" in lower, ( + "doc does not cover reconnect/reload after profile/config changes") + + +def test_operator_docs_link_deployment_doc(): + for name in ("llm-workflow-runbooks.md", "gitea-execution-profiles.md"): + text = (REPO_ROOT / "docs" / name).read_text(encoding="utf-8") + assert "gitea-dual-namespace-deployment.md" in text, ( + f"docs/{name} does not link the deployment doc") -- 2.43.7