codex-plugin-doctor 1.52.0 → 1.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +33 -17
  3. package/dist/core/bounded-http-client.js +2 -1
  4. package/dist/core/doctor-export-bundle.js +3 -1
  5. package/dist/core/mcp-registry.d.ts +57 -0
  6. package/dist/core/mcp-registry.js +455 -0
  7. package/dist/core/output-contract.js +73 -0
  8. package/dist/core/release-check.d.ts +2 -0
  9. package/dist/core/release-check.js +2 -0
  10. package/dist/core/release-evidence.d.ts +2 -0
  11. package/dist/core/release-evidence.js +2 -0
  12. package/dist/core/remote-mcp-probe.d.ts +1 -0
  13. package/dist/core/remote-mcp-probe.js +59 -1
  14. package/dist/core/remote-transport-reliability.d.ts +18 -0
  15. package/dist/core/remote-transport-reliability.js +243 -0
  16. package/dist/core/runtime-probe.d.ts +3 -1
  17. package/dist/core/runtime-probe.js +18 -1
  18. package/dist/core/sse-observation.d.ts +7 -0
  19. package/dist/core/sse-observation.js +67 -0
  20. package/dist/core/validate-plugin.js +4 -3
  21. package/dist/domain/types.d.ts +13 -0
  22. package/dist/index.d.ts +1 -0
  23. package/dist/index.js +1 -0
  24. package/dist/mcp/generic-mcp-doctor.d.ts +2 -0
  25. package/dist/mcp/generic-mcp-doctor.js +6 -3
  26. package/dist/reporting/render-markdown-report.js +13 -0
  27. package/dist/reporting/render-text-report.js +11 -0
  28. package/dist/rules/rule-catalog.js +108 -0
  29. package/dist/run-cli.js +143 -7
  30. package/examples/README.md +94 -94
  31. package/examples/codex-doctor-generic-mcp/.mcp.json +10 -10
  32. package/examples/codex-doctor-generic-mcp/server.js +1 -1
  33. package/examples/codex-doctor-risky/.codex-plugin/plugin.json +7 -7
  34. package/examples/codex-doctor-risky/.mcp.json +12 -12
  35. package/examples/codex-doctor-risky/mock-server.js +1 -1
  36. package/examples/codex-doctor-runtime/.codex-plugin/plugin.json +8 -8
  37. package/examples/codex-doctor-runtime/.mcp.json +9 -9
  38. package/examples/codex-doctor-runtime/mock-server.js +237 -237
  39. package/examples/codex-doctor-runtime/skills/context-check/SKILL.md +7 -7
  40. package/examples/codex-doctor-starter/.codex-plugin/plugin.json +7 -7
  41. package/examples/codex-doctor-starter/skills/repo-scan/SKILL.md +7 -7
  42. package/package.json +1 -1
@@ -43,6 +43,8 @@ export interface CheckOptions {
43
43
  runtime?: boolean;
44
44
  allowNetwork?: boolean;
45
45
  allowLocalNetwork?: boolean;
46
+ allowSessionLifecycle?: boolean;
47
+ requireRemoteReliability?: boolean;
46
48
  runtimeTranscript?: (line: string) => void;
47
49
  runtimeStartupTimeoutMs?: number;
48
50
  runtimeSandbox?: RuntimeSandboxMode;
@@ -103,8 +105,19 @@ export interface RemoteRuntimeScorecard {
103
105
  session: "absent" | "present-valid" | "present-invalid";
104
106
  protocolHeaders: RuntimeCapabilityStatus;
105
107
  authorization: RuntimeCapabilityStatus;
108
+ reliability?: RemoteTransportReliabilityScorecard;
106
109
  overall: "pass" | "warn" | "fail" | "skipped";
107
110
  }
111
+ export type RemoteTransportReliabilityStatus = "pass" | "warn" | "fail" | "skipped";
112
+ export interface RemoteTransportReliabilityScorecard {
113
+ getSse: RemoteTransportReliabilityStatus;
114
+ sessionPropagation: RemoteTransportReliabilityStatus;
115
+ resumability: RemoteTransportReliabilityStatus;
116
+ disconnectSafety: RemoteTransportReliabilityStatus;
117
+ sessionRestart: RemoteTransportReliabilityStatus;
118
+ termination: RemoteTransportReliabilityStatus;
119
+ overall: RemoteTransportReliabilityStatus;
120
+ }
108
121
  export type McpConformanceProfile = "legacy" | "2025-11-25" | "future-compatible";
109
122
  export interface RuntimeConformanceScorecard {
110
123
  protocolVersion: string | null;
package/dist/index.d.ts CHANGED
@@ -30,6 +30,7 @@ export { buildDoctorInspectorReport, renderDoctorInspectorReport, renderDoctorIn
30
30
  export { buildEcosystemAudit, renderEcosystemAudit, renderEcosystemAuditJson, type EcosystemAuditReport } from "./audit/ecosystem-audit.js";
31
31
  export { applyPolicyToDepAudit, applyPolicyToDoctorConfig, applyPolicyToSecurityAudit, parsePolicyPack, policyEnablesRuntime, policyFailsOnWarnings, policyPackNames, type PolicyPackName } from "./policy/policy-packs.js";
32
32
  export { buildGenericMcpDoctor, renderGenericMcpDoctor, renderGenericMcpDoctorJson, type GenericMcpDoctorReport } from "./mcp/generic-mcp-doctor.js";
33
+ export { buildMcpRegistryReadiness, inspectMcpRegistryServer, registryReadinessExitCode, renderMcpRegistryReadiness, renderMcpRegistryReadinessJson, type InspectMcpRegistryOptions, type McpRegistryFinding, type McpRegistryInstallability, type McpRegistryReadinessReport, type McpRegistryScorecard } from "./core/mcp-registry.js";
33
34
  export { watchPlugin, type WatchPluginOptions, type WatchPluginResult } from "./core/watch-plugin.js";
34
35
  export { buildDepAudit, buildDepAuditRecommendations, renderDepAudit, renderDepAuditJson, renderDepAuditSarif, type DepAuditRecommendation, type DepAuditReport, type DepAuditVulnerability } from "./core/dep-audit.js";
35
36
  export { initGitHooks, removeGitHooks, type InitGitHooksResult, type RemoveGitHooksResult } from "./core/init-git-hooks.js";
package/dist/index.js CHANGED
@@ -30,6 +30,7 @@ export { buildDoctorInspectorReport, renderDoctorInspectorReport, renderDoctorIn
30
30
  export { buildEcosystemAudit, renderEcosystemAudit, renderEcosystemAuditJson } from "./audit/ecosystem-audit.js";
31
31
  export { applyPolicyToDepAudit, applyPolicyToDoctorConfig, applyPolicyToSecurityAudit, parsePolicyPack, policyEnablesRuntime, policyFailsOnWarnings, policyPackNames } from "./policy/policy-packs.js";
32
32
  export { buildGenericMcpDoctor, renderGenericMcpDoctor, renderGenericMcpDoctorJson } from "./mcp/generic-mcp-doctor.js";
33
+ export { buildMcpRegistryReadiness, inspectMcpRegistryServer, registryReadinessExitCode, renderMcpRegistryReadiness, renderMcpRegistryReadinessJson } from "./core/mcp-registry.js";
33
34
  export { watchPlugin } from "./core/watch-plugin.js";
34
35
  export { buildDepAudit, buildDepAuditRecommendations, renderDepAudit, renderDepAuditJson, renderDepAuditSarif } from "./core/dep-audit.js";
35
36
  export { initGitHooks, removeGitHooks } from "./core/init-git-hooks.js";
@@ -17,6 +17,8 @@ export interface GenericMcpDoctorOptions {
17
17
  runtime?: boolean;
18
18
  allowNetwork?: boolean;
19
19
  allowLocalNetwork?: boolean;
20
+ allowSessionLifecycle?: boolean;
21
+ requireRemoteReliability?: boolean;
20
22
  runtimeStartupTimeoutMs?: number;
21
23
  }
22
24
  export declare function buildGenericMcpDoctor(targetPath: string, environment?: CompatibilityEnvironment, options?: GenericMcpDoctorOptions): Promise<GenericMcpDoctorReport>;
@@ -2,7 +2,7 @@ import { realpath, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { buildCompatibilityMatrix, readMcpConfigPath } from "../compatibility/compatibility-matrix.js";
4
4
  import { readJsonFile } from "../core/read-json-file.js";
5
- import { probeRuntimeConfig } from "../core/runtime-probe.js";
5
+ import { probeRuntimeConfig, remoteReliabilityGatePassed } from "../core/runtime-probe.js";
6
6
  import { formatFindingFingerprintLine, withFindingFingerprints } from "../reporting/finding-fingerprint.js";
7
7
  import { auditMcpServerConfig, buildSecurityAuditFromFindings } from "../security/security-audit.js";
8
8
  function buildFinding(severity, id, message, impact, suggestedFix, evidence) {
@@ -142,11 +142,14 @@ export async function buildGenericMcpDoctor(targetPath, environment = {}, option
142
142
  ? await probeRuntimeConfig(canonicalRootPath, canonicalMcpConfigPath, {
143
143
  startupTimeoutMs: options.runtimeStartupTimeoutMs,
144
144
  allowNetwork: options.allowNetwork,
145
- allowLocalNetwork: options.allowLocalNetwork
145
+ allowLocalNetwork: options.allowLocalNetwork,
146
+ allowSessionLifecycle: options.allowSessionLifecycle
146
147
  })
147
148
  : null;
148
149
  const fingerprintedFindings = withFindingFingerprints([...staticFindings, ...(runtimeResult?.findings ?? [])], rootPath);
149
- const status = mergeReportStatus(fingerprintedFindings, security);
150
+ const status = options.requireRemoteReliability === true && !remoteReliabilityGatePassed(runtimeResult?.scorecard)
151
+ ? "fail"
152
+ : mergeReportStatus(fingerprintedFindings, security);
150
153
  return {
151
154
  targetPath: rootPath,
152
155
  status,
@@ -42,6 +42,19 @@ function appendRuntimeScorecard(lines, result) {
42
42
  lines.push(`| Protocol headers | ${remote.protocolHeaders.toUpperCase()} |`);
43
43
  lines.push(`| Authorization | ${remote.authorization.toUpperCase()} |`);
44
44
  lines.push(`| Overall | ${remote.overall.toUpperCase()} |`);
45
+ if (remote.reliability) {
46
+ const reliability = remote.reliability;
47
+ lines.push("", "## Remote Transport Reliability", "");
48
+ lines.push("| Check | Status |");
49
+ lines.push("| --- | --- |");
50
+ lines.push(`| GET SSE | ${reliability.getSse.toUpperCase()} |`);
51
+ lines.push(`| Session propagation | ${reliability.sessionPropagation.toUpperCase()} |`);
52
+ lines.push(`| Resumability | ${reliability.resumability.toUpperCase()} |`);
53
+ lines.push(`| Disconnect safety | ${reliability.disconnectSafety.toUpperCase()} |`);
54
+ lines.push(`| Session restart | ${reliability.sessionRestart.toUpperCase()} |`);
55
+ lines.push(`| Termination | ${reliability.termination.toUpperCase()} |`);
56
+ lines.push(`| Overall | ${reliability.overall.toUpperCase()} |`);
57
+ }
45
58
  }
46
59
  }
47
60
  export function buildMarkdownReport(result, options) {
@@ -59,6 +59,17 @@ function appendRuntimeScorecard(lines, result) {
59
59
  lines.push(`protocol headers: ${remote.protocolHeaders}`);
60
60
  lines.push(`authorization: ${remote.authorization}`);
61
61
  lines.push(`overall: ${remote.overall}`);
62
+ if (remote.reliability) {
63
+ const reliability = remote.reliability;
64
+ lines.push("", "Remote Transport Reliability", "----------------------------");
65
+ lines.push(`GET SSE: ${reliability.getSse}`);
66
+ lines.push(`Session propagation: ${reliability.sessionPropagation}`);
67
+ lines.push(`Resumability: ${reliability.resumability}`);
68
+ lines.push(`Disconnect safety: ${reliability.disconnectSafety}`);
69
+ lines.push(`Session restart: ${reliability.sessionRestart}`);
70
+ lines.push(`Termination: ${reliability.termination}`);
71
+ lines.push(`Overall: ${reliability.overall}`);
72
+ }
62
73
  }
63
74
  }
64
75
  export function renderTextReport(result, options = {}) {
@@ -556,6 +556,114 @@ export const ruleCatalog = [
556
556
  why: "Authorization readiness cannot be confirmed without bounded metadata responses.",
557
557
  fix: "Make protected-resource and authorization-server metadata available over HTTPS.",
558
558
  example: "Return application/json discovery metadata."
559
+ },
560
+ {
561
+ id: "plugin.runtime.remote.reliability.get.status",
562
+ category: "runtime",
563
+ defaultSeverity: "fail",
564
+ summary: "The remote MCP endpoint rejected the SSE transport request.",
565
+ why: "Clients cannot rely on server-to-client streaming when an accepted SSE request does not return the expected status.",
566
+ fix: "Return HTTP 200 with a valid SSE response, or HTTP 405 when GET streaming is not supported.",
567
+ example: "HTTP/1.1 405 Method Not Allowed"
568
+ },
569
+ {
570
+ id: "plugin.runtime.remote.reliability.get.content_type",
571
+ category: "runtime",
572
+ defaultSeverity: "fail",
573
+ summary: "The remote MCP endpoint returned a non-SSE media type for GET streaming.",
574
+ why: "Clients cannot interpret a successful streaming response unless it is declared as Server-Sent Events.",
575
+ fix: "Return Content-Type: text/event-stream for accepted GET streaming requests.",
576
+ example: "Content-Type: text/event-stream"
577
+ },
578
+ {
579
+ id: "plugin.runtime.remote.reliability.get.inconclusive",
580
+ category: "runtime",
581
+ defaultSeverity: "warn",
582
+ summary: "The remote MCP GET streaming check was inconclusive.",
583
+ why: "A bounded probe could not observe a complete SSE event, so streaming reliability remains uncertain.",
584
+ fix: "Emit complete SSE event frames promptly when server-to-client streaming is supported.",
585
+ example: "id: event-1\n\ndata: {}\n\n"
586
+ },
587
+ {
588
+ id: "plugin.runtime.remote.reliability.get.failed",
589
+ category: "runtime",
590
+ defaultSeverity: "fail",
591
+ summary: "The remote MCP GET streaming request could not be completed.",
592
+ why: "Clients cannot rely on server-to-client streaming when a bounded transport request fails.",
593
+ fix: "Keep the Streamable HTTP transport reachable within the configured request bounds.",
594
+ example: "Complete the GET request before the runtime timeout."
595
+ },
596
+ {
597
+ id: "plugin.runtime.remote.reliability.get.malformed",
598
+ category: "runtime",
599
+ defaultSeverity: "fail",
600
+ summary: "The remote MCP endpoint emitted malformed SSE framing.",
601
+ why: "Malformed event framing prevents clients from safely processing stream progress or reconnecting.",
602
+ fix: "Return complete SSE events with valid id and retry fields.",
603
+ example: "id: event-1\n\nevent: message\n\ndata: {}\n\n"
604
+ },
605
+ {
606
+ id: "plugin.runtime.remote.reliability.resume.status",
607
+ category: "runtime",
608
+ defaultSeverity: "fail",
609
+ summary: "The remote MCP endpoint rejected the SSE resume request.",
610
+ why: "Clients cannot resume a dropped stream when the reconnect response has an unexpected status.",
611
+ fix: "Return HTTP 200 for an accepted Last-Event-ID reconnect request.",
612
+ example: "HTTP/1.1 200 OK"
613
+ },
614
+ {
615
+ id: "plugin.runtime.remote.reliability.resume.content_type",
616
+ category: "runtime",
617
+ defaultSeverity: "fail",
618
+ summary: "The remote MCP endpoint returned a non-SSE media type for a resume request.",
619
+ why: "Clients cannot process a resumed stream unless it is declared as Server-Sent Events.",
620
+ fix: "Return Content-Type: text/event-stream after accepting a Last-Event-ID reconnect request.",
621
+ example: "Content-Type: text/event-stream"
622
+ },
623
+ {
624
+ id: "plugin.runtime.remote.reliability.resume.inconclusive",
625
+ category: "runtime",
626
+ defaultSeverity: "warn",
627
+ summary: "The remote MCP SSE resume check was inconclusive.",
628
+ why: "A bounded probe could not confirm a complete resumed event, so reconnect reliability remains uncertain.",
629
+ fix: "Emit complete SSE event frames after accepting a reconnect.",
630
+ example: "Return one complete SSE event after a Last-Event-ID request."
631
+ },
632
+ {
633
+ id: "plugin.runtime.remote.reliability.resume.failed",
634
+ category: "runtime",
635
+ defaultSeverity: "fail",
636
+ summary: "The remote MCP SSE resume request could not be completed.",
637
+ why: "Clients cannot recover from a dropped stream when a bounded reconnect fails.",
638
+ fix: "Accept one bounded SSE reconnect using Last-Event-ID.",
639
+ example: "Honor the Last-Event-ID header on one reconnect."
640
+ },
641
+ {
642
+ id: "plugin.runtime.remote.reliability.resume.malformed",
643
+ category: "runtime",
644
+ defaultSeverity: "fail",
645
+ summary: "The remote MCP endpoint emitted malformed SSE framing after reconnecting.",
646
+ why: "Malformed resumed events prevent clients from safely continuing stream processing.",
647
+ fix: "Return complete SSE events with valid id and retry fields after reconnecting.",
648
+ example: "Return a complete event frame after reconnecting."
649
+ },
650
+ {
651
+ id: "plugin.runtime.remote.reliability.session_restart.failed",
652
+ category: "runtime",
653
+ defaultSeverity: "fail",
654
+ summary: "The remote MCP session could not be restarted.",
655
+ why: "Clients cannot recover from an expired MCP session when a fresh initialization sequence fails.",
656
+ fix: "Accept a fresh initialize sequence after an expired MCP session.",
657
+ example: "Return a valid initialize response after HTTP 404 for a session-bound request."
658
+ },
659
+ {
660
+ id: "plugin.runtime.remote.reliability.termination.failed",
661
+ category: "runtime",
662
+ defaultSeverity: "fail",
663
+ summary: "The remote MCP session could not be terminated.",
664
+ why: "A server that advertises session lifecycle support must safely handle the approved termination request.",
665
+ fix: "Return a successful response or HTTP 405 for a bounded MCP session DELETE request.",
666
+ example: "HTTP/1.1 204 No Content"
559
667
  }
560
668
  ];
561
669
  export function findRuleDefinition(id) {
package/dist/run-cli.js CHANGED
@@ -28,6 +28,7 @@ import { buildDoctorRuntimePolicyReport, renderDoctorRuntimePolicy, renderDoctor
28
28
  import { buildDoctorReviewBundle, diffDoctorReviewBundles, renderDoctorReviewBundle, renderDoctorReviewBundleDiff, renderDoctorReviewBundleDiffJson, renderDoctorReviewBundleJson, renderDoctorReviewBundleVerification, renderDoctorReviewBundleVerificationJson, verifyDoctorReviewBundle } from "./core/review-bundle.js";
29
29
  import { buildDoctorReleaseEvidenceAssetReport, buildDoctorReleaseEvidenceReport, renderDoctorReleaseEvidenceAsset, renderDoctorReleaseEvidenceAssetJson, renderDoctorReleaseEvidence, renderDoctorReleaseEvidenceJson, renderDoctorReleaseEvidenceVerification, renderDoctorReleaseEvidenceVerificationJson, verifyDoctorReleaseEvidence } from "./core/release-evidence.js";
30
30
  import { buildDoctorNpmPackageReport, renderDoctorNpmPackageReport, renderDoctorNpmPackageReportJson } from "./core/npm-package-doctor.js";
31
+ import { buildMcpRegistryReadiness, inspectMcpRegistryServer, registryReadinessExitCode, renderMcpRegistryReadiness, renderMcpRegistryReadinessJson } from "./core/mcp-registry.js";
31
32
  import { buildDoctorRiskDiffReport, renderDoctorRiskDiffReport, renderDoctorRiskDiffReportJson } from "./core/risk-diff.js";
32
33
  import { buildDoctorInspectorReport, renderDoctorInspectorReport, renderDoctorInspectorReportJson } from "./core/inspector-bridge.js";
33
34
  import { applyFixPlan, buildFixPlan, renderApplyFixResult, renderFixPlanJsonReport, renderFixPlan } from "./core/fix-plan.js";
@@ -102,6 +103,8 @@ function parseRuntimeSandbox(flags, options = { requireRuntime: true }) {
102
103
  function parseRemoteNetworkFlags(flags, runtime) {
103
104
  let allowNetwork = false;
104
105
  let allowLocalNetwork = false;
106
+ let allowSessionLifecycle = false;
107
+ let requireRemoteReliability = false;
105
108
  for (let index = 0; index < flags.length; index += 1) {
106
109
  const flag = flags[index];
107
110
  if (flag === "--allow-network") {
@@ -120,23 +123,57 @@ function parseRemoteNetworkFlags(flags, runtime) {
120
123
  }
121
124
  allowLocalNetwork = true;
122
125
  }
123
- else if (flag?.startsWith("--allow-network=") || flag?.startsWith("--allow-local-network=")) {
126
+ else if (flag === "--allow-session-lifecycle") {
127
+ if (allowSessionLifecycle)
128
+ return new CliUsageError("Duplicate runtime remote flag: --allow-session-lifecycle.");
129
+ if (flags[index + 1] && !flags[index + 1].startsWith("--")) {
130
+ return new CliUsageError("--allow-session-lifecycle does not accept a value.");
131
+ }
132
+ allowSessionLifecycle = true;
133
+ }
134
+ else if (flag === "--require-remote-reliability") {
135
+ if (requireRemoteReliability)
136
+ return new CliUsageError("Duplicate runtime remote flag: --require-remote-reliability.");
137
+ if (flags[index + 1] && !flags[index + 1].startsWith("--")) {
138
+ return new CliUsageError("--require-remote-reliability does not accept a value.");
139
+ }
140
+ requireRemoteReliability = true;
141
+ }
142
+ else if (flag?.startsWith("--allow-network=") ||
143
+ flag?.startsWith("--allow-local-network=") ||
144
+ flag?.startsWith("--allow-session-lifecycle=") ||
145
+ flag?.startsWith("--require-remote-reliability=")) {
124
146
  return new CliUsageError(`${flag.split("=", 1)[0]} does not accept a value.`);
125
147
  }
126
148
  }
149
+ if (allowSessionLifecycle && !runtime) {
150
+ return new CliUsageError("--allow-session-lifecycle requires --runtime.");
151
+ }
152
+ if (requireRemoteReliability && !runtime) {
153
+ return new CliUsageError("--require-remote-reliability requires --runtime and --allow-network.");
154
+ }
127
155
  if ((allowNetwork || allowLocalNetwork) && !runtime) {
128
156
  return new CliUsageError("--allow-network requires --runtime.");
129
157
  }
130
158
  if (allowLocalNetwork && !allowNetwork) {
131
159
  return new CliUsageError("--allow-local-network requires --allow-network.");
132
160
  }
133
- return { allowNetwork, allowLocalNetwork };
161
+ if (allowSessionLifecycle && !allowNetwork) {
162
+ return new CliUsageError("--allow-session-lifecycle requires --allow-network.");
163
+ }
164
+ if (requireRemoteReliability && !allowNetwork) {
165
+ return new CliUsageError("--require-remote-reliability requires --runtime and --allow-network.");
166
+ }
167
+ return { allowNetwork, allowLocalNetwork, allowSessionLifecycle, requireRemoteReliability };
134
168
  }
135
169
  function printUsage(io) {
136
170
  io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--policy codex-publish|mcp-strict|security] [--compat] [--json|--markdown|--badge-json|--badge-markdown] [--output <path>] [--history <path>] [--runtime [--allow-network [--allow-local-network]]] [--sandbox docker] [--require-runtime-approval --runtime-approval-digest <digest>] [--verbose-runtime] [--explain] [--no-animations] [--ascii] [--changed-since <ref>]\n codex-plugin-doctor audit --installed [filter] [--policy codex-publish|mcp-strict|security] [--security] [--compat] [--json] [--output <path>] [--cache] [--changed]\n codex-plugin-doctor audit deps <path> [--policy codex-publish|mcp-strict|security] [--recommend] [--json|--sarif] [--output <path>]\n codex-plugin-doctor mcp <path> [--runtime [--allow-network [--allow-local-network]]] [--json] [--output <path>]\n codex-plugin-doctor security <path> [--policy security] [--json|--scorecard]\n codex-plugin-doctor compat <path> [--all|--client <client>] [--json] [--scorecard] [--output <path>] [--install-preview|--apply --backup]\n codex-plugin-doctor suppress add <path> [--fingerprint <sha256> --reason <text> --expires-at YYYY-MM-DD] [--config <path>] [--json]\n codex-plugin-doctor suppress list <path> [--config <path>] [--json]\n codex-plugin-doctor suppress remove <path> [--fingerprint <sha256>|--index <n>] [--config <path>] [--json]\n codex-plugin-doctor fix <path> (--dry-run|--interactive --backup|--apply --backup)\n codex-plugin-doctor history <history.jsonl> [--json] [--fail-on-regression]\n codex-plugin-doctor watch <path> [--runtime] [--json] [--output <path>] [--debounce-ms <ms>] [--max-iterations <n>] [--fail-fast] [--accumulate-json <path>]\n codex-plugin-doctor doctor [npm <package>|contract|corpus [--manifest <corpus.json>] [--json] [--output <path>]|corpus metrics --manifest <corpus.json> [--json|--markdown] [--output <path>] [--min-precision <0..1>] [--min-recall <0..1>] [--max-false-positive-rate <0..1>]|runtime-plan <path> [--sandbox docker] [--json|--markdown] [--output <path>]|runtime-policy <path> [--sandbox docker] [--json] [--output <path>]|review-bundle <path> --output <dir> --sign-key-env NAME [--json] [--allow-dirty] [--allow-untagged]|review-bundle verify <bundle-dir> --target <path> --sign-key-env NAME [--json] [--output <path>] [--failures-only]|review-bundle diff --before <dir> --after <dir> [--json]|attest <path> [--sign-key-env NAME]|attest verify <attestation.json> --target <path> --sign-key-env NAME|release-evidence <path> --sign-key-env NAME [--runtime [--allow-network [--allow-local-network]]] [--sandbox docker] [--allow-dirty] [--allow-untagged] [--require-runtime-approval --runtime-approval-digest <digest>]|release-evidence verify <evidence.json> --target <path> --sign-key-env NAME|release-evidence asset <path> --tag <tag> --output <evidence.json> --sign-key-env NAME [--runtime [--allow-network [--allow-local-network]]] [--sandbox docker] [--allow-dirty] [--allow-untagged] [--require-runtime-approval --runtime-approval-digest <digest>] [--upload]|mcp <path> [--runtime [--allow-network [--allow-local-network]]]|inspector <path>|diff --before <path> --after <path>|recommend <path>|trust <path>|perf <path> [--max-total-ms <ms>] [--max-stage-ms stage=ms]|export --bundle <path>|snapshot|clients|--json|--update-check]\n codex-plugin-doctor init [path] [--template skill-only|mcp-stdio|mcp-http|full-runtime]\n codex-plugin-doctor init-ci [path]\n codex-plugin-doctor init-git-hooks [path] [--force] [--json]\n codex-plugin-doctor init-git-hooks [path] --remove [--json]\n codex-plugin-doctor completion bash|zsh|fish\n codex-plugin-doctor config validate <path> [--json]\n codex-plugin-doctor release check <path> [--json] [--runtime [--allow-network [--allow-local-network]]] [--sandbox docker]\n codex-plugin-doctor self-test\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version\n\nFirst run:\n codex-plugin-doctor doctor\n codex-plugin-doctor self-test\n codex-plugin-doctor init my-plugin\n codex-plugin-doctor check . --runtime --explain");
171
+ io.writeStderr("Registry readiness: codex-plugin-doctor registry check <server.json|directory> [--json] [--output <path>] [--require-registry-readiness]\n"
172
+ + " codex-plugin-doctor registry inspect <server-name> --allow-network [--json] [--output <path>] [--require-registry-readiness]");
137
173
  io.writeStderr("Corpus quality regression: codex-plugin-doctor doctor corpus metrics diff --before <metrics.json> --after <metrics.json> [--fail-on-regression] [--json|--markdown] [--output <path>]");
138
174
  io.writeStderr("Suppression governance: codex-plugin-doctor suppress list <path> [--fail-on-expired] [--fail-on-invalid] [--warn-expiring-within-days <days>]\n codex-plugin-doctor suppress prune <path> [--apply] [--json]");
139
175
  io.writeStderr("Baseline gating: codex-plugin-doctor baseline create <path> --output <path> [--runtime]\n codex-plugin-doctor check <path> --baseline <path>");
176
+ io.writeStderr("Remote MCP runtime flags (check, mcp, release check, and doctor release-evidence): --runtime --allow-network [--allow-local-network] [--allow-session-lifecycle] [--require-remote-reliability]\n --allow-session-lifecycle requires --runtime --allow-network and can terminate a remote session.\n --require-remote-reliability requires --runtime --allow-network, blocks non-pass reliability, and does not grant network consent.");
140
177
  }
141
178
  const suppressUsageText = [
142
179
  "Usage:",
@@ -687,6 +724,8 @@ function buildGenericMcpDoctorCommandArgs(commandTarget, flags) {
687
724
  let runtime = false;
688
725
  let allowNetwork = false;
689
726
  let allowLocalNetwork = false;
727
+ let allowSessionLifecycle = false;
728
+ let requireRemoteReliability = false;
690
729
  for (let index = 0; index < flags.length; index += 1) {
691
730
  const flag = flags[index];
692
731
  if (flag === "--runtime") {
@@ -715,6 +754,26 @@ function buildGenericMcpDoctorCommandArgs(commandTarget, flags) {
715
754
  allowLocalNetwork = true;
716
755
  continue;
717
756
  }
757
+ if (flag === "--allow-session-lifecycle") {
758
+ if (flags[index + 1] && !flags[index + 1].startsWith("--")) {
759
+ return "--allow-session-lifecycle does not accept a value.";
760
+ }
761
+ allowSessionLifecycle = true;
762
+ continue;
763
+ }
764
+ if (flag === "--require-remote-reliability") {
765
+ if (flags[index + 1] && !flags[index + 1].startsWith("--")) {
766
+ return "--require-remote-reliability does not accept a value.";
767
+ }
768
+ requireRemoteReliability = true;
769
+ continue;
770
+ }
771
+ if (flag.startsWith("--allow-network=") ||
772
+ flag.startsWith("--allow-local-network=") ||
773
+ flag.startsWith("--allow-session-lifecycle=") ||
774
+ flag.startsWith("--require-remote-reliability=")) {
775
+ return `${flag.split("=", 1)[0]} does not accept a value.`;
776
+ }
718
777
  if (flag === "--output") {
719
778
  if (outputPath !== null) {
720
779
  return "Duplicate MCP flag: --output.";
@@ -740,8 +799,10 @@ function buildGenericMcpDoctorCommandArgs(commandTarget, flags) {
740
799
  jsonOutput,
741
800
  outputPath,
742
801
  runtime,
743
- allowNetwork,
744
- allowLocalNetwork
802
+ allowNetwork: remoteNetwork.allowNetwork,
803
+ allowLocalNetwork: remoteNetwork.allowLocalNetwork,
804
+ allowSessionLifecycle,
805
+ requireRemoteReliability
745
806
  };
746
807
  }
747
808
  function parsePerformanceThresholds(flags) {
@@ -1031,7 +1092,9 @@ export async function runCli(args, io = defaultIo, options = {}) {
1031
1092
  }, {
1032
1093
  runtime: parsedMcpArgs.runtime,
1033
1094
  allowNetwork: parsedMcpArgs.allowNetwork,
1034
- allowLocalNetwork: parsedMcpArgs.allowLocalNetwork
1095
+ allowLocalNetwork: parsedMcpArgs.allowLocalNetwork,
1096
+ allowSessionLifecycle: parsedMcpArgs.allowSessionLifecycle,
1097
+ requireRemoteReliability: parsedMcpArgs.requireRemoteReliability
1035
1098
  });
1036
1099
  const renderedReport = parsedMcpArgs.jsonOutput
1037
1100
  ? renderGenericMcpDoctorJson(report)
@@ -1344,6 +1407,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
1344
1407
  runtime,
1345
1408
  ...(remoteNetwork.allowNetwork ? { allowNetwork: true } : {}),
1346
1409
  ...(remoteNetwork.allowLocalNetwork ? { allowLocalNetwork: true } : {}),
1410
+ ...(remoteNetwork.allowSessionLifecycle ? { allowSessionLifecycle: true } : {}),
1411
+ ...(remoteNetwork.requireRemoteReliability ? { requireRemoteReliability: true } : {}),
1347
1412
  ...(runtimeSandbox ? { sandbox: runtimeSandbox } : {}),
1348
1413
  environment: {
1349
1414
  env: terminalContext.env,
@@ -1507,6 +1572,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
1507
1572
  runtime,
1508
1573
  ...(remoteNetwork.allowNetwork ? { allowNetwork: true } : {}),
1509
1574
  ...(remoteNetwork.allowLocalNetwork ? { allowLocalNetwork: true } : {}),
1575
+ ...(remoteNetwork.allowSessionLifecycle ? { allowSessionLifecycle: true } : {}),
1576
+ ...(remoteNetwork.requireRemoteReliability ? { requireRemoteReliability: true } : {}),
1510
1577
  ...(runtimeSandbox ? { sandbox: runtimeSandbox } : {}),
1511
1578
  environment: {
1512
1579
  env: terminalContext.env,
@@ -2292,6 +2359,65 @@ export async function runCli(args, io = defaultIo, options = {}) {
2292
2359
  : renderApplyFixResult(result));
2293
2360
  return 0;
2294
2361
  }
2362
+ if (command === "registry") {
2363
+ const subcommand = maybePath;
2364
+ const target = remainingArgs[0];
2365
+ const flags = remainingArgs.slice(1);
2366
+ if ((subcommand !== "check" && subcommand !== "inspect") || !target || target.startsWith("--")) {
2367
+ io.writeStderr("Usage: codex-plugin-doctor registry check <server.json|directory> [--json] [--output <path>] [--require-registry-readiness]\n"
2368
+ + " codex-plugin-doctor registry inspect <server-name> --allow-network [--json] [--output <path>] [--require-registry-readiness]");
2369
+ return 2;
2370
+ }
2371
+ const allowedFlags = new Set([
2372
+ "--json",
2373
+ "--output",
2374
+ "--allow-network",
2375
+ "--require-registry-readiness"
2376
+ ]);
2377
+ let outputPath = null;
2378
+ for (let index = 0; index < flags.length; index += 1) {
2379
+ const flag = flags[index];
2380
+ if (!allowedFlags.has(flag)) {
2381
+ io.writeStderr(`Unknown registry flag: ${flag}.`);
2382
+ return 2;
2383
+ }
2384
+ if (flag === "--output") {
2385
+ const value = flags[index + 1];
2386
+ if (!value || value.startsWith("--")) {
2387
+ io.writeStderr("Missing path after --output.");
2388
+ return 2;
2389
+ }
2390
+ outputPath = value;
2391
+ index += 1;
2392
+ }
2393
+ }
2394
+ const allowNetwork = flags.includes("--allow-network");
2395
+ if (subcommand === "check" && allowNetwork) {
2396
+ io.writeStderr("--allow-network is supported only by registry inspect.");
2397
+ return 2;
2398
+ }
2399
+ if (subcommand === "inspect" && !allowNetwork) {
2400
+ io.writeStderr("registry inspect requires explicit --allow-network consent.");
2401
+ return 2;
2402
+ }
2403
+ try {
2404
+ const report = subcommand === "check"
2405
+ ? await buildMcpRegistryReadiness(target)
2406
+ : await inspectMcpRegistryServer(target, { allowNetwork: true });
2407
+ const rendered = flags.includes("--json")
2408
+ ? renderMcpRegistryReadinessJson(report)
2409
+ : renderMcpRegistryReadiness(report);
2410
+ if (outputPath) {
2411
+ await writeFile(outputPath, rendered, "utf8");
2412
+ }
2413
+ io.writeStdout(rendered);
2414
+ return registryReadinessExitCode(report, flags.includes("--require-registry-readiness"));
2415
+ }
2416
+ catch (error) {
2417
+ io.writeStderr(`Registry inspection failed: ${error.message}`);
2418
+ return 1;
2419
+ }
2420
+ }
2295
2421
  if (command === "security") {
2296
2422
  if (!maybePath || maybePath.startsWith("--")) {
2297
2423
  io.writeStderr("Missing target path. Usage: codex-plugin-doctor security <path> [--json|--scorecard]");
@@ -2332,7 +2458,9 @@ export async function runCli(args, io = defaultIo, options = {}) {
2332
2458
  }, {
2333
2459
  runtime: parsedMcpArgs.runtime,
2334
2460
  allowNetwork: parsedMcpArgs.allowNetwork,
2335
- allowLocalNetwork: parsedMcpArgs.allowLocalNetwork
2461
+ allowLocalNetwork: parsedMcpArgs.allowLocalNetwork,
2462
+ allowSessionLifecycle: parsedMcpArgs.allowSessionLifecycle,
2463
+ requireRemoteReliability: parsedMcpArgs.requireRemoteReliability
2336
2464
  });
2337
2465
  const renderedReport = parsedMcpArgs.jsonOutput
2338
2466
  ? renderGenericMcpDoctorJson(report)
@@ -2653,6 +2781,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
2653
2781
  runtime: runtimeProbeEnabled,
2654
2782
  ...(remoteNetwork.allowNetwork ? { allowNetwork: true } : {}),
2655
2783
  ...(remoteNetwork.allowLocalNetwork ? { allowLocalNetwork: true } : {}),
2784
+ ...(remoteNetwork.allowSessionLifecycle ? { allowSessionLifecycle: true } : {}),
2785
+ ...(remoteNetwork.requireRemoteReliability ? { requireRemoteReliability: true } : {}),
2656
2786
  ...(runtimeSandbox ? { runtimeSandbox } : {}),
2657
2787
  runCheck: options.runCheckImpl
2658
2788
  });
@@ -2867,6 +2997,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
2867
2997
  runtime: effectiveRuntimeProbeEnabled,
2868
2998
  ...(remoteNetwork.allowNetwork ? { allowNetwork: true } : {}),
2869
2999
  ...(remoteNetwork.allowLocalNetwork ? { allowLocalNetwork: true } : {}),
3000
+ ...(remoteNetwork.allowSessionLifecycle ? { allowSessionLifecycle: true } : {}),
3001
+ ...(remoteNetwork.requireRemoteReliability ? { requireRemoteReliability: true } : {}),
2870
3002
  runtimeTranscript: effectiveRuntimeProbeEnabled && verboseRuntime
2871
3003
  ? (line) => io.writeStderr(line)
2872
3004
  : undefined
@@ -2938,7 +3070,9 @@ export async function runCli(args, io = defaultIo, options = {}) {
2938
3070
  const pluginResult = applyDoctorConfig(await runCheckImpl(pluginRoot, {
2939
3071
  runtime: effectiveRuntimeProbeEnabled,
2940
3072
  ...(remoteNetwork.allowNetwork ? { allowNetwork: true } : {}),
2941
- ...(remoteNetwork.allowLocalNetwork ? { allowLocalNetwork: true } : {})
3073
+ ...(remoteNetwork.allowLocalNetwork ? { allowLocalNetwork: true } : {}),
3074
+ ...(remoteNetwork.allowSessionLifecycle ? { allowSessionLifecycle: true } : {}),
3075
+ ...(remoteNetwork.requireRemoteReliability ? { requireRemoteReliability: true } : {})
2942
3076
  }), applyPolicyToDoctorConfig(applyCheckProfile(await loadDoctorConfig(pluginRoot, configPath), checkProfile), policy));
2943
3077
  io.writeStdout(`\n${pluginResult.targetPath}: ${pluginResult.status.toUpperCase()}`);
2944
3078
  validatedCount += 1;
@@ -2962,6 +3096,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
2962
3096
  runtime: effectiveRuntimeProbeEnabled,
2963
3097
  ...(remoteNetwork.allowNetwork ? { allowNetwork: true } : {}),
2964
3098
  ...(remoteNetwork.allowLocalNetwork ? { allowLocalNetwork: true } : {}),
3099
+ ...(remoteNetwork.allowSessionLifecycle ? { allowSessionLifecycle: true } : {}),
3100
+ ...(remoteNetwork.requireRemoteReliability ? { requireRemoteReliability: true } : {}),
2965
3101
  ...(runtimeSandbox ? { runtimeSandbox } : {}),
2966
3102
  ...(effectiveRuntimeProbeEnabled && verboseRuntime
2967
3103
  ? { runtimeTranscript: (line) => io.writeStderr(line) }