Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
1e899d0
feat: validate remote MCP transport URLs
Esquetta Jul 25, 2026
b5bc7d9
fix: reject empty remote URL query and fragment
Esquetta Jul 25, 2026
5cf8864
fix: align insecure HTTP rule severity
Esquetta Jul 25, 2026
28c2c7d
fix: align remote HTTP severity surfaces
Esquetta Jul 25, 2026
0436a46
fix: report resolved MCP config evidence
Esquetta Jul 25, 2026
8de68b1
docs: document remote MCP validation rules
Esquetta Jul 25, 2026
4ba87c3
docs: document standalone MCP transport conflict
Esquetta Jul 25, 2026
24862b2
fix: deduplicate remote MCP security findings
Esquetta Jul 25, 2026
3888c18
fix: preserve all-interfaces MCP finding
Esquetta Jul 25, 2026
2f345de
feat: add bounded remote MCP HTTP client
Esquetta Jul 25, 2026
5ff6633
fix: harden bounded remote HTTP transport
Esquetta Jul 25, 2026
16b1897
test: close remote transport boundary gaps
Esquetta Jul 25, 2026
35f01b8
feat: probe remote MCP initialization safely
Esquetta Jul 25, 2026
fff067e
fix: contain remote SSE stop failures
Esquetta Jul 25, 2026
7c7beb3
fix: handle MCP SSE primer events
Esquetta Jul 25, 2026
2f29a7a
feat: validate remote MCP OAuth readiness
Esquetta Jul 25, 2026
20c2ee3
fix: bound remote OAuth discovery
Esquetta Jul 25, 2026
32b0157
fix: parse OAuth challenges safely
Esquetta Jul 25, 2026
a049415
fix: classify invalid OAuth metadata
Esquetta Jul 25, 2026
4a1fceb
fix: follow MCP OAuth discovery order
Esquetta Jul 25, 2026
1c3b5e4
feat: expose remote MCP readiness controls
Esquetta Jul 25, 2026
4c434a9
fix: enforce remote runtime approvals
Esquetta Jul 25, 2026
f93cbf9
docs: publish remote MCP readiness workflow
Esquetta Jul 25, 2026
4abcbd6
fix: preserve release evidence approval reports
Esquetta Jul 25, 2026
5c77994
fix: remediate development dependency audit
Esquetta Jul 25, 2026
b337b92
fix: clarify loopback network approval boundary
Esquetta Jul 25, 2026
ceb431c
fix: align remote network security documentation
Esquetta Jul 25, 2026
eeb9bdf
chore: prepare v1.52.0 release
Esquetta Jul 25, 2026
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
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,27 @@ 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.52.0] - 2026-07-25

### Added

- added remote Streamable HTTP initialize readiness with explicit `--allow-network` and loopback-only `--allow-local-network` consent
- added GitHub Action inputs and documentation for remote runtime consent controls

### Changed

- extended runtime plans, policy, reports, scorecards, and output contracts with remote MCP readiness results
- limited OAuth handling to bounded metadata discovery without authentication or redirect following

### Fixed

- preserved release-evidence compatibility when runtime readiness data is present

### Security

- added SSRF-safe bounded client and peer/DNS checks for remote MCP readiness probes
- remediated development-only `postcss` and `nanoid` audit findings

## [1.51.0] - 2026-07-24

### Added
Expand Down
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,17 @@ Runtime MCP validation with `--runtime`:
- optional runtime approval gating with a precomputed `doctor runtime-plan` digest
- optional Docker isolation for local Node.js stdio servers with `--sandbox docker`

### Remote MCP Readiness

Remote MCP runtime probing is disabled until you explicitly pass `--allow-network`. Local 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).

Output formats:

- human text output
Expand Down Expand Up @@ -427,9 +438,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: Esquetta/CodexPluginDoctor@v1.51.0
- uses: Esquetta/CodexPluginDoctor@v1.52.0
with:
version: "1.51.0"
version: "1.52.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 @@ -14,6 +14,14 @@ inputs:
description: Run optional MCP runtime probing.
required: false
default: "false"
allow-network:
description: Explicitly allow remote MCP runtime probes to make outbound network requests.
required: false
default: "false"
allow-local-network:
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"
installed:
description: Validate plugins from the local Codex plugin cache.
required: false
Expand Down Expand Up @@ -160,6 +168,8 @@ runs:
id: run-doctor
shell: bash
env:
ALLOW_NETWORK_INPUT: ${{ inputs['allow-network'] }}
ALLOW_LOCAL_NETWORK_INPUT: ${{ inputs['allow-local-network'] }}
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 @@ -199,6 +209,14 @@ runs:
args+=(--runtime)
fi

if [[ "$ALLOW_NETWORK_INPUT" == "true" ]]; then
args+=(--allow-network)
fi

if [[ "$ALLOW_LOCAL_NETWORK_INPUT" == "true" ]]; then
args+=(--allow-local-network)
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 @@ -9,6 +9,7 @@ This directory contains public documentation for users, contributors, and securi
- [Suppression Management](architecture/suppression-management.md)
- [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)
- [Real-World Corpus Quality Metrics](architecture/real-world-corpus-quality-metrics.md)
- [Corpus Metrics Regression Diff](architecture/corpus-metrics-regression-diff.md)

Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/mcp-2025-11-conformance.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ This feature adds version-aware, read-only conformance checks to the existing ru
- accepting or rejecting elicitation requests
- handling URL-mode elicitation in a browser
- servicing `sampling/createMessage` requests
- validating remote HTTP authorization or OAuth discovery
- performing remote OAuth metadata discovery as part of conformance; remote runtime readiness performs metadata discovery only
- adding a new top-level CLI command
- requiring older servers to implement capabilities introduced after their negotiated version
- building a generic external rule-pack engine
Expand Down
36 changes: 36 additions & 0 deletions docs/architecture/remote-mcp-readiness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Remote MCP Readiness

## Purpose

Codex Plugin Doctor can make bounded, read-only checks against a remote MCP endpoint. This is an opt-in readiness check, not a general remote MCP client.

## Explicit Consent

Remote requests require `--runtime --allow-network`. `--allow-local-network` is a second opt-in for loopback endpoints only (`localhost`, `127.0.0.0/8`, or `::1`). Private, link-local, multicast, unspecified, reserved, and NAT64 ranges remain blocked.

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

The same consent is available in the GitHub Action through `allow-network: "true"` and, only when needed, `allow-local-network: "true"`. Keep both inputs false for ordinary static validation.

## 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.

## SSRF Controls

Before connecting, the CLI requires an absolute HTTP or HTTPS URL without credentials, query strings, fragments, or numeric IP literals. It resolves hostnames and rejects non-public targets. The local-network exception permits loopback only; private, link-local, multicast, unspecified, reserved, cloud-metadata, and NAT64 destinations remain blocked. Requests have fixed size and time limits and do not follow redirects.

These checks reduce SSRF exposure but cannot account for every network topology. In particular, arbitrary network-specific NAT64 Pref64 mappings can change an address's effective route. Apply runner or host egress controls as the final boundary.

## Out Of Scope

- 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.
59 changes: 37 additions & 22 deletions docs/guides/github-action.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,21 @@ Use the Codex Plugin Doctor GitHub Action when a plugin repository should fail p

The action installs `codex-plugin-doctor` from npm, then runs the same CLI used locally.

## Remote MCP Runtime Probing

Remote MCP checks are off by default. Set `runtime: "true"` and give explicit network consent only for endpoints you trust. Use `allow-local-network: "true"` for loopback endpoints only (`localhost`, `127.0.0.0/8`, or `::1`). Private, link-local, multicast, unspecified, reserved, and NAT64 ranges remain blocked.

```yaml
- uses: ./
with:
path: .
runtime: "true"
allow-network: "true"
allow-local-network: "true" # Remove for public endpoints.
```

The Action transfers these boolean inputs through environment-backed shell variables and a Bash argument array. Remote probes remain read-only and redact diagnostics; see [Remote MCP Readiness](../architecture/remote-mcp-readiness.md) for SSRF and OAuth metadata-discovery boundaries.

## Recommended Workflow

```yaml
Expand All @@ -22,9 +37,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: Esquetta/CodexPluginDoctor@v1.51.0
- uses: Esquetta/CodexPluginDoctor@v1.52.0
with:
version: "1.51.0"
version: "1.52.0"
path: .
runtime: "true"
policy: codex-publish
Expand All @@ -51,9 +66,9 @@ Every action run also writes `codex-plugin-doctor-action-manifest.json`. The man
Use SARIF when repository security tooling should ingest validation findings.

```yaml
- uses: Esquetta/CodexPluginDoctor@v1.51.0
- uses: Esquetta/CodexPluginDoctor@v1.52.0
with:
version: "1.51.0"
version: "1.52.0"
path: .
sarif: "true"
```
Expand All @@ -65,9 +80,9 @@ The action writes `codex-plugin-doctor.sarif` into `output-dir`. Uploading it to
Use artifact and summary controls when the workflow needs custom retention or wants to disable generated report uploads.

```yaml
- uses: Esquetta/CodexPluginDoctor@v1.51.0
- uses: Esquetta/CodexPluginDoctor@v1.52.0
with:
version: "1.51.0"
version: "1.52.0"
path: .
output-dir: doctor-ci-reports
artifact-name: codex-plugin-doctor-reports
Expand Down Expand Up @@ -102,11 +117,11 @@ The action also exposes these workflow outputs for follow-up steps:
Use review bundle artifacts when a pull request or release workflow should preserve signed runtime approval, runtime policy, attestation, and release evidence handoff files.

```yaml
- uses: Esquetta/CodexPluginDoctor@v1.51.0
- uses: Esquetta/CodexPluginDoctor@v1.52.0
env:
CODEX_PLUGIN_DOCTOR_SIGNING_KEY: ${{ secrets.CODEX_PLUGIN_DOCTOR_SIGNING_KEY }}
with:
version: "1.51.0"
version: "1.52.0"
path: .
review-bundle: "true"
review-bundle-verify: "true"
Expand Down Expand Up @@ -137,19 +152,19 @@ The CLI can produce badge output for release notes, README automation, or a stat
Use a private corpus metrics manifest to measure reviewed precision, recall, and false-positive share in CI. The action writes only the public-safe metrics report into its artifact directory; snapshots, manifest contents, local paths, and review notes are not copied.

```yaml
- uses: Esquetta/CodexPluginDoctor@v1.51.0
- uses: Esquetta/CodexPluginDoctor@v1.52.0
with:
version: "1.51.0"
version: "1.52.0"
path: .
corpus-metrics-manifest: ../private-corpus/metrics.json
```

This writes `corpus-metrics.json`. To compare the result with a retained report and fail the job on regression:

```yaml
- uses: Esquetta/CodexPluginDoctor@v1.51.0
- uses: Esquetta/CodexPluginDoctor@v1.52.0
with:
version: "1.51.0"
version: "1.52.0"
path: .
corpus-metrics-manifest: ../private-corpus/metrics.json
corpus-metrics-baseline: .doctor-baselines/corpus-metrics.json
Expand Down Expand Up @@ -178,9 +193,9 @@ The history file is newline-delimited JSON. Store it as an artifact, cache, or r
The composite action can also append history directly:

```yaml
- uses: Esquetta/CodexPluginDoctor@v1.51.0
- uses: Esquetta/CodexPluginDoctor@v1.52.0
with:
version: "1.51.0"
version: "1.52.0"
path: .
runtime: "true"
history: validation-history.jsonl
Expand All @@ -200,9 +215,9 @@ Use profiles when a consuming workflow needs a named validation policy instead o
The composite action can pass profiles directly:

```yaml
- uses: Esquetta/CodexPluginDoctor@v1.51.0
- uses: Esquetta/CodexPluginDoctor@v1.52.0
with:
version: "1.51.0"
version: "1.52.0"
path: .
profile: publish
```
Expand All @@ -212,9 +227,9 @@ The composite action can pass profiles directly:
Use policy presets when a workflow should apply one of the opinionated release gates without adding a local `.codex-doctor.json`.

```yaml
- uses: Esquetta/CodexPluginDoctor@v1.51.0
- uses: Esquetta/CodexPluginDoctor@v1.52.0
with:
version: "1.51.0"
version: "1.52.0"
path: .
policy: codex-publish
```
Expand All @@ -226,9 +241,9 @@ Supported policy values are `codex-publish`, `mcp-strict`, and `security`. The C
Use installed-cache mode only in environments where Codex plugins are already available on the runner.

```yaml
- uses: Esquetta/CodexPluginDoctor@v1.51.0
- uses: Esquetta/CodexPluginDoctor@v1.52.0
with:
version: "1.51.0"
version: "1.52.0"
installed: "true"
filter: github
runtime: "false"
Expand All @@ -239,9 +254,9 @@ Use installed-cache mode only in environments where Codex plugins are already av
Pin both the action ref and npm package version for reproducible CI:

```yaml
- uses: Esquetta/CodexPluginDoctor@v1.51.0
- uses: Esquetta/CodexPluginDoctor@v1.52.0
with:
version: "1.51.0"
version: "1.52.0"
```

Use `version: "latest"` only when the consuming repository intentionally wants automatic CLI upgrades.
10 changes: 9 additions & 1 deletion docs/rules/catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ codex-plugin-doctor explain plugin.manifest.missing
| `plugin.mcp.invalid_shape` | fail | MCP config does not contain a valid `mcpServers` object. |
| `plugin.mcp.server.invalid` | fail | MCP server entry is not an object. |
| `plugin.mcp.server.transport.missing` | fail | MCP server entry is missing both `command` and `url`. |
| `mcp.server.transport.conflict` | fail | An MCP server defines both command and URL transports. |
| `plugin.mcp.server.transport.conflict` | fail | A bundled MCP server defines both command and URL transports. |

## Security Rules

Expand All @@ -52,7 +54,13 @@ codex-plugin-doctor explain plugin.manifest.missing
| `plugin.security.path_traversal_risk` | fail | MCP server passes a package-external path to a path-like runtime argument. |
| `plugin.security.dangerous_env_usage` | fail | MCP server sets an environment variable that can alter code loading. |
| `plugin.security.cwd_outside_root` | fail | MCP server `cwd` resolves outside the plugin package root. |
| `plugin.security.insecure_http_url` | warn | MCP server uses a plain HTTP URL. |
| `plugin.security.insecure_http_url` | fail | MCP server uses a plain HTTP URL. |
| `plugin.security.remote_mcp_url.invalid` | fail | An MCP server URL is not an absolute HTTP or HTTPS URL. |
| `plugin.security.remote_mcp_url.unsupported_scheme` | fail | An MCP server URL uses an unsupported scheme. |
| `plugin.security.remote_mcp_url.credentials` | fail | An MCP server URL embeds credentials. |
| `plugin.security.remote_mcp_url.query` | fail | An MCP server URL contains a query string. |
| `plugin.security.remote_mcp_url.fragment` | fail | An MCP server URL contains a fragment. |
| `plugin.security.remote_mcp_url.ip_literal` | fail | An MCP server URL uses a numeric IP literal. |
| `plugin.security.prompt_injection_text` | fail | Packaged text contains prompt-injection or secret-exfiltration instructions. |

## Runtime Rules
Expand Down
6 changes: 6 additions & 0 deletions docs/security/security-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ Run structural and config checks before any runtime command execution.

This is an approval gate, not a sandbox. It reduces accidental or unreviewed execution, but it does not isolate the process after launch.

### Remote MCP Network Boundary

Remote probing requires explicit network consent and separately requires `--allow-local-network` consent for loopback endpoints only. Private, link-local, multicast, unspecified, reserved, and NAT64 ranges remain blocked. Before each request, the CLI validates the URL and resolved addresses to block credentials, query and fragment components, numeric IP literals, loopback and private ranges, link-local and cloud-metadata ranges, and other SSRF targets. Requests are bounded, redirect-free, and redacted in reports.

DNS and IP classification cannot eliminate arbitrary network-specific NAT64 Pref64 mappings. Use runner or host egress controls to limit the destinations that a CI job or workstation can reach.

### Secret Hygiene

- redact values that look like tokens in reports
Expand Down
18 changes: 9 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading