gitea_resolve_task_capability is not side-effect free: stale-runtime path touches mcp_config.json and terminates its own process (os._exit) causing client EOF #685

Open
opened 2026-07-12 18:23:59 -05:00 by jcwalker3 · 2 comments
Owner

Problem

gitea_resolve_task_capability is documented and treated as read-only, but its stale-runtime path mutates external MCP client configuration and destroys the active client transport. A nominally read-only preflight resolver must never do either.

Canonical root cause (verified at master 22698c1)

  1. gitea_resolve_task_capability calls _check_mcp_runtimes_diagnostics (gitea_mcp_server.py:11218; also invoked from :757 and :4645; function at :10913).
  2. When the serving process is classified stale (start_time < code_mtime of gitea_mcp_server.py, or boot HEAD != current HEAD), it invokes _trigger_mcp_auto_restart() (:10993–10994).
  3. _trigger_mcp_auto_restart (:10888) touches the MCP client config — os.utime(config_path, None) at :10901, default ~/.gemini/config/mcp_config.json, MCP_CONFIG_PATH override — changing its modification time to provoke a client reload.
  4. It then spawns a daemon thread that sleeps 1.0 s and calls os._exit(0) (:10905–10910), terminating the serving process from inside a tool call.
  5. The client transport closes with EOF; every namespace bound to that process drops mid-session.
  6. Net effect: a read-only capability resolver mutates external configuration and destroys the active transport.

Additional side effects present on every resolver call, even with a fresh runtime: _check_mcp_runtimes_diagnostics spawns subprocesses — ps -ax (:10927), a per-PID ps eww for every mcp_server.py process found (:10968), and git rev-parse HEAD (:10936).

Related poisoning defect: the staleness scan keeps only the newest process per profile (running_profiles, :10986), so a manually launched duplicate role server masks stale client-managed runtimes. Guardrail work for detecting/rejecting such duplicates is tracked separately (see linkage below).

Observed incident (2026-07-12, #584 / #672 cluster)

  • During PR #684 formal-verdict recovery, a resolver call on a stale runtime triggered the auto-restart path.
  • Config touch fingerprints: ~/.gemini/antigravity-ide/mcp_config.json mtime 19:08:29, ~/.gemini/config/mcp_config.json mtime 19:14:29 (local, 2026-07-12).
  • Client transport returned EOF; the session lost all role namespaces at once (matches the #584 signature).
  • The EOF then provoked an unsupported manual recovery attempt (GITEA_DUMMY=2 venv/bin/python3 mcp_server.py — see violation record on #584/#672), compounding the damage surface.
  • Formal APPROVE recording for PR #684 (evidence comment 10606, head d302602) was delayed until a supported client-managed reviewer namespace was available.

Acceptance criteria

  1. Capability resolution is side-effect free.
  2. No config file is touched or edited.
  3. No process is spawned, killed, signaled, or terminated.
  4. No thread is created for recovery.
  5. No client or daemon reload is triggered.
  6. Stale runtime returns a typed blocker: blocker_kind=runtime_reconnect_required, stop_required=true, restart_required=true, mutation_performed=false, and a precise exact_next_action.
  7. Recovery is owned by the IDE/client reconnect path — never by the resolver.
  8. A fresh process must repeat all preflight and cannot inherit capability, workflow, lease, or decision state.
  9. Tests verify unchanged: config contents, config mtime, process inventory, thread inventory, session state, lease state.
  10. Tests cover author, reviewer, merger, and reconciler profiles.
  11. Repeated resolver calls cannot cause an EOF/restart loop.
  12. Tool metadata and documentation accurately state whether the resolver is read-only.

Required tests

  • Resolver call on a stale runtime: assert typed blocker returned; assert config mtime/contents, process table, thread count, session/lease state all unchanged; assert the process is still alive afterward.
  • Resolver call repeated N times on a stale runtime: no exit, no reload, no EOF loop.
  • All four role profiles (author, reviewer, merger, reconciler) exercise the same contract.
  • Regression: _trigger_mcp_auto_restart (or successor) is unreachable from any read-only tool.

Explicit non-goals

  • Do not weaken stale-runtime detection — fail-closed reporting stays; only the self-recovery side effects are removed.
  • Do not add any new restart path; governed restart work belongs to #655/#657/#669.

Linkage

  • Incident cluster: #584 (transport EOF), #672 (Antigravity config drift; explicitly lists gitea_mcp_server.py auto-restart/config-touch as a non-repair path).
  • Related, not owners: #610 (resolver vs parity staleness disagreement), #630 (manual process-kill contamination), #655/#657 (restart governance/inventory — #657 will inventory _trigger_mcp_auto_restart; this issue removes it from the resolver), #669 (scoped recovery), #678 (sanctioned reconnect exposure).

Canonical issue state

STATE: ready-for-author
WHO_IS_NEXT: author
NEXT_ACTION: Make gitea_resolve_task_capability side-effect free; stale runtime returns typed runtime_reconnect_required blocker; delete config-touch/os._exit recovery from the resolver path
NEXT_PROMPT:
Role: AUTHOR
Repo: prgs / Scaled-Tech-Consulting / Gitea-Tools
Issue: this issue

1. gitea_whoami + gitea_resolve_task_capability(work_issue) on prgs-author; bind an issue-backed worktree under branches/ (never the root checkout).
2. Remove _trigger_mcp_auto_restart invocation from _check_mcp_runtimes_diagnostics and any other read-only path; keep stale detection.
3. Stale runtime → return typed blocker: blocker_kind=runtime_reconnect_required, stop_required=true, restart_required=true, mutation_performed=false, precise exact_next_action pointing at IDE/client reconnect.
4. Eliminate config os.utime, thread spawn, and os._exit from the resolver path entirely; recovery is owned by the IDE/client.
5. Minimize/eliminate subprocess use in the resolver hot path or document why any read-only subprocess is required.
6. Tests per issue AC 9–11 across all four role profiles; verify config contents+mtime, process/thread inventory, session/lease state unchanged; repeated-call loop test.
7. Update tool docstring/docs to state the true read-only contract.
8. PR with Closes reference to this issue; stop; hand off to independent reviewer.

Required final evidence

PR removing the side effects, typed-blocker output sample, and the unchanged-environment test battery across all four profiles.

## Problem `gitea_resolve_task_capability` is documented and treated as **read-only**, but its stale-runtime path mutates external MCP client configuration and destroys the active client transport. A nominally read-only preflight resolver must never do either. ## Canonical root cause (verified at master `22698c1`) 1. `gitea_resolve_task_capability` calls `_check_mcp_runtimes_diagnostics` (`gitea_mcp_server.py:11218`; also invoked from `:757` and `:4645`; function at `:10913`). 2. When the serving process is classified stale (`start_time < code_mtime` of `gitea_mcp_server.py`, or boot HEAD != current HEAD), it invokes `_trigger_mcp_auto_restart()` (`:10993–10994`). 3. `_trigger_mcp_auto_restart` (`:10888`) touches the MCP client config — `os.utime(config_path, None)` at `:10901`, default `~/.gemini/config/mcp_config.json`, `MCP_CONFIG_PATH` override — changing its modification time to provoke a client reload. 4. It then spawns a daemon thread that sleeps 1.0 s and calls `os._exit(0)` (`:10905–10910`), terminating the serving process from inside a tool call. 5. The client transport closes with EOF; every namespace bound to that process drops mid-session. 6. Net effect: a read-only capability resolver mutates external configuration and destroys the active transport. Additional side effects present on **every** resolver call, even with a fresh runtime: `_check_mcp_runtimes_diagnostics` spawns subprocesses — `ps -ax` (`:10927`), a per-PID `ps eww` for every `mcp_server.py` process found (`:10968`), and `git rev-parse HEAD` (`:10936`). Related poisoning defect: the staleness scan keeps only the newest process per profile (`running_profiles`, `:10986`), so a manually launched duplicate role server masks stale client-managed runtimes. Guardrail work for detecting/rejecting such duplicates is tracked separately (see linkage below). ## Observed incident (2026-07-12, #584 / #672 cluster) * During PR #684 formal-verdict recovery, a resolver call on a stale runtime triggered the auto-restart path. * Config touch fingerprints: `~/.gemini/antigravity-ide/mcp_config.json` mtime 19:08:29, `~/.gemini/config/mcp_config.json` mtime 19:14:29 (local, 2026-07-12). * Client transport returned EOF; the session lost all role namespaces at once (matches the #584 signature). * The EOF then provoked an **unsupported manual recovery attempt** (`GITEA_DUMMY=2 venv/bin/python3 mcp_server.py` — see violation record on #584/#672), compounding the damage surface. * Formal APPROVE recording for PR #684 (evidence comment 10606, head d302602) was delayed until a supported client-managed reviewer namespace was available. ## Acceptance criteria 1. Capability resolution is side-effect free. 2. No config file is touched or edited. 3. No process is spawned, killed, signaled, or terminated. 4. No thread is created for recovery. 5. No client or daemon reload is triggered. 6. Stale runtime returns a typed blocker: `blocker_kind=runtime_reconnect_required`, `stop_required=true`, `restart_required=true`, `mutation_performed=false`, and a precise `exact_next_action`. 7. Recovery is owned by the IDE/client reconnect path — never by the resolver. 8. A fresh process must repeat all preflight and cannot inherit capability, workflow, lease, or decision state. 9. Tests verify unchanged: config contents, config mtime, process inventory, thread inventory, session state, lease state. 10. Tests cover author, reviewer, merger, and reconciler profiles. 11. Repeated resolver calls cannot cause an EOF/restart loop. 12. Tool metadata and documentation accurately state whether the resolver is read-only. ## Required tests * Resolver call on a stale runtime: assert typed blocker returned; assert config mtime/contents, process table, thread count, session/lease state all unchanged; assert the process is still alive afterward. * Resolver call repeated N times on a stale runtime: no exit, no reload, no EOF loop. * All four role profiles (author, reviewer, merger, reconciler) exercise the same contract. * Regression: `_trigger_mcp_auto_restart` (or successor) is unreachable from any read-only tool. ## Explicit non-goals * Do not weaken stale-runtime **detection** — fail-closed reporting stays; only the self-recovery side effects are removed. * Do not add any new restart path; governed restart work belongs to #655/#657/#669. ## Linkage * Incident cluster: #584 (transport EOF), #672 (Antigravity config drift; explicitly lists `gitea_mcp_server.py` auto-restart/config-touch as a non-repair path). * Related, not owners: #610 (resolver vs parity staleness disagreement), #630 (manual process-kill contamination), #655/#657 (restart governance/inventory — #657 will inventory `_trigger_mcp_auto_restart`; this issue removes it from the resolver), #669 (scoped recovery), #678 (sanctioned reconnect exposure). ## Canonical issue state ```text STATE: ready-for-author WHO_IS_NEXT: author NEXT_ACTION: Make gitea_resolve_task_capability side-effect free; stale runtime returns typed runtime_reconnect_required blocker; delete config-touch/os._exit recovery from the resolver path NEXT_PROMPT: Role: AUTHOR Repo: prgs / Scaled-Tech-Consulting / Gitea-Tools Issue: this issue 1. gitea_whoami + gitea_resolve_task_capability(work_issue) on prgs-author; bind an issue-backed worktree under branches/ (never the root checkout). 2. Remove _trigger_mcp_auto_restart invocation from _check_mcp_runtimes_diagnostics and any other read-only path; keep stale detection. 3. Stale runtime → return typed blocker: blocker_kind=runtime_reconnect_required, stop_required=true, restart_required=true, mutation_performed=false, precise exact_next_action pointing at IDE/client reconnect. 4. Eliminate config os.utime, thread spawn, and os._exit from the resolver path entirely; recovery is owned by the IDE/client. 5. Minimize/eliminate subprocess use in the resolver hot path or document why any read-only subprocess is required. 6. Tests per issue AC 9–11 across all four role profiles; verify config contents+mtime, process/thread inventory, session/lease state unchanged; repeated-call loop test. 7. Update tool docstring/docs to state the true read-only contract. 8. PR with Closes reference to this issue; stop; hand off to independent reviewer. ``` ## Required final evidence PR removing the side effects, typed-blocker output sample, and the unchanged-environment test battery across all four profiles.
Author
Owner

Factual correction (CONTROLLER) — causation not confirmed for recent EOF incident

Canonical Issue State

STATE:
ready-for-author

WHO_IS_NEXT:
author

NEXT_ACTION:
Continue #685 implementation (side-effect-free capability resolver) independently of #699; do not treat the recent 401/auth incident as proof of #685 causation.

NEXT_PROMPT:

Role: AUTHOR
Repo: prgs / Scaled-Tech-Consulting / Gitea-Tools
Issue: #685 (resolver side-effect free) — separate from #699 (auth structured errors / transport survival)

Implement only #685 ACs. Cross-link #699 if discussing EOF classes, but do not mix scopes.

WHAT_HAPPENED:
Controller factual correction after managed-log review of a recent EOF incident. Causation was an unhandled HTTP 401 authentication exception escaping the MCP tool boundary (revoked/stale Keychain credential → Gitea 401 → unhandled RuntimeError → client EOF). This was initially suspected to be #685 but was not confirmed as #685 causation. #685 remains the resolver config-touch + os._exit defect. Durable tracking for the auth path is #699.

WHY:
Prevent incorrect root-cause attribution that would mis-prioritize or incorrectly close #685. Keep two distinct EOF classes separate.

RELATED_PRS:
PR #696 (reviewer held until Keychain recovery + #699 transport-survival resolved). Adjacent context only: #695, #697, #698.

BLOCKERS:
none newly introduced for #685 by this correction

VALIDATION:

  • #685 body still describes resolver config-touch + os._exit (scope unchanged)
  • #699 created (type:bug, status:ready, mcp-health, safety, workflow-hardening, anti-stomp)
  • Live whoami after Keychain multi-account cleanup: author=jcwalker3; reviewer/reconciler=sysadmin

LAST_UPDATED_BY:
controller

## Factual correction (CONTROLLER) — causation not confirmed for recent EOF incident ### Canonical Issue State STATE: ready-for-author WHO_IS_NEXT: author NEXT_ACTION: Continue #685 implementation (side-effect-free capability resolver) independently of #699; do not treat the recent 401/auth incident as proof of #685 causation. NEXT_PROMPT: ```text Role: AUTHOR Repo: prgs / Scaled-Tech-Consulting / Gitea-Tools Issue: #685 (resolver side-effect free) — separate from #699 (auth structured errors / transport survival) Implement only #685 ACs. Cross-link #699 if discussing EOF classes, but do not mix scopes. ``` WHAT_HAPPENED: Controller factual correction after managed-log review of a recent EOF incident. Causation was an unhandled HTTP 401 authentication exception escaping the MCP tool boundary (revoked/stale Keychain credential → Gitea 401 → unhandled RuntimeError → client EOF). This was initially suspected to be #685 but was **not confirmed** as #685 causation. #685 remains the resolver config-touch + os._exit defect. Durable tracking for the auth path is **#699**. WHY: Prevent incorrect root-cause attribution that would mis-prioritize or incorrectly close #685. Keep two distinct EOF classes separate. RELATED_PRS: PR #696 (reviewer held until Keychain recovery + #699 transport-survival resolved). Adjacent context only: #695, #697, #698. BLOCKERS: none newly introduced for #685 by this correction VALIDATION: - #685 body still describes resolver config-touch + os._exit (scope unchanged) - #699 created (type:bug, status:ready, mcp-health, safety, workflow-hardening, anti-stomp) - Live whoami after Keychain multi-account cleanup: author=jcwalker3; reviewer/reconciler=sysadmin LAST_UPDATED_BY: controller
Author
Owner

[THREAD STATE LEDGER] Issue #685 — attribution correction for the 2026-07-13 INC-695-P1-CRED EOF; the defect itself remains open and unaffected

What is true now:

  • Server-side decision state: no server-side review or merge state changed by this comment; #685 remains in open state with its acceptance criteria unchanged.
  • Local verdict/state: attribution correction. Earlier statements during INC-695-P1-CRED-2026-07-13 (incident #700) attributed that incident's namespace-wide transport EOF to this issue's resolver self-exit path. Managed-log diagnosis after recovery shows the observed EOF was caused by an outdated revoked Keychain credential plus an unhandled HTTP-authentication exception escaping the tool boundary — the defect now tracked as #699. #685 causation for that specific event was NOT confirmed. This correction does not weaken #685: the resolver side-effect defect (mcp_config.json touch + os._exit on the stale-runtime path) was verified against code at 22698c1 and stands on its own evidence, including the 2026-07-12 #584/#672 incident.
  • Latest known validation: during the 2026-07-13 recovery pass, gitea_resolve_task_capability was called on both the author and reconciler namespaces after credential rotation and managed reconnect; both calls returned normally with no self-exit, no config-touch fingerprint, and no EOF (in-parity runtime at 2376567, so the stale-runtime path was not exercised).

What changed:

  • The causal chain for the 2026-07-13 EOF is reassigned from this issue to the authentication-failure path recorded in #699; incident #700 records the sanitized incident and recovery.
  • Cross-links: #699 explicitly excludes the #685 resolver scope, and #685 keeps sole ownership of removing the stale-runtime self-recovery side effects.

What is blocked:

  • Blocker classification: no blocker
  • Implementation of this issue can proceed independently; it is not on the INC-695 critical path (#699#697#698 → fresh PR #696 review), though it remains high-priority mcp-health work.

Who/what acts next:

  • Next actor: author
  • Required action: implement per this issue's existing NEXT_PROMPT (remove config-touch/os._exit from the resolver path, typed runtime_reconnect_required blocker, side-effect-free contract tests across all four role profiles).
  • Do not do: do not re-attribute the 2026-07-13 INC-695 EOF to this issue in future handoffs; do not absorb #699's authentication-error mapping scope into this issue; do not exercise the stale-runtime resolver path on a live session to "reproduce" — it terminates the serving process.

Canonical Issue State

STATE:
attribution-corrected-defect-open

WHO_IS_NEXT:
author

NEXT_ACTION:
Implement the resolver side-effect removal per this issue's acceptance criteria; treat the 2026-07-13 INC-695 EOF as #699's evidence, not this issue's.

NEXT_PROMPT:

Role: AUTHOR (prgs-author)
Repo: prgs / Scaled-Tech-Consulting / Gitea-Tools
Issue: #685 — make gitea_resolve_task_capability side-effect free

Use the NEXT_PROMPT already recorded in the issue body (remove _trigger_mcp_auto_restart from read-only paths; typed blocker_kind=runtime_reconnect_required; unchanged-environment test battery across author/reviewer/merger/reconciler; repeated-call no-EOF-loop test).
Note for evidence sections: the 2026-07-13 INC-695 EOF is attributed to #699 (revoked credential + unhandled HTTP-authentication exception), not to this resolver path; cite the 2026-07-12 #584/#672 incident as this issue's observed occurrence.

WHAT_HAPPENED:
Post-recovery managed-log diagnosis reassigned the 2026-07-13 incident EOF to the authentication-failure path (#699); the earlier attribution to this issue's resolver self-exit was provisional and is corrected here.

WHY:
Accurate causation matters for both fixes: #699 owns tool-boundary authentication-error mapping and transport survival; #685 owns removing the stale-runtime self-recovery side effects verified at 22698c1.

RELATED_PRS:
none for this issue yet; PR #696 is unrelated to this correction (security hold per its comment 11071).

BLOCKERS:
No blocker for authoring this issue; it sits outside the INC-695 canonical order.

VALIDATION:
Both namespaces survived gitea_resolve_task_capability on 2026-07-13 post-rotation (author and reconciler; no self-exit, no EOF, runtime in parity at 2376567); #699 body records the reproduction chain for the authentication-failure EOF.

LAST_UPDATED_BY:
controller / prgs-author (jcwalker3)

[THREAD STATE LEDGER] Issue #685 — attribution correction for the 2026-07-13 INC-695-P1-CRED EOF; the defect itself remains open and unaffected What is true now: - Server-side decision state: no server-side review or merge state changed by this comment; #685 remains in open state with its acceptance criteria unchanged. - Local verdict/state: attribution correction. Earlier statements during INC-695-P1-CRED-2026-07-13 (incident #700) attributed that incident's namespace-wide transport EOF to this issue's resolver self-exit path. Managed-log diagnosis after recovery shows the observed EOF was caused by an outdated revoked Keychain credential plus an unhandled HTTP-authentication exception escaping the tool boundary — the defect now tracked as #699. #685 causation for that specific event was NOT confirmed. This correction does not weaken #685: the resolver side-effect defect (mcp_config.json touch + os._exit on the stale-runtime path) was verified against code at `22698c1` and stands on its own evidence, including the 2026-07-12 #584/#672 incident. - Latest known validation: during the 2026-07-13 recovery pass, `gitea_resolve_task_capability` was called on both the author and reconciler namespaces after credential rotation and managed reconnect; both calls returned normally with no self-exit, no config-touch fingerprint, and no EOF (in-parity runtime at `2376567`, so the stale-runtime path was not exercised). What changed: - The causal chain for the 2026-07-13 EOF is reassigned from this issue to the authentication-failure path recorded in #699; incident #700 records the sanitized incident and recovery. - Cross-links: #699 explicitly excludes the #685 resolver scope, and #685 keeps sole ownership of removing the stale-runtime self-recovery side effects. What is blocked: - Blocker classification: no blocker - Implementation of this issue can proceed independently; it is not on the INC-695 critical path (#699 → #697 → #698 → fresh PR #696 review), though it remains high-priority mcp-health work. Who/what acts next: - Next actor: author - Required action: implement per this issue's existing NEXT_PROMPT (remove config-touch/os._exit from the resolver path, typed runtime_reconnect_required blocker, side-effect-free contract tests across all four role profiles). - Do not do: do not re-attribute the 2026-07-13 INC-695 EOF to this issue in future handoffs; do not absorb #699's authentication-error mapping scope into this issue; do not exercise the stale-runtime resolver path on a live session to "reproduce" — it terminates the serving process. ## Canonical Issue State STATE: attribution-corrected-defect-open WHO_IS_NEXT: author NEXT_ACTION: Implement the resolver side-effect removal per this issue's acceptance criteria; treat the 2026-07-13 INC-695 EOF as #699's evidence, not this issue's. NEXT_PROMPT: ```text Role: AUTHOR (prgs-author) Repo: prgs / Scaled-Tech-Consulting / Gitea-Tools Issue: #685 — make gitea_resolve_task_capability side-effect free Use the NEXT_PROMPT already recorded in the issue body (remove _trigger_mcp_auto_restart from read-only paths; typed blocker_kind=runtime_reconnect_required; unchanged-environment test battery across author/reviewer/merger/reconciler; repeated-call no-EOF-loop test). Note for evidence sections: the 2026-07-13 INC-695 EOF is attributed to #699 (revoked credential + unhandled HTTP-authentication exception), not to this resolver path; cite the 2026-07-12 #584/#672 incident as this issue's observed occurrence. ``` WHAT_HAPPENED: Post-recovery managed-log diagnosis reassigned the 2026-07-13 incident EOF to the authentication-failure path (#699); the earlier attribution to this issue's resolver self-exit was provisional and is corrected here. WHY: Accurate causation matters for both fixes: #699 owns tool-boundary authentication-error mapping and transport survival; #685 owns removing the stale-runtime self-recovery side effects verified at 22698c1. RELATED_PRS: none for this issue yet; PR #696 is unrelated to this correction (security hold per its comment 11071). BLOCKERS: No blocker for authoring this issue; it sits outside the INC-695 canonical order. VALIDATION: Both namespaces survived gitea_resolve_task_capability on 2026-07-13 post-rotation (author and reconciler; no self-exit, no EOF, runtime in parity at 2376567); #699 body records the reproduction chain for the authentication-failure EOF. LAST_UPDATED_BY: controller / prgs-author (jcwalker3)
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#685