Epic: Migrate the Gitea MCP fleet from local stdio processes to a remote MCP deployment #929

Open
opened 2026-07-26 00:28:12 -05:00 by jcwalker3 · 1 comment
Owner

Problem

The Gitea MCP fleet runs as five local operating-system processes on the operator's Mac. Each one is mcp_server.py launched by the LLM client, pinned to a single role by the GITEA_MCP_PROFILE environment variable, reading its token from the operator's macOS keychain, and speaking JSON-RPC over stdio pipes owned by that client.

That model is load-bearing in the code, not incidental. The transport is bound literally, once, at process start:

# gitea_mcp_server.py:23750
mcp_daemon_guard.bind_native_mcp_transport(transport="stdio")

and the safety architecture is built on top of that bind. Mutations fail closed unless the process can prove it was launched by a client with real stdio pipes and GITEA_CLIENT_MANAGED provenance; the master-parity gate compares the commit the process started at against the working tree on the same disk; author tools operate directly on local branches/ worktrees; and every lock, lease, and session record lives in local files keyed to a local PID.

The consequences of that coupling are the reason to move:

  • Capacity is one fleet per workstation. A second operator, a CI runner, or a cloud agent cannot attach at all.
  • Five processes must be respawned, and the whole fleet re-verified, on every client reconnect.
  • Role separation is enforced by which process a call reaches, so it cannot survive a shared endpoint without a per-request principal.
  • Runtime freshness is defined as "this process's startup commit matches this disk", which has no meaning for a host that does not carry the operator's checkout.
  • Credentials are bound to one human's keychain, so the trust boundary cannot be audited or rotated centrally.

Children

Ten children, dependency-ordered. A child is implementation-ready only when every blocker is closed.

# Issue Child Depends on Status
1 #930 Inventory every stdio- and localhost-coupled assumption in the MCP server ready
2 #931 Introduce a transport-neutral bind seam accepting a remote transport #930 blocked
3 #932 Replace per-process profile binding with per-request principal resolution #930, #931 blocked
4 #933 Replace macOS-keychain token resolution with a server-side credential provider #932 blocked
5 #934 Define remote-session provenance to replace the stdio client-managed guard #931, #932 blocked
6 #935 Redefine the master-parity mutation gate for a remote deployment #931 blocked
7 #936 Split local-filesystem tools from remotable tools and fail closed on the boundary #930 blocked
8 #937 Move session, lock, and lease state to a store safe for concurrent remote sessions #932, #936 blocked
9 #938 Stand up the authenticated remote MCP endpoint #933, #934, #935, #937 blocked
10 #939 Dual-run cutover, parity validation, and rollback runbook #938 blocked

Dependency order, as a critical path:

#930 ─┬─> #931 ─┬─> #932 ─┬─> #933 ─────────────┐
      │         │         ├─> #934 ─────────────┤
      │         └────────────> #935 ────────────┤
      │                   └─> #937 ─────────────┤──> #938 ──> #939
      └─> #936 ────────────────> #937 ──────────┘

The single implementation-ready child is #930. Everything else is blocked until its blockers close.

Epic completion gate

Do not decommission the local stdio fleet until all of the following hold:

  • #930 through #939 are closed.
  • A remote endpoint serves all five roles with per-request principal resolution, and no role's permissions can be obtained by a caller holding another role's credential.
  • No mutation path reads the operator's macOS keychain.
  • Every tool that requires the operator's local filesystem is explicitly classified, and calling one against the remote endpoint fails closed with a named blocker rather than acting on the wrong disk.
  • The runtime-freshness gate returns a defined, testable verdict on the remote deployment.
  • Two different LLM clients complete a supervised end-to-end canary against the remote endpoint for the author, reviewer, and merger roles.
  • The rollback runbook has been executed once, on purpose, and the local fleet was restored from it.

Non-goals

  • Changing the role taxonomy or the permission sets attached to each role.
  • Changing Gitea-side branch protection or the review and merge state machine.
  • Rewriting the author transactional-safety work tracked by issue #887.
  • Multi-tenant hosting for organizations beyond this one.

Duplicate verdict

No existing issue covers this scope. Verified against a full scan of every issue in this repository, numbers 1 through 927. Issue #887 lists replacing the MCP transport and role-namespace model as an explicit non-goal. Issue #631 is a web console for the control plane, a different surface. Issue #689 is deterministic namespace attachment within the current local model. Issue #808 concerns worker discovery, not transport.

## Problem The Gitea MCP fleet runs as five local operating-system processes on the operator's Mac. Each one is `mcp_server.py` launched by the LLM client, pinned to a single role by the `GITEA_MCP_PROFILE` environment variable, reading its token from the operator's macOS keychain, and speaking JSON-RPC over stdio pipes owned by that client. That model is load-bearing in the code, not incidental. The transport is bound literally, once, at process start: ``` # gitea_mcp_server.py:23750 mcp_daemon_guard.bind_native_mcp_transport(transport="stdio") ``` and the safety architecture is built on top of that bind. Mutations fail closed unless the process can prove it was launched by a client with real stdio pipes and `GITEA_CLIENT_MANAGED` provenance; the master-parity gate compares the commit the process started at against the working tree on the same disk; author tools operate directly on local `branches/` worktrees; and every lock, lease, and session record lives in local files keyed to a local PID. The consequences of that coupling are the reason to move: - Capacity is one fleet per workstation. A second operator, a CI runner, or a cloud agent cannot attach at all. - Five processes must be respawned, and the whole fleet re-verified, on every client reconnect. - Role separation is enforced by which process a call reaches, so it cannot survive a shared endpoint without a per-request principal. - Runtime freshness is defined as "this process's startup commit matches this disk", which has no meaning for a host that does not carry the operator's checkout. - Credentials are bound to one human's keychain, so the trust boundary cannot be audited or rotated centrally. ## Children Ten children, dependency-ordered. A child is implementation-ready only when every blocker is closed. | # | Issue | Child | Depends on | Status | | - | ----- | ----- | ---------- | ------ | | 1 | #930 | Inventory every stdio- and localhost-coupled assumption in the MCP server | — | ready | | 2 | #931 | Introduce a transport-neutral bind seam accepting a remote transport | #930 | blocked | | 3 | #932 | Replace per-process profile binding with per-request principal resolution | #930, #931 | blocked | | 4 | #933 | Replace macOS-keychain token resolution with a server-side credential provider | #932 | blocked | | 5 | #934 | Define remote-session provenance to replace the stdio client-managed guard | #931, #932 | blocked | | 6 | #935 | Redefine the master-parity mutation gate for a remote deployment | #931 | blocked | | 7 | #936 | Split local-filesystem tools from remotable tools and fail closed on the boundary | #930 | blocked | | 8 | #937 | Move session, lock, and lease state to a store safe for concurrent remote sessions | #932, #936 | blocked | | 9 | #938 | Stand up the authenticated remote MCP endpoint | #933, #934, #935, #937 | blocked | | 10 | #939 | Dual-run cutover, parity validation, and rollback runbook | #938 | blocked | Dependency order, as a critical path: ``` #930 ─┬─> #931 ─┬─> #932 ─┬─> #933 ─────────────┐ │ │ ├─> #934 ─────────────┤ │ └────────────> #935 ────────────┤ │ └─> #937 ─────────────┤──> #938 ──> #939 └─> #936 ────────────────> #937 ──────────┘ ``` The single implementation-ready child is #930. Everything else is blocked until its blockers close. ## Epic completion gate Do not decommission the local stdio fleet until all of the following hold: - #930 through #939 are closed. - A remote endpoint serves all five roles with per-request principal resolution, and no role's permissions can be obtained by a caller holding another role's credential. - No mutation path reads the operator's macOS keychain. - Every tool that requires the operator's local filesystem is explicitly classified, and calling one against the remote endpoint fails closed with a named blocker rather than acting on the wrong disk. - The runtime-freshness gate returns a defined, testable verdict on the remote deployment. - Two different LLM clients complete a supervised end-to-end canary against the remote endpoint for the author, reviewer, and merger roles. - The rollback runbook has been executed once, on purpose, and the local fleet was restored from it. ## Non-goals - Changing the role taxonomy or the permission sets attached to each role. - Changing Gitea-side branch protection or the review and merge state machine. - Rewriting the author transactional-safety work tracked by issue #887. - Multi-tenant hosting for organizations beyond this one. ## Duplicate verdict No existing issue covers this scope. Verified against a full scan of every issue in this repository, numbers 1 through 927. Issue #887 lists replacing the MCP transport and role-namespace model as an explicit non-goal. Issue #631 is a web console for the control plane, a different surface. Issue #689 is deterministic namespace attachment within the current local model. Issue #808 concerns worker discovery, not transport.
Author
Owner

[THREAD STATE LEDGER] Issue #929 — verified child chain, seven new security owners, and the issue that completes workstation credential removal

What is true now

  • Server-side decision state: the #930 through #939 chain is verified live at master aad5c8b42361d380a8eeb07b94b90815e594c2c5. Seven security requirements that no child owned now have durable trackers: #956, #957, #958, #959, #960, #961, and #962.
  • Local verdict/state: prgs-author session, identity jcwalker3, remote prgs, repo Scaled-Tech-Consulting/Gitea-Tools. Runtime parity verified: startup_head, daemon_start_head, local_head, current_head, and live_remote_head all aad5c8b42361d380a8eeb07b94b90815e594c2c5; mutation_safe true; live_stale false; restart_required false.
  • Latest known validation: each new issue was read back natively and confirms issue state open, full body, and valid workflow labels.

Verified #930 through #939 chain

# Issue Depends on Issue state
1 #930 — inventory stdio and localhost coupling closed
2 #931 — transport-neutral bind seam #930 open, unblocked
3 #932 — per-request principal resolution #930, #931 open, blocked
4 #933 — server-side credential provider #932 open, blocked
5 #934 — remote-session provenance #931, #932 open, blocked
6 #935 — remote master-parity gate #931 open, blocked
7 #936 — split local-filesystem from remotable tools #930 open, blocked
8 #937 — concurrent-safe session, lock, and lease store #932, #936 open, blocked
9 #938 — authenticated remote endpoint #933, #934, #935, #937 open, blocked
10 #939 — dual-run cutover, parity validation, rollback runbook #938 open, blocked

The declared dependency edges were confirmed against the control-plane edge table. Querying edges targeting #930 returned three issue_blocked_by_issue edges from #931, #932, and #936, each in state met with observed_state closed. #931 is therefore the only implementation-eligible child of the original ten.

New canonical owners created this session

Issue Requirement it owns Depends on
#956 Threat model, trust boundaries, service-per-boundary decomposition #930
#957 Deny-by-default authorization over the full request tuple; read-only by default #932, #934, #937, #956
#958 Host hardening and reproducible service packaging #938, #956
#959 Tamper-evident audit, full-surface secret redaction, attribution-is-not-authentication #938, #957
#960 Credential rotation, revocation, emergency client and session invalidation #933, #934, #938
#961 Decommission Gitea credentials and privileged local MCP processes from LLM workstations #938, #939, #960
#962 Credential-exfiltration canary against the remote endpoint #938, #957, #958, #959, #961

Why each was needed rather than folded into an existing child: #930 classifies portability, never trust, so no threat model exists. #932 carries six principal fields and states no deny-by-default or read-only posture. #938 covers transport, handshake, rate limiting, and logging, but no filesystem, service-account, egress, or packaging constraint. #700 AC1 and the closed #171 cover three redaction surfaces and raw URLs; structured errors, exceptions, traces, headers, and subprocess output are uncovered. The closed #18 added audit logging with no tamper-evidence. The closed #86 introduced LLM-Agent-SHA attribution and nothing forbids treating it as authentication. #933 and #938 cover credential resolution and a rotation runbook, not revocation or emergency invalidation as operations.

The issue that completes workstation credential and privileged-process removal

#961. This is the only issue whose completion definition is the stated end state: LLM clients possess no Gitea credentials or privileged infrastructure access.

Every other child makes the remote endpoint possible; none removes the credential from the workstation. #939 explicitly lists permanent decommissioning as a non-goal, calling it a separate decision for the operator. This epic's completion gate says no mutation path reads the operator's macOS keychain, but assigns that removal to no child. #933's non-goals retain the keychain provider. #961 closes that gap: it enumerates every workstation credential artifact, executes removal, converts every client to remote-endpoint configuration, and verifies absence by executed check rather than inspection.

Resulting dependency order

#930 (closed) ─┬─> #956
               ├─> #931 ─┬─> #932 ─┬─> #933 ─────────────┐
               │         │         ├─> #934 ─────────────┤
               │         │         └─> #937 ─────────────┤
               │         └─> #935 ────────────────────────┤──> #938 ──> #939
               └─> #936 ──> #937 ───────────────────────┘

#932 + #934 + #937 + #956 ──> #957
#938 + #956               ──> #958
#938 + #957               ──> #959
#933 + #934 + #938        ──> #960
#938 + #939 + #960        ──> #961      <-- completes workstation credential removal
#957 + #958 + #959 + #961 ──> #962      <-- credential-exfiltration canary
#962 + #963 + #899        ──> #964      <-- combined certification gate (under umbrella #955)

What changed

  • Server-side mutation ledger: #956, #957, #958, #959, #960, #961, and #962 created and parented to this epic; this comment. No existing issue body, label, branch, pull request, or lease was modified.
  • Local: none. No branch, worktree, commit, or push.

What is blocked

  • Blocker classification: no blocker.
  • All new issues except #956 carry the blocked status label pending their listed dependencies, which is their designed state.

Who/what acts next

  • Next actor: author.
  • Required action: implement #931, the only implementation-eligible child of the original chain, and #956, which depends only on the closed #930 and unblocks the security work.
  • Do not do: do not decommission the local fleet before #961; do not treat #939 as covering credential removal, which its non-goals explicitly exclude.

Canonical Issue State

STATE:
open — ten-child chain verified; seven security owners added; #961 identified as the end-state issue

WHO_IS_NEXT:
author

NEXT_ACTION:
Implement #931 and #956 in parallel; both depend only on the closed #930.

NEXT_PROMPT:

Role: AUTHOR
Repo: prgs / Scaled-Tech-Consulting / Gitea-Tools
Issue: #931 or #956

1. gitea_whoami + gitea_resolve_task_capability(work_issue) on prgs-author; bind a branches/ worktree.
2. Implement the selected issue's acceptance criteria only.
3. For #956 the deliverable is docs/remote-mcp/threat-model.md; it changes no server behavior.
4. Open a PR with a Closes reference for that issue; stop; hand off to an independent reviewer.

WHAT_HAPPENED:
A live roadmap audit mapped all nineteen secure-isolation requirements to canonical owners at master aad5c8b423. The existing #930 through #939 chain covered transport, principal resolution, credential provisioning, provenance, freshness, tool boundary, concurrent state, endpoint authentication, and cutover. Fifteen requirements were uncovered or only partially covered. Seven new issues now own them, and #961 is identified as the single issue whose completion makes the secure-isolation end state objectively true.

WHY:
This epic makes a remote endpoint possible but never removes the credential from the operator workstation, and #939 places that removal out of scope. Without #961 an LLM client still holds a Gitea token no matter how the endpoint behaves. Without #956 the later children have no trust model to scope themselves against. Without #957 a correctly authenticated caller can still act on a foreign lease or a stale head. Without #962 every control is asserted only by tests written alongside it, which #700 demonstrates is not trustworthy.

ISSUE: 929

RELATED_PRS:
none

BLOCKERS:
none

VALIDATION:
Parity verified before and after every mutation and unchanged at aad5c8b423 with mutation_safe true, live_stale false, restart_required false. The create_issue capability was resolved immediately before each creation. Each new issue was read back natively with issue state open, correct title, complete body, and valid workflow labels carrying exactly one type label and one status label. The duplicate search was repeated after every creation and returned exactly one instance of each new title. Dependency edges targeting #930 were confirmed live in state met.

LAST_UPDATED_BY:
jcwalker3 / prgs-author / AUTHOR / 2026-07-28

[THREAD STATE LEDGER] Issue #929 — verified child chain, seven new security owners, and the issue that completes workstation credential removal **What is true now** - Server-side decision state: the #930 through #939 chain is verified live at master `aad5c8b42361d380a8eeb07b94b90815e594c2c5`. Seven security requirements that no child owned now have durable trackers: #956, #957, #958, #959, #960, #961, and #962. - Local verdict/state: prgs-author session, identity jcwalker3, remote prgs, repo Scaled-Tech-Consulting/Gitea-Tools. Runtime parity verified: startup_head, daemon_start_head, local_head, current_head, and live_remote_head all `aad5c8b42361d380a8eeb07b94b90815e594c2c5`; mutation_safe true; live_stale false; restart_required false. - Latest known validation: each new issue was read back natively and confirms issue state open, full body, and valid workflow labels. **Verified #930 through #939 chain** | # | Issue | Depends on | Issue state | | - | ----- | ---------- | ----------- | | 1 | #930 — inventory stdio and localhost coupling | — | closed | | 2 | #931 — transport-neutral bind seam | #930 | open, unblocked | | 3 | #932 — per-request principal resolution | #930, #931 | open, blocked | | 4 | #933 — server-side credential provider | #932 | open, blocked | | 5 | #934 — remote-session provenance | #931, #932 | open, blocked | | 6 | #935 — remote master-parity gate | #931 | open, blocked | | 7 | #936 — split local-filesystem from remotable tools | #930 | open, blocked | | 8 | #937 — concurrent-safe session, lock, and lease store | #932, #936 | open, blocked | | 9 | #938 — authenticated remote endpoint | #933, #934, #935, #937 | open, blocked | | 10 | #939 — dual-run cutover, parity validation, rollback runbook | #938 | open, blocked | The declared dependency edges were confirmed against the control-plane edge table. Querying edges targeting #930 returned three `issue_blocked_by_issue` edges from #931, #932, and #936, each in state `met` with observed_state closed. #931 is therefore the only implementation-eligible child of the original ten. **New canonical owners created this session** | Issue | Requirement it owns | Depends on | | --- | --- | --- | | #956 | Threat model, trust boundaries, service-per-boundary decomposition | #930 | | #957 | Deny-by-default authorization over the full request tuple; read-only by default | #932, #934, #937, #956 | | #958 | Host hardening and reproducible service packaging | #938, #956 | | #959 | Tamper-evident audit, full-surface secret redaction, attribution-is-not-authentication | #938, #957 | | #960 | Credential rotation, revocation, emergency client and session invalidation | #933, #934, #938 | | #961 | Decommission Gitea credentials and privileged local MCP processes from LLM workstations | #938, #939, #960 | | #962 | Credential-exfiltration canary against the remote endpoint | #938, #957, #958, #959, #961 | Why each was needed rather than folded into an existing child: #930 classifies portability, never trust, so no threat model exists. #932 carries six principal fields and states no deny-by-default or read-only posture. #938 covers transport, handshake, rate limiting, and logging, but no filesystem, service-account, egress, or packaging constraint. #700 AC1 and the closed #171 cover three redaction surfaces and raw URLs; structured errors, exceptions, traces, headers, and subprocess output are uncovered. The closed #18 added audit logging with no tamper-evidence. The closed #86 introduced `LLM-Agent-SHA` attribution and nothing forbids treating it as authentication. #933 and #938 cover credential resolution and a rotation runbook, not revocation or emergency invalidation as operations. **The issue that completes workstation credential and privileged-process removal** **#961.** This is the only issue whose completion definition is the stated end state: LLM clients possess no Gitea credentials or privileged infrastructure access. Every other child makes the remote endpoint possible; none removes the credential from the workstation. #939 explicitly lists permanent decommissioning as a non-goal, calling it a separate decision for the operator. This epic's completion gate says no mutation path reads the operator's macOS keychain, but assigns that removal to no child. #933's non-goals retain the keychain provider. #961 closes that gap: it enumerates every workstation credential artifact, executes removal, converts every client to remote-endpoint configuration, and verifies absence by executed check rather than inspection. **Resulting dependency order** ``` #930 (closed) ─┬─> #956 ├─> #931 ─┬─> #932 ─┬─> #933 ─────────────┐ │ │ ├─> #934 ─────────────┤ │ │ └─> #937 ─────────────┤ │ └─> #935 ────────────────────────┤──> #938 ──> #939 └─> #936 ──> #937 ───────────────────────┘ #932 + #934 + #937 + #956 ──> #957 #938 + #956 ──> #958 #938 + #957 ──> #959 #933 + #934 + #938 ──> #960 #938 + #939 + #960 ──> #961 <-- completes workstation credential removal #957 + #958 + #959 + #961 ──> #962 <-- credential-exfiltration canary #962 + #963 + #899 ──> #964 <-- combined certification gate (under umbrella #955) ``` **What changed** - Server-side mutation ledger: #956, #957, #958, #959, #960, #961, and #962 created and parented to this epic; this comment. No existing issue body, label, branch, pull request, or lease was modified. - Local: none. No branch, worktree, commit, or push. **What is blocked** - Blocker classification: no blocker. - All new issues except #956 carry the blocked status label pending their listed dependencies, which is their designed state. **Who/what acts next** - Next actor: author. - Required action: implement #931, the only implementation-eligible child of the original chain, and #956, which depends only on the closed #930 and unblocks the security work. - Do not do: do not decommission the local fleet before #961; do not treat #939 as covering credential removal, which its non-goals explicitly exclude. ## Canonical Issue State STATE: open — ten-child chain verified; seven security owners added; #961 identified as the end-state issue WHO_IS_NEXT: author NEXT_ACTION: Implement #931 and #956 in parallel; both depend only on the closed #930. NEXT_PROMPT: ```text Role: AUTHOR Repo: prgs / Scaled-Tech-Consulting / Gitea-Tools Issue: #931 or #956 1. gitea_whoami + gitea_resolve_task_capability(work_issue) on prgs-author; bind a branches/ worktree. 2. Implement the selected issue's acceptance criteria only. 3. For #956 the deliverable is docs/remote-mcp/threat-model.md; it changes no server behavior. 4. Open a PR with a Closes reference for that issue; stop; hand off to an independent reviewer. ``` WHAT_HAPPENED: A live roadmap audit mapped all nineteen secure-isolation requirements to canonical owners at master aad5c8b42361d380a8eeb07b94b90815e594c2c5. The existing #930 through #939 chain covered transport, principal resolution, credential provisioning, provenance, freshness, tool boundary, concurrent state, endpoint authentication, and cutover. Fifteen requirements were uncovered or only partially covered. Seven new issues now own them, and #961 is identified as the single issue whose completion makes the secure-isolation end state objectively true. WHY: This epic makes a remote endpoint possible but never removes the credential from the operator workstation, and #939 places that removal out of scope. Without #961 an LLM client still holds a Gitea token no matter how the endpoint behaves. Without #956 the later children have no trust model to scope themselves against. Without #957 a correctly authenticated caller can still act on a foreign lease or a stale head. Without #962 every control is asserted only by tests written alongside it, which #700 demonstrates is not trustworthy. ISSUE: 929 RELATED_PRS: none BLOCKERS: none VALIDATION: Parity verified before and after every mutation and unchanged at aad5c8b42361d380a8eeb07b94b90815e594c2c5 with mutation_safe true, live_stale false, restart_required false. The create_issue capability was resolved immediately before each creation. Each new issue was read back natively with issue state open, correct title, complete body, and valid workflow labels carrying exactly one type label and one status label. The duplicate search was repeated after every creation and returned exactly one instance of each new title. Dependency edges targeting #930 were confirmed live in state met. LAST_UPDATED_BY: jcwalker3 / prgs-author / AUTHOR / 2026-07-28
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Scaled-Tech-Consulting/Gitea-Tools#929