Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,23 @@ All notable changes to `codex-plugin-doctor` are documented here.

This changelog groups the shipped work into product-level release blocks instead of repeating every low-level git diff in isolation.

## [1.53.0] - 2026-07-26

### Added

- added a bounded remote MCP transport-reliability readiness check that performs a post-initialize SSE `GET`, preserves session propagation, and observes bounded resume and session-restart paths
- added `allow-session-lifecycle` and `require-remote-reliability` GitHub Action inputs for explicit session-lifecycle approval and strict reliability-result gating

### Changed

- extended remote runtime reports, scorecards, output contracts, and release evidence with additive transport-reliability results
- made `--require-remote-reliability` fail when an attempted remote reliability scorecard does not pass; the flag grants no network consent and does not affect local-only runs

### Security

- kept remote session termination opt-in: after a valid session identifier, `--allow-session-lifecycle` permits at most one bounded session `DELETE`
- hardened remote reliability diagnostics to redact response content and sensitive transport details

## [1.52.0] - 2026-07-25

### Added
Expand Down
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,18 @@ Runtime MCP validation with `--runtime`:

### Remote MCP Readiness

Remote MCP runtime probing is disabled until you explicitly pass `--allow-network`. Local endpoints also require `--allow-local-network`:
Remote MCP runtime probing is disabled until you explicitly pass `--runtime --allow-network`. Loopback endpoints also require `--allow-local-network`:

```bash
codex-plugin-doctor check ./remote-mcp --runtime --allow-network
codex-plugin-doctor check ./remote-mcp --runtime --allow-network --allow-local-network
```

The probe makes only bounded, read-only protocol and OAuth metadata-discovery requests, redacts report output, and applies SSRF controls. It does not authenticate or follow redirects. See [Remote MCP Readiness](./docs/architecture/remote-mcp-readiness.md).
The probe makes only bounded protocol and OAuth metadata-discovery requests, redacts report output, and applies SSRF controls. It does not authenticate or follow redirects.

Remote transport reliability adds one bounded SSE GET after initialization. HTTP `200` must be `text/event-stream`; HTTP `405` is compliant when the endpoint does not offer server-to-client SSE. At most one SSE resume and one session restart are attempted, and no remote response content is retained. This is a bounded readiness check, not a live interoperability, delivery-guarantee, or load-test claim.

`--allow-session-lifecycle` is disabled by default and is state-changing: only after a valid `MCP-Session-Id` it permits one bounded session `DELETE`. `--require-remote-reliability` is a strict result gate: it fails unless every attempted remote reliability scorecard passes. Local-only runs are unaffected. It grants no network consent, so `--runtime --allow-network` (and loopback consent when applicable) remain required. See [Remote MCP Readiness](./docs/architecture/remote-mcp-readiness.md) and [Remote MCP Transport Reliability](./docs/architecture/remote-mcp-transport-reliability.md).

Output formats:

Expand Down Expand Up @@ -438,9 +442,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: Esquetta/CodexPluginDoctor@v1.52.0
- uses: Esquetta/CodexPluginDoctor@v1.53.0
with:
version: "1.52.0"
version: "1.53.0"
path: .
runtime: "true"
policy: codex-publish
Expand Down
18 changes: 18 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ inputs:
description: Explicitly allow remote MCP runtime probes to contact loopback endpoints only (localhost, 127.0.0.0/8, or ::1). Private, link-local, multicast, unspecified, reserved, and NAT64 ranges remain blocked.
required: false
default: "false"
allow-session-lifecycle:
description: Explicitly allow remote MCP runtime probing to send one state-changing session termination request when a valid session is issued.
required: false
default: "false"
require-remote-reliability:
description: Fail the validation result unless every attempted remote MCP reliability scorecard passes; this does not grant network access.
required: false
default: "false"
installed:
description: Validate plugins from the local Codex plugin cache.
required: false
Expand Down Expand Up @@ -170,6 +178,8 @@ runs:
env:
ALLOW_NETWORK_INPUT: ${{ inputs['allow-network'] }}
ALLOW_LOCAL_NETWORK_INPUT: ${{ inputs['allow-local-network'] }}
ALLOW_SESSION_LIFECYCLE_INPUT: ${{ inputs['allow-session-lifecycle'] }}
REQUIRE_REMOTE_RELIABILITY_INPUT: ${{ inputs['require-remote-reliability'] }}
CORPUS_METRICS_MANIFEST_INPUT: ${{ inputs['corpus-metrics-manifest'] }}
CORPUS_METRICS_BASELINE_INPUT: ${{ inputs['corpus-metrics-baseline'] }}
CORPUS_METRICS_FAIL_ON_REGRESSION_INPUT: ${{ inputs['corpus-metrics-fail-on-regression'] }}
Expand Down Expand Up @@ -217,6 +227,14 @@ runs:
args+=(--allow-local-network)
fi

if [[ "$ALLOW_SESSION_LIFECYCLE_INPUT" == "true" ]]; then
args+=(--allow-session-lifecycle)
fi

if [[ "$REQUIRE_REMOTE_RELIABILITY_INPUT" == "true" ]]; then
args+=(--require-remote-reliability)
fi

if [[ -n "${{ inputs.config }}" ]]; then
args+=(--config "${{ inputs.config }}")
fi
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ This directory contains public documentation for users, contributors, and securi
- [Runtime Sandbox and External Corpus](architecture/runtime-sandbox-and-external-corpus.md)
- [MCP 2025-11 Conformance](architecture/mcp-2025-11-conformance.md)
- [Remote MCP Readiness](architecture/remote-mcp-readiness.md)
- [Remote MCP Transport Reliability](architecture/remote-mcp-transport-reliability.md)
- [Real-World Corpus Quality Metrics](architecture/real-world-corpus-quality-metrics.md)
- [Corpus Metrics Regression Diff](architecture/corpus-metrics-regression-diff.md)

Expand Down
9 changes: 7 additions & 2 deletions docs/architecture/remote-mcp-readiness.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ The same consent is available in the GitHub Action through `allow-network: "true

## Read-Only Scope

The probe uses a bounded HTTP request for MCP initialization and only follows the OAuth metadata-discovery path advertised by an unauthenticated challenge. It never sends credentials or tokens, and reporting redacts sensitive values, response bodies, session identifiers, and authorization metadata.
The probe uses bounded HTTP requests for MCP initialization and only follows the OAuth metadata-discovery path advertised by an unauthenticated challenge. It never sends credentials or tokens, and reporting redacts sensitive values, response bodies, session identifiers, SSE event identifiers, SSE retry values, and authorization metadata.

## Transport Reliability

The optional reliability scorecard runs only within the same `--runtime --allow-network` consent boundary; loopback endpoints still need `--allow-local-network`. It makes one bounded GET request after initialization. A `200` response must use `text/event-stream`; a `405` response is compliant when server-to-client SSE is unsupported. The probe can make at most one SSE resume and one session restart, and it retains no raw response body or SSE data. It is a bounded readiness check, not a claim of live remote interoperability, replay correctness, delivery guarantees, or load capacity.

`--allow-session-lifecycle` is false by default. It is state-changing and permits one bounded `DELETE` only after initialization supplies a valid `MCP-Session-Id`. `--require-remote-reliability` is a strict result gate: it fails unless every attempted remote reliability scorecard passes. Local-only runs are unaffected. The flag grants no network or loopback consent. See [Remote MCP Transport Reliability](remote-mcp-transport-reliability.md) for the protocol sequence and classifications.

## SSRF Controls

Expand All @@ -30,7 +36,6 @@ These checks reduce SSRF exposure but cannot account for every network topology.
- authenticated OAuth
- custom headers
- remote tool/resource/prompt/task calls
- GET SSE/resumability
- redirects

Use a dedicated MCP client with its own authorization and network policy when any of these capabilities are required.
212 changes: 212 additions & 0 deletions docs/architecture/remote-mcp-transport-reliability.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
# Remote MCP Transport Reliability

## Purpose

Codex Plugin Doctor verifies whether an explicitly approved remote MCP endpoint
behaves safely and predictably across Streamable HTTP session and SSE lifecycle
boundaries. The probe is a bounded readiness check, not a general-purpose MCP
client or load test.

## Goals

- validate protocol-compliant GET behavior after initialization
- validate session propagation without exposing session identifiers
- observe bounded SSE event framing and resumability when evidence is available
- classify disconnects, timeouts, retry delays, and one session restart without
retaining remote response content
- optionally terminate an initialized session after explicit user consent
- expose a stable, additive reliability scorecard and CI gate

## Non-Goals

- authenticated OAuth requests
- tool, resource, prompt, or task execution
- arbitrary custom request headers
- unbounded SSE subscriptions
- automatic retries beyond one evidence-driven resume attempt or one
protocol-required session restart
- performance, availability, or load testing

## Consent Model

Remote probing continues to require `--runtime --allow-network`. Loopback
endpoints additionally require `--allow-local-network`.

The default reliability probe does not send a session termination request.
`--allow-session-lifecycle` explicitly permits one bounded `DELETE` request
after the probe completes and only when initialization returned a valid
`MCP-Session-Id`.

`--require-remote-reliability` is a result gate, not network consent, and
requires `--runtime --allow-network`. It fails unless every attempted remote
reliability scorecard passes. Local-only runs are unaffected.

## Probe Sequence

1. Perform the existing bounded `initialize` request.
2. Validate `MCP-Session-Id` as visible ASCII when present.
3. Send `notifications/initialized` with the negotiated protocol and session
headers.
4. Send one bounded GET request with `Accept: text/event-stream`, the negotiated
protocol version, and the session identifier when present.
5. Accept HTTP 405 as a protocol-compliant declaration that the endpoint does
not offer a server-to-client SSE stream.
6. For HTTP 200, require `text/event-stream` and inspect only enough bytes to
identify a complete SSE event or reach the configured bounds.
7. If a valid SSE `id` field is observed, make one bounded reconnect attempt
using `Last-Event-ID`. Honor a valid server-provided SSE `retry` delay within
the remaining probe deadline. Do not reconnect when no event identifier is
observed.
8. If a GET carrying an issued session identifier returns HTTP 404, discard
that identifier and make one bounded re-initialization attempt without it.
Never restart a session more than once.
9. If lifecycle consent is enabled and a current session exists, send one bounded
`DELETE` request with the protocol and session headers.

No step follows redirects. Every request reuses the existing DNS resolution,
peer matching, response-size, encoding, and timeout controls.
The complete probe has a fixed request-count ceiling; reconnect and session
restart paths cannot recurse.

## HTTP Classification

### GET

- `200` with `text/event-stream`: supported; continue bounded SSE inspection
- `405`: compliant but unsupported; no resumability attempt
- any other status: fail
- `200` with another media type: fail

An SSE connection that produces no complete event before the observation
deadline is inconclusive, not proof of malformed framing. A malformed complete
event, oversized response, unsafe peer change, or transport error fails the
relevant capability.

A valid SSE `retry` field is respected before reconnecting. If its delay exceeds
the remaining bounded probe deadline, resumability is inconclusive and no late
request is sent. Invalid retry fields are ignored according to SSE parsing
rules.

### Expired Session

HTTP 404 from a GET carrying an issued session identifier means the session
expired or was terminated. The probe discards the stale identifier and performs
one fresh initialize request without a session header. A successful restart
continues with the new session; a second session-expiry response fails restart
reliability. A DELETE 404 never restarts or retries and is a termination
failure.

### DELETE

- any `2xx`: termination passed
- `405`: compliant but unsupported
- any other status or transport failure: termination failed

DELETE classification is `skipped` when lifecycle consent is absent or no
session identifier was issued.

## Resumability

The probe attempts resumability only when the first bounded GET produces a
complete event with a non-empty valid `id` field. The exact event identifier is
held in memory only for the duration of the reconnect request and is sent as
`Last-Event-ID`.

A reconnect confirms request propagation and bounded transport behavior. It
does not claim delivery guarantees or replay correctness because the probe does
not retain or compare application payloads.

## Scorecard

The existing remote runtime scorecard gains an additive `reliability` object:

```json
{
"getSse": "pass",
"sessionPropagation": "pass",
"resumability": "skipped",
"disconnectSafety": "pass",
"sessionRestart": "skipped",
"termination": "skipped",
"overall": "pass"
}
```

Capability fields use the existing `pass`, `warn`, `fail`, and `skipped`
statuses. `overall` is:

- `fail` when any attempted reliability check fails
- `warn` when no check fails but an attempted check is inconclusive
- `pass` when all applicable checks are compliant
- `skipped` when reliability probing does not run

Protocol-compliant unsupported behavior, such as GET or DELETE returning 405,
does not lower an otherwise passing score.

## Data Handling

The following values must never appear in text, Markdown, JSON, evidence,
transcript, error, or debug output:

- raw `MCP-Session-Id` values
- raw `Last-Event-ID` values
- raw SSE `retry` values
- SSE event data
- remote response bodies
- authorization metadata beyond existing redacted readiness classifications

Reports expose only classifications and stable finding identifiers. The probe
does not write remote values to disk.

## HTTP Client Changes

The bounded HTTP client may add only the capabilities required by this design:

- allow the `Last-Event-ID` request header
- preserve safe status and response-header metadata on a timeout after response
headers arrive
- keep existing three-second and one-megabyte upper bounds

It must not add redirects, cookies, authorization headers, connection pooling,
or configurable unsafe headers.

## CLI And GitHub Action

The `check`, release-check, release-evidence, and GitHub Action paths that
already expose remote runtime probing receive consistent inputs:

- `--allow-session-lifecycle`
- `--require-remote-reliability`
- `allow-session-lifecycle`
- `require-remote-reliability`

Help output and public documentation must state that lifecycle consent can
change remote server state. Existing commands remain unchanged when both new
flags are absent.

## Findings

New findings use the `plugin.runtime.remote.reliability.*` namespace and must
provide actionable remediation without including remote values. At minimum,
tests cover invalid GET status, invalid SSE media type, malformed event framing,
resume failure, and termination failure.

## Verification

The implementation requires deterministic local HTTP fixtures for:

- GET returning 405
- valid SSE with no event identifier
- valid SSE with an event identifier and one resume request
- valid SSE retry delay and an over-deadline retry delay
- invalid SSE content type and malformed framing
- session propagation on initialized, GET, resume, and DELETE requests
- session expiry followed by one successful or failed re-initialization
- timeout and disconnect behavior
- DELETE disabled, successful, unsupported, and failed states
- output redaction across text, Markdown, JSON, and release evidence
- CLI validation and GitHub Action input forwarding
- `--require-remote-reliability` pass, warn, fail, and skipped outcomes

Completion requires focused tests, the full test suite, build, dependency audit,
package verification, and a clean consumer installation.
Loading