codex-plugin-doctor 1.51.0 → 1.52.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.
@@ -161,6 +161,24 @@ export const ruleCatalog = [
161
161
  fix: "Add `command` for stdio servers or `url` for remote servers.",
162
162
  example: '{ "command": "node", "args": ["server.js"] }'
163
163
  },
164
+ {
165
+ id: "mcp.server.transport.conflict",
166
+ category: "mcp",
167
+ defaultSeverity: "fail",
168
+ summary: "An MCP server defines both command and URL transports.",
169
+ why: "Clients cannot select a transport deterministically when a server defines both process and remote connection settings.",
170
+ fix: "Keep either command for stdio or url for remote MCP, but not both.",
171
+ example: '{ "url": "https://example.com/mcp" }'
172
+ },
173
+ {
174
+ id: "plugin.mcp.server.transport.conflict",
175
+ category: "mcp",
176
+ defaultSeverity: "fail",
177
+ summary: "A bundled MCP server defines both command and URL transports.",
178
+ why: "Codex cannot select a transport deterministically when a bundled server defines both process and remote connection settings.",
179
+ fix: "Keep either command for stdio or url for remote MCP, but not both.",
180
+ example: '{ "url": "https://example.com/mcp" }'
181
+ },
164
182
  {
165
183
  id: "plugin.security.path_traversal",
166
184
  category: "security",
@@ -245,12 +263,66 @@ export const ruleCatalog = [
245
263
  {
246
264
  id: "plugin.security.insecure_http_url",
247
265
  category: "security",
248
- defaultSeverity: "warn",
266
+ defaultSeverity: "fail",
249
267
  summary: "An MCP server uses a plain HTTP URL.",
250
268
  why: "Plain HTTP can expose MCP traffic and does not verify endpoint identity on non-local networks.",
251
269
  fix: "Use HTTPS for remote MCP servers; reserve HTTP for explicit localhost development endpoints.",
252
270
  example: '{ "url": "https://example.com/mcp" }'
253
271
  },
272
+ {
273
+ id: "plugin.security.remote_mcp_url.invalid",
274
+ category: "security",
275
+ defaultSeverity: "fail",
276
+ summary: "An MCP server URL is not an absolute HTTP or HTTPS URL.",
277
+ why: "Clients cannot reliably connect to malformed or relative remote MCP endpoints.",
278
+ fix: "Use an absolute HTTPS URL, or an explicit localhost HTTP development URL.",
279
+ example: '{ "url": "https://example.com/mcp" }'
280
+ },
281
+ {
282
+ id: "plugin.security.remote_mcp_url.unsupported_scheme",
283
+ category: "security",
284
+ defaultSeverity: "fail",
285
+ summary: "An MCP server URL uses an unsupported scheme.",
286
+ why: "Remote MCP transport supports only HTTP and HTTPS endpoints.",
287
+ fix: "Use an HTTPS URL, or an explicit localhost HTTP development URL.",
288
+ example: '{ "url": "https://example.com/mcp" }'
289
+ },
290
+ {
291
+ id: "plugin.security.remote_mcp_url.credentials",
292
+ category: "security",
293
+ defaultSeverity: "fail",
294
+ summary: "An MCP server URL embeds credentials.",
295
+ why: "URL credentials can leak through configuration, logs, reports, and package artifacts.",
296
+ fix: "Remove URL credentials and configure authentication outside the endpoint URL.",
297
+ example: '{ "url": "https://example.com/mcp" }'
298
+ },
299
+ {
300
+ id: "plugin.security.remote_mcp_url.query",
301
+ category: "security",
302
+ defaultSeverity: "fail",
303
+ summary: "An MCP server URL contains a query string.",
304
+ why: "Query strings can carry secrets and make endpoint configuration ambiguous.",
305
+ fix: "Remove the query string from the MCP endpoint URL.",
306
+ example: '{ "url": "https://example.com/mcp" }'
307
+ },
308
+ {
309
+ id: "plugin.security.remote_mcp_url.fragment",
310
+ category: "security",
311
+ defaultSeverity: "fail",
312
+ summary: "An MCP server URL contains a fragment.",
313
+ why: "Fragments are not sent to servers and can hide misleading endpoint configuration.",
314
+ fix: "Remove the fragment from the MCP endpoint URL.",
315
+ example: '{ "url": "https://example.com/mcp" }'
316
+ },
317
+ {
318
+ id: "plugin.security.remote_mcp_url.ip_literal",
319
+ category: "security",
320
+ defaultSeverity: "fail",
321
+ summary: "An MCP server URL uses a numeric IP literal.",
322
+ why: "Numeric IP endpoints bypass hostname-based endpoint review and are not an accepted remote MCP shape.",
323
+ fix: "Use a reviewed hostname; use localhost only for local development.",
324
+ example: '{ "url": "https://example.com/mcp" }'
325
+ },
254
326
  {
255
327
  id: "plugin.security.mcp_binds_all_interfaces",
256
328
  category: "security",
@@ -376,6 +448,114 @@ export const ruleCatalog = [
376
448
  why: "MCP stdio transport requires newline-delimited JSON-RPC messages on stdout.",
377
449
  fix: "Send logs to stderr and reserve stdout for JSON-RPC protocol messages only.",
378
450
  example: "Use `console.error` for diagnostics in Node stdio servers."
451
+ },
452
+ {
453
+ id: "plugin.runtime.remote.network_not_approved",
454
+ category: "runtime",
455
+ defaultSeverity: "fail",
456
+ summary: "Remote MCP probing was not explicitly approved.",
457
+ why: "Remote initialization creates outbound network traffic.",
458
+ fix: "Review the plan, then use --runtime --allow-network; add --allow-local-network only for localhost HTTP.",
459
+ example: "codex-plugin-doctor mcp . --runtime --allow-network"
460
+ },
461
+ {
462
+ id: "plugin.runtime.remote.url.invalid",
463
+ category: "runtime",
464
+ defaultSeverity: "fail",
465
+ summary: "A remote MCP endpoint URL is unsafe or unsupported.",
466
+ why: "Credentials, queries, fragments, IP literals, and unsupported schemes bypass the remote probe boundary.",
467
+ fix: "Use an absolute HTTPS URL without credentials, query parameters, fragments, or IP literals.",
468
+ example: "https://mcp.example/mcp"
469
+ },
470
+ {
471
+ id: "plugin.runtime.remote.transport.timeout",
472
+ category: "runtime",
473
+ defaultSeverity: "fail",
474
+ summary: "The remote MCP initialize request timed out.",
475
+ why: "A bounded probe cannot safely negotiate an unavailable endpoint.",
476
+ fix: "Make the endpoint reachable and complete initialize within the configured timeout.",
477
+ example: "Return a valid initialize response promptly."
478
+ },
479
+ {
480
+ id: "plugin.runtime.remote.transport.response_too_large",
481
+ category: "runtime",
482
+ defaultSeverity: "fail",
483
+ summary: "The remote MCP response exceeded the bounded probe limit.",
484
+ why: "Unbounded remote responses can exhaust local resources.",
485
+ fix: "Return a compact initialize response and keep discovery metadata bounded.",
486
+ example: "Return only the MCP initialize result."
487
+ },
488
+ {
489
+ id: "plugin.runtime.remote.transport.failed",
490
+ category: "runtime",
491
+ defaultSeverity: "fail",
492
+ summary: "The remote MCP transport request failed.",
493
+ why: "Protocol negotiation cannot proceed without a safe bounded connection.",
494
+ fix: "Verify endpoint reachability, TLS, and remote network policy eligibility.",
495
+ example: "Use a public HTTPS endpoint or explicitly approved localhost HTTP."
496
+ },
497
+ {
498
+ id: "plugin.runtime.remote.http_status.invalid",
499
+ category: "runtime",
500
+ defaultSeverity: "fail",
501
+ summary: "The remote MCP initialize response used an unexpected HTTP status.",
502
+ why: "Streamable HTTP initialization requires a successful response before negotiation.",
503
+ fix: "Return HTTP 200 for initialize or publish valid OAuth discovery metadata for protected endpoints.",
504
+ example: "HTTP/1.1 200 OK"
505
+ },
506
+ {
507
+ id: "plugin.runtime.remote.content_type.invalid",
508
+ category: "runtime",
509
+ defaultSeverity: "fail",
510
+ summary: "The remote MCP initialize response used an unsupported content type.",
511
+ why: "The probe can only validate JSON or Server-Sent Events protocol responses.",
512
+ fix: "Return application/json or text/event-stream with a JSON-RPC response.",
513
+ example: "Content-Type: application/json"
514
+ },
515
+ {
516
+ id: "plugin.runtime.remote.session.invalid",
517
+ category: "runtime",
518
+ defaultSeverity: "fail",
519
+ summary: "The remote MCP session header is invalid.",
520
+ why: "An invalid session identifier cannot be safely replayed on the initialized notification.",
521
+ fix: "Return MCP-Session-Id only as visible ASCII characters.",
522
+ example: "MCP-Session-Id: session-123"
523
+ },
524
+ {
525
+ id: "plugin.runtime.remote.initialize.invalid",
526
+ category: "runtime",
527
+ defaultSeverity: "fail",
528
+ summary: "The remote MCP initialize JSON-RPC result is invalid.",
529
+ why: "Invalid negotiation results leave capabilities and protocol version unknown.",
530
+ fix: "Return a JSON-RPC 2.0 initialize result for protocol version 2025-11-25.",
531
+ example: '{ "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2025-11-25" } }'
532
+ },
533
+ {
534
+ id: "plugin.runtime.remote.initialized.failed",
535
+ category: "runtime",
536
+ defaultSeverity: "fail",
537
+ summary: "The remote MCP server did not acknowledge notifications/initialized.",
538
+ why: "The session may not be ready for subsequent protocol traffic.",
539
+ fix: "Accept a successful initialized notification at the configured MCP endpoint.",
540
+ example: "Return HTTP 204 to notifications/initialized."
541
+ },
542
+ {
543
+ id: "plugin.runtime.remote.authorization.metadata.invalid",
544
+ category: "runtime",
545
+ defaultSeverity: "fail",
546
+ summary: "Remote OAuth discovery metadata is invalid.",
547
+ why: "Protected MCP endpoints cannot be assessed safely without valid authorization metadata.",
548
+ fix: "Publish valid HTTPS protected-resource and authorization-server metadata.",
549
+ example: "https://mcp.example/.well-known/oauth-protected-resource"
550
+ },
551
+ {
552
+ id: "plugin.runtime.remote.authorization.metadata.unavailable",
553
+ category: "runtime",
554
+ defaultSeverity: "fail",
555
+ summary: "Remote OAuth discovery metadata is unavailable.",
556
+ why: "Authorization readiness cannot be confirmed without bounded metadata responses.",
557
+ fix: "Make protected-resource and authorization-server metadata available over HTTPS.",
558
+ example: "Return application/json discovery metadata."
379
559
  }
380
560
  ];
381
561
  export function findRuleDefinition(id) {
package/dist/run-cli.js CHANGED
@@ -99,8 +99,41 @@ function parseRuntimeSandbox(flags, options = { requireRuntime: true }) {
99
99
  }
100
100
  return value;
101
101
  }
102
+ function parseRemoteNetworkFlags(flags, runtime) {
103
+ let allowNetwork = false;
104
+ let allowLocalNetwork = false;
105
+ for (let index = 0; index < flags.length; index += 1) {
106
+ const flag = flags[index];
107
+ if (flag === "--allow-network") {
108
+ if (allowNetwork)
109
+ return new CliUsageError("Duplicate runtime network flag: --allow-network.");
110
+ if (flags[index + 1] && !flags[index + 1].startsWith("--")) {
111
+ return new CliUsageError("--allow-network does not accept a value.");
112
+ }
113
+ allowNetwork = true;
114
+ }
115
+ else if (flag === "--allow-local-network") {
116
+ if (allowLocalNetwork)
117
+ return new CliUsageError("Duplicate runtime network flag: --allow-local-network.");
118
+ if (flags[index + 1] && !flags[index + 1].startsWith("--")) {
119
+ return new CliUsageError("--allow-local-network does not accept a value.");
120
+ }
121
+ allowLocalNetwork = true;
122
+ }
123
+ else if (flag?.startsWith("--allow-network=") || flag?.startsWith("--allow-local-network=")) {
124
+ return new CliUsageError(`${flag.split("=", 1)[0]} does not accept a value.`);
125
+ }
126
+ }
127
+ if ((allowNetwork || allowLocalNetwork) && !runtime) {
128
+ return new CliUsageError("--allow-network requires --runtime.");
129
+ }
130
+ if (allowLocalNetwork && !allowNetwork) {
131
+ return new CliUsageError("--allow-local-network requires --allow-network.");
132
+ }
133
+ return { allowNetwork, allowLocalNetwork };
134
+ }
102
135
  function printUsage(io) {
103
- 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] [--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] [--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> [--runtime --sandbox docker] [--json|--markdown] [--output <path>]|runtime-policy <path> [--runtime --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 --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 [--upload]|mcp <path>|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 --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");
136
+ 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");
104
137
  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>]");
105
138
  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]");
106
139
  io.writeStderr("Baseline gating: codex-plugin-doctor baseline create <path> --output <path> [--runtime]\n codex-plugin-doctor check <path> --baseline <path>");
@@ -652,6 +685,8 @@ function buildGenericMcpDoctorCommandArgs(commandTarget, flags) {
652
685
  let jsonOutput = false;
653
686
  let outputPath = null;
654
687
  let runtime = false;
688
+ let allowNetwork = false;
689
+ let allowLocalNetwork = false;
655
690
  for (let index = 0; index < flags.length; index += 1) {
656
691
  const flag = flags[index];
657
692
  if (flag === "--runtime") {
@@ -668,6 +703,18 @@ function buildGenericMcpDoctorCommandArgs(commandTarget, flags) {
668
703
  jsonOutput = true;
669
704
  continue;
670
705
  }
706
+ if (flag === "--allow-network") {
707
+ if (allowNetwork)
708
+ return "Duplicate MCP flag: --allow-network.";
709
+ allowNetwork = true;
710
+ continue;
711
+ }
712
+ if (flag === "--allow-local-network") {
713
+ if (allowLocalNetwork)
714
+ return "Duplicate MCP flag: --allow-local-network.";
715
+ allowLocalNetwork = true;
716
+ continue;
717
+ }
671
718
  if (flag === "--output") {
672
719
  if (outputPath !== null) {
673
720
  return "Duplicate MCP flag: --output.";
@@ -684,11 +731,17 @@ function buildGenericMcpDoctorCommandArgs(commandTarget, flags) {
684
731
  ? `Unknown MCP flag: ${flag}.`
685
732
  : `Unexpected MCP argument: ${flag}.`;
686
733
  }
734
+ const remoteNetwork = parseRemoteNetworkFlags(flags, runtime);
735
+ if (remoteNetwork instanceof CliUsageError) {
736
+ return remoteNetwork.message;
737
+ }
687
738
  return {
688
739
  targetPath: commandTarget,
689
740
  jsonOutput,
690
741
  outputPath,
691
- runtime
742
+ runtime,
743
+ allowNetwork,
744
+ allowLocalNetwork
692
745
  };
693
746
  }
694
747
  function parsePerformanceThresholds(flags) {
@@ -976,7 +1029,9 @@ export async function runCli(args, io = defaultIo, options = {}) {
976
1029
  env: terminalContext.env,
977
1030
  platform: terminalContext.platform
978
1031
  }, {
979
- runtime: parsedMcpArgs.runtime
1032
+ runtime: parsedMcpArgs.runtime,
1033
+ allowNetwork: parsedMcpArgs.allowNetwork,
1034
+ allowLocalNetwork: parsedMcpArgs.allowLocalNetwork
980
1035
  });
981
1036
  const renderedReport = parsedMcpArgs.jsonOutput
982
1037
  ? renderGenericMcpDoctorJson(report)
@@ -1218,7 +1273,12 @@ export async function runCli(args, io = defaultIo, options = {}) {
1218
1273
  ? null
1219
1274
  : assetFlags[runtimeApprovalDigestIndex + 1];
1220
1275
  const runtime = assetFlags.includes("--runtime");
1276
+ const remoteNetwork = parseRemoteNetworkFlags(assetFlags, runtime);
1221
1277
  let runtimeSandbox;
1278
+ if (remoteNetwork instanceof CliUsageError) {
1279
+ io.writeStderr(remoteNetwork.message);
1280
+ return 2;
1281
+ }
1222
1282
  try {
1223
1283
  runtimeSandbox = parseRuntimeSandbox(assetFlags);
1224
1284
  }
@@ -1282,6 +1342,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
1282
1342
  requireRuntimeApproval,
1283
1343
  runtimeApprovalDigest,
1284
1344
  runtime,
1345
+ ...(remoteNetwork.allowNetwork ? { allowNetwork: true } : {}),
1346
+ ...(remoteNetwork.allowLocalNetwork ? { allowLocalNetwork: true } : {}),
1285
1347
  ...(runtimeSandbox ? { sandbox: runtimeSandbox } : {}),
1286
1348
  environment: {
1287
1349
  env: terminalContext.env,
@@ -1290,6 +1352,10 @@ export async function runCli(args, io = defaultIo, options = {}) {
1290
1352
  runCheck: options.runCheckImpl ?? runCheck,
1291
1353
  performanceThresholds: parsedThresholds.thresholds
1292
1354
  });
1355
+ if (!evidence.releaseReady) {
1356
+ io.writeStdout(jsonOutput ? renderDoctorReleaseEvidenceJson(evidence) : renderDoctorReleaseEvidence(evidence));
1357
+ return evidence.exitCode;
1358
+ }
1293
1359
  await writeFile(resolvedOutputPath, renderDoctorReleaseEvidenceJson(evidence), "utf8");
1294
1360
  let uploaded = false;
1295
1361
  const uploadArgs = ["release", "upload", tag, resolvedOutputPath, "--clobber"];
@@ -1387,7 +1453,12 @@ export async function runCli(args, io = defaultIo, options = {}) {
1387
1453
  ? null
1388
1454
  : evidenceFlags[runtimeApprovalDigestIndex + 1];
1389
1455
  const runtime = evidenceFlags.includes("--runtime");
1456
+ const remoteNetwork = parseRemoteNetworkFlags(evidenceFlags, runtime);
1390
1457
  let runtimeSandbox;
1458
+ if (remoteNetwork instanceof CliUsageError) {
1459
+ io.writeStderr(remoteNetwork.message);
1460
+ return 2;
1461
+ }
1391
1462
  try {
1392
1463
  runtimeSandbox = parseRuntimeSandbox(evidenceFlags);
1393
1464
  }
@@ -1434,6 +1505,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
1434
1505
  requireRuntimeApproval,
1435
1506
  runtimeApprovalDigest,
1436
1507
  runtime,
1508
+ ...(remoteNetwork.allowNetwork ? { allowNetwork: true } : {}),
1509
+ ...(remoteNetwork.allowLocalNetwork ? { allowLocalNetwork: true } : {}),
1437
1510
  ...(runtimeSandbox ? { sandbox: runtimeSandbox } : {}),
1438
1511
  environment: {
1439
1512
  env: terminalContext.env,
@@ -2257,7 +2330,9 @@ export async function runCli(args, io = defaultIo, options = {}) {
2257
2330
  env: terminalContext.env,
2258
2331
  platform: terminalContext.platform
2259
2332
  }, {
2260
- runtime: parsedMcpArgs.runtime
2333
+ runtime: parsedMcpArgs.runtime,
2334
+ allowNetwork: parsedMcpArgs.allowNetwork,
2335
+ allowLocalNetwork: parsedMcpArgs.allowLocalNetwork
2261
2336
  });
2262
2337
  const renderedReport = parsedMcpArgs.jsonOutput
2263
2338
  ? renderGenericMcpDoctorJson(report)
@@ -2534,7 +2609,12 @@ export async function runCli(args, io = defaultIo, options = {}) {
2534
2609
  : remainingArgs.slice(1);
2535
2610
  const jsonOutput = releaseFlags.includes("--json");
2536
2611
  const runtimeProbeEnabled = releaseFlags.includes("--runtime");
2612
+ const remoteNetwork = parseRemoteNetworkFlags(releaseFlags, runtimeProbeEnabled);
2537
2613
  let runtimeSandbox;
2614
+ if (remoteNetwork instanceof CliUsageError) {
2615
+ io.writeStderr(remoteNetwork.message);
2616
+ return 2;
2617
+ }
2538
2618
  try {
2539
2619
  runtimeSandbox = parseRuntimeSandbox(releaseFlags);
2540
2620
  }
@@ -2571,6 +2651,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
2571
2651
  env: terminalContext.env,
2572
2652
  platform: terminalContext.platform,
2573
2653
  runtime: runtimeProbeEnabled,
2654
+ ...(remoteNetwork.allowNetwork ? { allowNetwork: true } : {}),
2655
+ ...(remoteNetwork.allowLocalNetwork ? { allowLocalNetwork: true } : {}),
2574
2656
  ...(runtimeSandbox ? { runtimeSandbox } : {}),
2575
2657
  runCheck: options.runCheckImpl
2576
2658
  });
@@ -2717,6 +2799,11 @@ export async function runCli(args, io = defaultIo, options = {}) {
2717
2799
  const effectiveRuntimeProbeEnabled = runtimeProbeEnabled ||
2718
2800
  checkProfile === "publish" ||
2719
2801
  policyEnablesRuntime(policy);
2802
+ const remoteNetwork = parseRemoteNetworkFlags(normalizedFlags, effectiveRuntimeProbeEnabled);
2803
+ if (remoteNetwork instanceof CliUsageError) {
2804
+ io.writeStderr(remoteNetwork.message);
2805
+ return 2;
2806
+ }
2720
2807
  if (requireRuntimeApproval && !effectiveRuntimeProbeEnabled) {
2721
2808
  io.writeStderr("Runtime approval requires runtime probing. Add --runtime, --profile publish, or a runtime-enabled policy.");
2722
2809
  return 2;
@@ -2778,6 +2865,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
2778
2865
  plugin,
2779
2866
  result: applyDoctorConfig(await runCheckImpl(plugin.rootPath, {
2780
2867
  runtime: effectiveRuntimeProbeEnabled,
2868
+ ...(remoteNetwork.allowNetwork ? { allowNetwork: true } : {}),
2869
+ ...(remoteNetwork.allowLocalNetwork ? { allowLocalNetwork: true } : {}),
2781
2870
  runtimeTranscript: effectiveRuntimeProbeEnabled && verboseRuntime
2782
2871
  ? (line) => io.writeStderr(line)
2783
2872
  : undefined
@@ -2847,7 +2936,9 @@ export async function runCli(args, io = defaultIo, options = {}) {
2847
2936
  continue;
2848
2937
  }
2849
2938
  const pluginResult = applyDoctorConfig(await runCheckImpl(pluginRoot, {
2850
- runtime: effectiveRuntimeProbeEnabled
2939
+ runtime: effectiveRuntimeProbeEnabled,
2940
+ ...(remoteNetwork.allowNetwork ? { allowNetwork: true } : {}),
2941
+ ...(remoteNetwork.allowLocalNetwork ? { allowLocalNetwork: true } : {})
2851
2942
  }), applyPolicyToDoctorConfig(applyCheckProfile(await loadDoctorConfig(pluginRoot, configPath), checkProfile), policy));
2852
2943
  io.writeStdout(`\n${pluginResult.targetPath}: ${pluginResult.status.toUpperCase()}`);
2853
2944
  validatedCount += 1;
@@ -2869,6 +2960,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
2869
2960
  const doctorConfig = applyPolicyToDoctorConfig(applyCheckProfile(await loadDoctorConfig(targetPath, configPath), checkProfile), policy);
2870
2961
  const configuredResult = applyDoctorConfig(await runCheckImpl(targetPath, {
2871
2962
  runtime: effectiveRuntimeProbeEnabled,
2963
+ ...(remoteNetwork.allowNetwork ? { allowNetwork: true } : {}),
2964
+ ...(remoteNetwork.allowLocalNetwork ? { allowLocalNetwork: true } : {}),
2872
2965
  ...(runtimeSandbox ? { runtimeSandbox } : {}),
2873
2966
  ...(effectiveRuntimeProbeEnabled && verboseRuntime
2874
2967
  ? { runtimeTranscript: (line) => io.writeStderr(line) }
@@ -2,6 +2,7 @@ import { readFile, readdir, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { discoverPackage } from "../core/discover-package.js";
4
4
  import { readJsonFile } from "../core/read-json-file.js";
5
+ import { inspectRemoteMcpUrl } from "../core/remote-url-policy.js";
5
6
  import { validatePlugin } from "../core/validate-plugin.js";
6
7
  import { formatFindingFingerprintLine, withFindingFingerprints } from "../reporting/finding-fingerprint.js";
7
8
  import { formatFindingEvidenceLine } from "../reporting/format-finding-evidence.js";
@@ -18,6 +19,16 @@ function buildFinding(severity, id, message, impact, suggestedFix, evidence) {
18
19
  function isPlainObject(value) {
19
20
  return typeof value === "object" && value !== null && !Array.isArray(value);
20
21
  }
22
+ function remoteUrlIssueFindingId(issue) {
23
+ return issue === "insecure_non_loopback"
24
+ ? "plugin.security.insecure_http_url"
25
+ : `plugin.security.remote_mcp_url.${issue}`;
26
+ }
27
+ function remoteUrlIssueMessage(issue) {
28
+ return issue === "insecure_non_loopback"
29
+ ? "uses an insecure public HTTP URL"
30
+ : `uses a remote MCP URL with ${issue.replaceAll("_", " ")}`;
31
+ }
21
32
  function isPathWithinRoot(rootPath, candidatePath) {
22
33
  const relativePath = path.relative(rootPath, candidatePath);
23
34
  return (relativePath === "" ||
@@ -245,19 +256,33 @@ export function auditMcpServerConfig(rootPath, parsedConfig, options = {}) {
245
256
  }));
246
257
  }
247
258
  }
248
- if (typeof url === "string" && /^http:\/\//i.test(url)) {
249
- findings.push(buildFinding("warn", "plugin.security.insecure_http_url", `The MCP server \`${serverName}\` uses an insecure HTTP URL.`, "Plain HTTP transports can expose MCP traffic on non-local networks and make endpoint identity harder to verify.", "Use HTTPS for remote MCP servers; reserve HTTP for explicit localhost development endpoints.", { serverName, configPath, url }));
250
- }
251
- if (typeof url === "string" && /\/\/0\.0\.0\.0[:/]/i.test(url)) {
252
- findings.push(buildFinding("warn", "plugin.security.mcp_binds_all_interfaces", `The MCP server \`${serverName}\` URL binds to \`0.0.0.0\`.`, "Servers that listen on all interfaces can accept connections from external hosts, which is rarely intended for local MCP development.", "Use `127.0.0.1` or `localhost` instead of `0.0.0.0` unless external access is explicitly required.", { serverName, configPath, url }));
259
+ if (typeof url === "string") {
260
+ const inspection = inspectRemoteMcpUrl(url);
261
+ for (const issue of inspection.issues) {
262
+ findings.push(buildFinding("fail", remoteUrlIssueFindingId(issue), `The MCP server \`${serverName}\` ${remoteUrlIssueMessage(issue)}.`, "Unsafe or ambiguous remote transport configuration can expose credentials or prevent reliable MCP connectivity.", "Use an absolute HTTPS URL without credentials, query parameters, fragments, or numeric IP literals; HTTP is only supported for localhost development.", {
263
+ serverName,
264
+ configPath,
265
+ url: inspection.sanitizedUrl
266
+ }));
267
+ }
268
+ if (inspection.parsedUrl?.hostname === "0.0.0.0") {
269
+ findings.push(buildFinding("warn", "plugin.security.mcp_binds_all_interfaces", `The MCP server \`${serverName}\` URL binds to \`0.0.0.0\`.`, "Servers that listen on all interfaces can accept connections from external hosts, which is rarely intended for local MCP development.", "Use `127.0.0.1` or `localhost` instead of `0.0.0.0` unless external access is explicitly required.", {
270
+ serverName,
271
+ configPath,
272
+ url: inspection.sanitizedUrl
273
+ }));
274
+ }
253
275
  }
254
276
  }
255
277
  return findings;
256
278
  }
257
279
  const externalUrlPattern = /https?:\/\/[^\s`"'<>)]+/gi;
258
- async function auditSkillExternalReferences(rootPath) {
280
+ async function auditSkillExternalReferences(rootPath, mcpConfigPath) {
259
281
  const findings = [];
260
282
  for (const filePath of await collectPromptPoisoningScanFiles(rootPath)) {
283
+ if (mcpConfigPath !== null && path.resolve(filePath) === mcpConfigPath) {
284
+ continue;
285
+ }
261
286
  const content = await readFile(filePath, "utf8");
262
287
  const matches = content.match(externalUrlPattern);
263
288
  if (!matches) {
@@ -422,7 +447,9 @@ export async function buildSecurityAudit(targetPath) {
422
447
  ...(await auditMcpCommandSurface(discoveredPackage)),
423
448
  ...(await auditChildProcessSourceSurface(discoveredPackage.rootPath)),
424
449
  ...(await auditPromptPoisoningSurface(discoveredPackage.rootPath)),
425
- ...(await auditSkillExternalReferences(discoveredPackage.rootPath))
450
+ ...(await auditSkillExternalReferences(discoveredPackage.rootPath, discoveredPackage.manifest.mcpServers
451
+ ? path.resolve(discoveredPackage.rootPath, discoveredPackage.manifest.mcpServers)
452
+ : null))
426
453
  ];
427
454
  return buildSecurityAuditFromFindings(discoveredPackage.rootPath, findings);
428
455
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-plugin-doctor",
3
- "version": "1.51.0",
3
+ "version": "1.52.0",
4
4
  "description": "CLI-first validator for Codex plugins, skills, and MCP package surfaces with runtime MCP protocol validation.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",