Remote-MCP 02: Introduce a transport-neutral bind seam accepting a remote transport #931

Closed
opened 2026-07-26 00:29:55 -05:00 by jcwalker3 · 1 comment
Owner

Parent epic: #929.
Depends on: #930.

Problem

The server binds one transport, by name, at the bottom of the module:

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

Everything that later asks "is this a trusted native session" resolves that question through the value bound here, including assess_transport_for_auth_mint, the decision-lock provenance stamp that defaults to transport: untrusted, and the mutation guards that refuse offline imports. The string is treated as a constant in the guard logic rather than as configuration, so a remote transport currently has no way to be trusted, and no way to be distinguished from an untrusted offline import.

Scope

Introduce a seam so the bound transport is a first-class, validated value rather than a hardcoded literal, without changing what stdio sessions are allowed to do today.

  • Accept a transport identifier from deployment configuration, defaulting to stdio when unset.
  • Define the permitted set of transport identifiers and reject anything outside it at bind time, before any tool can dispatch.
  • Make every guard that currently reasons about "native transport" consult the bound value through one shared accessor instead of assuming stdio.
  • Record the bound transport in decision-lock provenance and in audit records, so an operator can tell from a durable record which transport performed a mutation.
  • Keep the fail-closed default: an unbound or unrecognized transport must refuse mutations, exactly as an offline import does today.

Acceptance criteria

  • The transport identifier is supplied by configuration, and an unset value still yields stdio.
  • An unrecognized transport identifier fails closed at bind time with a named reason, and no tool dispatches.
  • No guard reads the literal string stdio outside the shared accessor and the permitted-set definition.
  • Decision-lock provenance and audit records carry the bound transport identifier.
  • An offline import with no bind still fails closed on mutations, with the pre-existing reason code.
  • Behavior for a stdio session is unchanged: the full test suite passes with no new expected-value edits for stdio paths.

Verification

  • Tests cover: default bind, explicit stdio bind, an accepted non-stdio identifier, an unrecognized identifier, and no bind at all.
  • A test asserts that the transport identifier reaches a durable decision-lock record.
  • Full suite compared against the recorded baseline. Run it from a branches/ worktree, not from a temporary directory.

Non-goals

  • Implementing a remote transport listener. This child only makes the bind pluggable; child 9 stands up the endpoint.
  • Changing which operations each role may perform.
  • Changing the client-managed provenance rules. That is child 5, which builds on this seam.
Parent epic: #929. Depends on: #930. ## Problem The server binds one transport, by name, at the bottom of the module: ``` # gitea_mcp_server.py:23750 mcp_daemon_guard.bind_native_mcp_transport(transport="stdio") ``` Everything that later asks "is this a trusted native session" resolves that question through the value bound here, including `assess_transport_for_auth_mint`, the decision-lock provenance stamp that defaults to `transport: untrusted`, and the mutation guards that refuse offline imports. The string is treated as a constant in the guard logic rather than as configuration, so a remote transport currently has no way to be trusted, and no way to be distinguished from an untrusted offline import. ## Scope Introduce a seam so the bound transport is a first-class, validated value rather than a hardcoded literal, without changing what stdio sessions are allowed to do today. - Accept a transport identifier from deployment configuration, defaulting to `stdio` when unset. - Define the permitted set of transport identifiers and reject anything outside it at bind time, before any tool can dispatch. - Make every guard that currently reasons about "native transport" consult the bound value through one shared accessor instead of assuming stdio. - Record the bound transport in decision-lock provenance and in audit records, so an operator can tell from a durable record which transport performed a mutation. - Keep the fail-closed default: an unbound or unrecognized transport must refuse mutations, exactly as an offline import does today. ## Acceptance criteria - The transport identifier is supplied by configuration, and an unset value still yields `stdio`. - An unrecognized transport identifier fails closed at bind time with a named reason, and no tool dispatches. - No guard reads the literal string `stdio` outside the shared accessor and the permitted-set definition. - Decision-lock provenance and audit records carry the bound transport identifier. - An offline import with no bind still fails closed on mutations, with the pre-existing reason code. - Behavior for a stdio session is unchanged: the full test suite passes with no new expected-value edits for stdio paths. ## Verification - Tests cover: default bind, explicit stdio bind, an accepted non-stdio identifier, an unrecognized identifier, and no bind at all. - A test asserts that the transport identifier reaches a durable decision-lock record. - Full suite compared against the recorded baseline. Run it from a `branches/` worktree, not from a temporary directory. ## Non-goals - Implementing a remote transport listener. This child only makes the bind pluggable; child 9 stands up the endpoint. - Changing which operations each role may perform. - Changing the client-managed provenance rules. That is child 5, which builds on this seam.
jcwalker3 added status:ready and removed status:blocked labels 2026-07-26 02:26:37 -05:00
jcwalker3 added status:pr-open and removed status:ready labels 2026-07-28 10:10:38 -05:00
Author
Owner

[THREAD STATE LEDGER] Issue #931 / PR #966 — transport-neutral bind seam implemented; handed to reviewer

What is true now:

  • Issue state: open, status:pr-open
  • PR state: PR #966 open, mergeable, no formal review yet (reviews: [])
  • Current head SHA: 2fb835a1aa
  • Base SHA: 9b80e75ca3
  • Server-side decision state: PR #966 created and open; no review verdict recorded; merge not performed
  • Local verdict/state: implementation complete, worktree clean at the pushed head
  • Latest known validation: full suite run from the branches/ worktree; failing test IDs identical to the pinned base

What changed:

  • Branch feat/issue-931-transport-neutral-bind-seam created at base 9b80e75c
  • Commits c1626081 (the seam) and 2fb835a1 (anchor-regeneration stamp) pushed
  • PR #966 opened against master with Closes #931
  • Issue label moved to status:pr-open by the create_pr transition

What is blocked:

  • Blocker classification: no blocker

Who/what acts next:

  • Next actor: reviewer
  • Required action: independent formal review of PR #966 against the #931 acceptance criteria in a fresh session
  • Do not do: self-review, self-merge, or start #932
  • Resume from: PR #966 diff and its acceptance matrix

Canonical Issue State

STATE:
pr-open-awaiting-independent-review

WHO_IS_NEXT:
reviewer

NEXT_ACTION:
Perform an independent formal review of PR #966 (head 2fb835a1aa) against the six #931 acceptance criteria, then record APPROVE or REQUEST_CHANGES via gitea_submit_pr_review.

NEXT_PROMPT:

Role: REVIEWER
Repo: prgs / Scaled-Tech-Consulting / Gitea-Tools
PR: #966  Issue: #931
Head: 2fb835a1aa4cb6260445f2ef3f10057aabd2fbe7
Base: 9b80e75ca3f441fec2fb077a1b5f874faa0912e2

1. Native MCP only on gitea-reviewer / prgs-reviewer. Run gitea_whoami,
   gitea_load_review_workflow, then gitea_resolve_task_capability(review_pr).
   Bind an existing branches/ worktree. Call gitea_get_pr_review_feedback
   first; an empty reviews list is the proof this PR is unstarted.
2. Check AC1: unset GITEA_MCP_TRANSPORT still yields stdio.
3. Check AC2: an unregistered identifier fails closed at bind time with a
   named reason and no tool dispatches.
4. Check AC3: grep the tree; the stdio literal must appear only in
   mcp_transport_config (DEFAULT_TRANSPORT and the permitted set).
5. Check AC4: bound_transport reaches a durable decision-lock record via
   mcp_session_state.save_state.
6. Check AC5: an offline import with no bind still fails closed with the
   pre-existing reason code.
7. Check AC6: re-run the full suite from a branches/ worktree and compare
   failing test IDs against base 9b80e75c. Compare IDs, never counts.
8. Probe the seam adversarially: attempt to move the bound transport after
   bind via the environment, attempt a conflicting rebind, and attempt to
   reach a bind through a non-canonical entrypoint.
9. Record the verdict with gitea_submit_pr_review. Do not merge.

WHAT_HAPPENED:
Introduced mcp_transport_config as the single source of truth for the bound MCP transport: it owns the permitted set {stdio, streamable-http}, the stdio default, and the resolution of GITEA_MCP_TRANSPORT into a checked identifier. bind_native_mcp_transport now resolves from deployment configuration when no argument is given, validates before writing the runtime record, and pins the result; bound_transport is the shared accessor every transport-aware guard reads, and assert_transport_bound fails closed before mcp.run. A conflicting rebind is refused so two guards cannot observe different values. mutation_provenance_fields gained bound_transport, which reaches the durable decision lock and the audit records. Both former stdio literals in the entrypoint are gone. #956's anchor fixture and threat model were re-anchored for the 29 lines this change shifted, and the two boundary claims #931 makes false were restated.

Changed files: mcp_transport_config.py (new), tests/test_issue_931_transport_bind_seam.py (new), mcp_daemon_guard.py, gitea_mcp_server.py, mcp_server.py, mcp_session_state.py, irrecoverable_provenance.py, docs/remote-mcp/threat-model.md, docs/remote-mcp/threat-model-anchors.json.

Acceptance matrix (each criterion mapped to code and to independent test evidence):

  • AC1 configuration supplies the identifier, unset yields stdio -> mcp_transport_config.resolve_configured_transport; tests test_unset_yields_default, test_blank_and_whitespace_are_treated_as_unset, test_1_no_configured_transport_binds_default.
  • AC2 unrecognized identifier fails closed at bind with a named reason, no tool dispatches -> bind_native_mcp_transport validation arm; tests test_4_unregistered_identifier_rejected_at_bind_time, test_4c_superseded_sse_rejected_at_bind_time, test_5_invalid_bind_prevents_tool_service.
  • AC3 no guard reads the stdio literal outside the accessor and the permitted set -> mcp_transport_config is the only enumeration; test test_no_default_transport_literal_outside_the_seam scans five guard modules.
  • AC4 decision-lock provenance and audit records carry the identifier -> mutation_provenance_fields plus mcp_session_state.save_state; tests test_9_decision_lock_records_the_bound_transport, test_9b_unbound_runtime_records_no_transport_identifier.
  • AC5 offline import with no bind still fails closed with the pre-existing reason code -> unchanged assert_sanctioned_mutation_runtime; tests test_6_unbound_state_fails_closed, test_6b_no_runtime_at_all_fails_closed.
  • AC6 stdio behavior unchanged, no new expected-value edits for stdio paths -> trust-class transport field left intact, identifier added under a new key; tests test_10_trust_class_field_keeps_its_pre_931_values, test_10b_default_bind_reproduces_the_pre_931_runtime_record, plus the base/head comparison below.

WHY:
The bound transport was a literal in the authorization chain rather than configuration, so a remote transport could neither be trusted nor distinguished from an untrusted offline import. #931 is the seam that makes the identifier first-class, checked at bind, and durably recorded, without changing what a stdio session may do. Every later child of epic #929 that reasons about transport depends on this one accessor existing.

RELATED_PRS:
PR #966 (open, head 2fb835a1aa) implements this issue. PR #942 / issue #941 (merge performed at aab54d48) supplied the author-bootstrap prerequisite that let this issue obtain a sanctioned worktree. Issue #930 (closed) supplied the coupling inventory rows T1-T9 that scope this work.

BLOCKERS:
none

VALIDATION:
New module tests/test_issue_931_transport_bind_seam.py, 42 tests, exit 0. Targeted suites all exit 0: transport and daemon guard 33 passed; decision-lock and session-state 140 passed; #956 threat model 17 passed; capability registration 48 passed with 86 subtests; tool inventory 39 passed; provenance and security 34 passed. Full suite from branches/issue-931-transport-bind-seam: exit 1, 28 failed, 5843 passed, 6 skipped, 1047 subtests passed. Pinned base 9b80e75c from branches/baseline-931-9b80e75c: exit 1, 28 failed, 5801 passed, 6 skipped, 1047 subtests passed. Compared by failing test ID rather than by count, the two sets are identical in both directions; no failure was added and none was fixed, and the +42 passed are exactly this PR's new module. Not run: any deployment, restart, reconnect, or live remote-transport exercise, because no listener exists until #938.

Known limitations and explicit non-goals: the remote listener (#938), per-request principal resolution (#932), client-managed provenance policy (#934), TLS and remote client authentication, role capability sets, repository-binding semantics, deployment, client commissioning, concurrency certification, and unattended scheduling are all out of scope and untouched. streamable-http is accepted at the seam but nothing serves it yet. Issues #932, #934, #936, #938, #949 and #957 were read only to confirm scope boundaries and were not modified.

LAST_UPDATED_BY:
author (jcwalker3 / prgs-author / gitea-author namespace)

[THREAD STATE LEDGER] Issue #931 / PR #966 — transport-neutral bind seam implemented; handed to reviewer What is true now: - Issue state: open, status:pr-open - PR state: PR #966 open, mergeable, no formal review yet (reviews: []) - Current head SHA: 2fb835a1aa4cb6260445f2ef3f10057aabd2fbe7 - Base SHA: 9b80e75ca3f441fec2fb077a1b5f874faa0912e2 - Server-side decision state: PR #966 created and open; no review verdict recorded; merge not performed - Local verdict/state: implementation complete, worktree clean at the pushed head - Latest known validation: full suite run from the branches/ worktree; failing test IDs identical to the pinned base What changed: - Branch feat/issue-931-transport-neutral-bind-seam created at base 9b80e75c - Commits c1626081 (the seam) and 2fb835a1 (anchor-regeneration stamp) pushed - PR #966 opened against master with Closes #931 - Issue label moved to status:pr-open by the create_pr transition What is blocked: - Blocker classification: no blocker Who/what acts next: - Next actor: reviewer - Required action: independent formal review of PR #966 against the #931 acceptance criteria in a fresh session - Do not do: self-review, self-merge, or start #932 - Resume from: PR #966 diff and its acceptance matrix ## Canonical Issue State STATE: pr-open-awaiting-independent-review WHO_IS_NEXT: reviewer NEXT_ACTION: Perform an independent formal review of PR #966 (head 2fb835a1aa4cb6260445f2ef3f10057aabd2fbe7) against the six #931 acceptance criteria, then record APPROVE or REQUEST_CHANGES via gitea_submit_pr_review. NEXT_PROMPT: ```text Role: REVIEWER Repo: prgs / Scaled-Tech-Consulting / Gitea-Tools PR: #966 Issue: #931 Head: 2fb835a1aa4cb6260445f2ef3f10057aabd2fbe7 Base: 9b80e75ca3f441fec2fb077a1b5f874faa0912e2 1. Native MCP only on gitea-reviewer / prgs-reviewer. Run gitea_whoami, gitea_load_review_workflow, then gitea_resolve_task_capability(review_pr). Bind an existing branches/ worktree. Call gitea_get_pr_review_feedback first; an empty reviews list is the proof this PR is unstarted. 2. Check AC1: unset GITEA_MCP_TRANSPORT still yields stdio. 3. Check AC2: an unregistered identifier fails closed at bind time with a named reason and no tool dispatches. 4. Check AC3: grep the tree; the stdio literal must appear only in mcp_transport_config (DEFAULT_TRANSPORT and the permitted set). 5. Check AC4: bound_transport reaches a durable decision-lock record via mcp_session_state.save_state. 6. Check AC5: an offline import with no bind still fails closed with the pre-existing reason code. 7. Check AC6: re-run the full suite from a branches/ worktree and compare failing test IDs against base 9b80e75c. Compare IDs, never counts. 8. Probe the seam adversarially: attempt to move the bound transport after bind via the environment, attempt a conflicting rebind, and attempt to reach a bind through a non-canonical entrypoint. 9. Record the verdict with gitea_submit_pr_review. Do not merge. ``` WHAT_HAPPENED: Introduced mcp_transport_config as the single source of truth for the bound MCP transport: it owns the permitted set {stdio, streamable-http}, the stdio default, and the resolution of GITEA_MCP_TRANSPORT into a checked identifier. bind_native_mcp_transport now resolves from deployment configuration when no argument is given, validates before writing the runtime record, and pins the result; bound_transport is the shared accessor every transport-aware guard reads, and assert_transport_bound fails closed before mcp.run. A conflicting rebind is refused so two guards cannot observe different values. mutation_provenance_fields gained bound_transport, which reaches the durable decision lock and the audit records. Both former stdio literals in the entrypoint are gone. #956's anchor fixture and threat model were re-anchored for the 29 lines this change shifted, and the two boundary claims #931 makes false were restated. Changed files: mcp_transport_config.py (new), tests/test_issue_931_transport_bind_seam.py (new), mcp_daemon_guard.py, gitea_mcp_server.py, mcp_server.py, mcp_session_state.py, irrecoverable_provenance.py, docs/remote-mcp/threat-model.md, docs/remote-mcp/threat-model-anchors.json. Acceptance matrix (each criterion mapped to code and to independent test evidence): - AC1 configuration supplies the identifier, unset yields stdio -> mcp_transport_config.resolve_configured_transport; tests test_unset_yields_default, test_blank_and_whitespace_are_treated_as_unset, test_1_no_configured_transport_binds_default. - AC2 unrecognized identifier fails closed at bind with a named reason, no tool dispatches -> bind_native_mcp_transport validation arm; tests test_4_unregistered_identifier_rejected_at_bind_time, test_4c_superseded_sse_rejected_at_bind_time, test_5_invalid_bind_prevents_tool_service. - AC3 no guard reads the stdio literal outside the accessor and the permitted set -> mcp_transport_config is the only enumeration; test test_no_default_transport_literal_outside_the_seam scans five guard modules. - AC4 decision-lock provenance and audit records carry the identifier -> mutation_provenance_fields plus mcp_session_state.save_state; tests test_9_decision_lock_records_the_bound_transport, test_9b_unbound_runtime_records_no_transport_identifier. - AC5 offline import with no bind still fails closed with the pre-existing reason code -> unchanged assert_sanctioned_mutation_runtime; tests test_6_unbound_state_fails_closed, test_6b_no_runtime_at_all_fails_closed. - AC6 stdio behavior unchanged, no new expected-value edits for stdio paths -> trust-class transport field left intact, identifier added under a new key; tests test_10_trust_class_field_keeps_its_pre_931_values, test_10b_default_bind_reproduces_the_pre_931_runtime_record, plus the base/head comparison below. WHY: The bound transport was a literal in the authorization chain rather than configuration, so a remote transport could neither be trusted nor distinguished from an untrusted offline import. #931 is the seam that makes the identifier first-class, checked at bind, and durably recorded, without changing what a stdio session may do. Every later child of epic #929 that reasons about transport depends on this one accessor existing. RELATED_PRS: PR #966 (open, head 2fb835a1aa4cb6260445f2ef3f10057aabd2fbe7) implements this issue. PR #942 / issue #941 (merge performed at aab54d48) supplied the author-bootstrap prerequisite that let this issue obtain a sanctioned worktree. Issue #930 (closed) supplied the coupling inventory rows T1-T9 that scope this work. BLOCKERS: none VALIDATION: New module tests/test_issue_931_transport_bind_seam.py, 42 tests, exit 0. Targeted suites all exit 0: transport and daemon guard 33 passed; decision-lock and session-state 140 passed; #956 threat model 17 passed; capability registration 48 passed with 86 subtests; tool inventory 39 passed; provenance and security 34 passed. Full suite from branches/issue-931-transport-bind-seam: exit 1, 28 failed, 5843 passed, 6 skipped, 1047 subtests passed. Pinned base 9b80e75c from branches/baseline-931-9b80e75c: exit 1, 28 failed, 5801 passed, 6 skipped, 1047 subtests passed. Compared by failing test ID rather than by count, the two sets are identical in both directions; no failure was added and none was fixed, and the +42 passed are exactly this PR's new module. Not run: any deployment, restart, reconnect, or live remote-transport exercise, because no listener exists until #938. Known limitations and explicit non-goals: the remote listener (#938), per-request principal resolution (#932), client-managed provenance policy (#934), TLS and remote client authentication, role capability sets, repository-binding semantics, deployment, client commissioning, concurrency certification, and unattended scheduling are all out of scope and untouched. streamable-http is accepted at the seam but nothing serves it yet. Issues #932, #934, #936, #938, #949 and #957 were read only to confirm scope boundaries and were not modified. LAST_UPDATED_BY: author (jcwalker3 / prgs-author / gitea-author namespace)
sysadmin removed the status:pr-open label 2026-07-28 13:36:54 -05:00
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#931