codex-plugin-doctor 1.1.0 → 1.3.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.
package/README.md CHANGED
@@ -64,7 +64,7 @@ Security scorecard with `security`:
64
64
  - MCP server `cwd` paths that escape the package root
65
65
  - plain HTTP remote transport warnings
66
66
 
67
- Runtime MCP validation with `--runtime`:
67
+ Runtime MCP validation with `--runtime`:
68
68
 
69
69
  - `initialize`
70
70
  - `notifications/initialized`
@@ -76,8 +76,9 @@ Runtime MCP validation with `--runtime`:
76
76
  - `prompts/list`
77
77
  - `prompts/get`
78
78
  - paginated list responses
79
- - runtime capability scorecard
80
- - redacted verbose transcript with `--verbose-runtime`
79
+ - runtime capability scorecard
80
+ - redacted verbose transcript with `--verbose-runtime`
81
+ - optional runtime approval gating with a precomputed `doctor runtime-plan` digest
81
82
 
82
83
  Output formats:
83
84
 
@@ -212,6 +213,9 @@ codex-plugin-doctor doctor trust . --json --output trust-score.json
212
213
  codex-plugin-doctor doctor perf .
213
214
  codex-plugin-doctor doctor perf . --json --output perf.json
214
215
  codex-plugin-doctor doctor perf . --max-total-ms 2500 --max-stage-ms validation=500
216
+ codex-plugin-doctor doctor runtime-plan .
217
+ codex-plugin-doctor doctor runtime-plan . --json --output runtime-plan.json
218
+ codex-plugin-doctor doctor runtime-plan . --markdown --output runtime-plan.md
215
219
  codex-plugin-doctor doctor mcp .
216
220
  codex-plugin-doctor doctor mcp . --json --output mcp-healthcheck.json
217
221
  codex-plugin-doctor doctor export --bundle .
@@ -267,9 +271,10 @@ codex-plugin-doctor check . --badge-json --output doctor-badge.json
267
271
  codex-plugin-doctor check . --badge-markdown
268
272
  codex-plugin-doctor check . --sarif --output results.sarif
269
273
  codex-plugin-doctor check . --ascii
270
- codex-plugin-doctor check . --no-animations
271
- codex-plugin-doctor check . --runtime
272
- codex-plugin-doctor check . --config .codex-doctor.json
274
+ codex-plugin-doctor check . --no-animations
275
+ codex-plugin-doctor check . --runtime
276
+ codex-plugin-doctor check . --runtime --require-runtime-approval --runtime-approval-digest sha256:<approved-plan-digest>
277
+ codex-plugin-doctor check . --config .codex-doctor.json
273
278
  codex-plugin-doctor check . --history validation-history.jsonl
274
279
  codex-plugin-doctor history validation-history.jsonl
275
280
  codex-plugin-doctor history validation-history.jsonl --json
@@ -282,7 +287,7 @@ codex-plugin-doctor check . --json --runtime --verbose-runtime
282
287
 
283
288
  `self-test` runs the bundled runtime-complete sample through static validation, runtime MCP probes, and the compatibility scorecard. It is the fastest post-install check after `npm install -g codex-plugin-doctor`.
284
289
 
285
- `doctor` checks the local environment, including package version, platform, Node version, npm global prefix, Codex home, and Codex plugin cache visibility. The text output also includes recommended next commands for self-test, installed plugin discovery, runtime checks, compatibility scoring, and CI setup. `doctor contract` publishes the machine-readable output contract, including public JSON schema surfaces, stable-through-1.0 compatibility metadata, and a frozen rule catalog digest. Add `--json` for automation or `--output output-contract.json` to write the contract to disk. `doctor corpus` runs the bundled validation corpus against healthy runtime, risky security, starter skill, and generic MCP packages, then reports whether each case matched its expected outcome. Add `--json` for automation or `--output validation-corpus.json` to write the corpus report to disk. `doctor npm <package>` runs a preinstall scan by packing the npm package with scripts disabled, extracting the publish tarball, and running validation, security, trust, and recommendation checks against the shipped contents. Use a published Codex plugin package as the target; scanning `codex-plugin-doctor` itself intentionally reports a missing plugin manifest because this CLI package is not a plugin package. Add `--json` for automation or `--output npm-preinstall.json` to write the report to disk. `doctor attest <path>` creates a local attestation with stable package/report digests, validation/security/compatibility/trust summary, and verification metadata. Add `--sign-key-env NAME` to attach a local HMAC-SHA256 signature without printing the secret, or `--json --output attestation.json` to write the artifact to disk. `doctor attest verify <attestation.json> --target <path> --sign-key-env NAME` recomputes the package fingerprint, report digest, and HMAC signature offline; verification intentionally treats `generatedAt`, `targetPath`, `verification`, and `signature.keyHint` as unsigned display metadata. `doctor release-evidence <path> --sign-key-env NAME` creates one redacted release bundle with signed attestation, offline verification, corpus, performance, security, trust, package metadata, and git release gates. Strict release evidence requires a clean tagged worktree; use `--allow-dirty` or `--allow-untagged` only for local rehearsal. `doctor release-evidence verify <evidence.json> --target <path> --sign-key-env NAME` verifies a shared release evidence artifact offline against an explicit package path; the artifact target path is treated as display metadata, not trusted input. `doctor release-evidence asset <path> --tag <tag> --output <evidence.json> --sign-key-env NAME` writes a signed release evidence file and prints the `gh release upload` command; add `--upload` to run the upload through GitHub CLI with `--clobber`. `doctor inspector <path>` builds a safe MCP Inspector launch command from a packaged `.mcp.json` file without starting the Inspector proxy automatically. Use `--server <name>` when the package contains multiple MCP server entries. `doctor diff --before <path> --after <path>` compares two package roots and reports new findings, resolved findings, trust score delta, and whether risk increased. `doctor recommend <path>` turns validation, security, and compatibility signals into a prioritized action plan with blocker, high, medium, and info actions. Add `--json` for automation or `--output recommendations.json` to write the report to disk. `doctor trust <path>` creates a local trust score from package lifecycle scripts, dependency specs, and MCP security findings. Use it before release when you want supply-chain risks summarized as one score. `doctor perf <path>` profiles the shared package analysis pipeline and reports per-stage durations for validation, config, security, compatibility, trust, recommendations, and total runtime. Add `--max-total-ms <ms>` or repeatable `--max-stage-ms stage=ms` to fail CI when a budget is exceeded. `doctor mcp <path>` exposes the generic MCP static health report under the doctor command family without starting local MCP servers. `doctor export --bundle <path>` creates a redacted operator handoff bundle that includes validation JSON, security scorecard data, compatibility matrix, recommendations, and trust score in one file. `doctor snapshot` creates a redacted diagnostics bundle with environment health, client config readiness, installed plugin metadata, and next commands. Add `--json` for machine-readable output or `--output doctor-snapshot.json` to write the bundle to disk. `doctor clients` reports local Codex, Claude Desktop, Cursor, Cline, and Windsurf config readiness. `doctor --update-check` compares the installed CLI version with the latest npm version and prints the upgrade command when a newer release is available.
290
+ `doctor` checks the local environment, including package version, platform, Node version, npm global prefix, Codex home, and Codex plugin cache visibility. The text output also includes recommended next commands for self-test, installed plugin discovery, runtime checks, compatibility scoring, and CI setup. `doctor contract` publishes the machine-readable output contract, including public JSON schema surfaces, stable-through-1.0 compatibility metadata, and a frozen rule catalog digest. Add `--json` for automation or `--output output-contract.json` to write the contract to disk. `doctor corpus` runs the bundled validation corpus against healthy runtime, risky security, starter skill, and generic MCP packages, then reports whether each case matched its expected outcome. Add `--json` for automation or `--output validation-corpus.json` to write the corpus report to disk. `doctor npm <package>` runs a preinstall scan by packing the npm package with scripts disabled, extracting the publish tarball, and running validation, security, trust, and recommendation checks against the shipped contents. Use a published Codex plugin package as the target; scanning `codex-plugin-doctor` itself intentionally reports a missing plugin manifest because this CLI package is not a plugin package. Add `--json` for automation or `--output npm-preinstall.json` to write the report to disk. `doctor attest <path>` creates a local attestation with stable package/report digests, validation/security/compatibility/trust summary, and verification metadata. Add `--sign-key-env NAME` to attach a local HMAC-SHA256 signature without printing the secret, or `--json --output attestation.json` to write the artifact to disk. `doctor attest verify <attestation.json> --target <path> --sign-key-env NAME` recomputes the package fingerprint, report digest, and HMAC signature offline; verification intentionally treats `generatedAt`, `targetPath`, `verification`, and `signature.keyHint` as unsigned display metadata. `doctor runtime-plan <path>` creates a non-executing runtime plan that lists MCP server commands, safe probe methods, risk reasons, and a stable approval digest before any local server is started. Add `--markdown --output runtime-plan.md` to preserve a review-ready approval artifact with the execution boundary, checklist, servers, probes, and risk reasons. `check --runtime --require-runtime-approval --runtime-approval-digest <digest>` refuses to run runtime probes unless the current plan digest matches the approved digest. `doctor release-evidence <path> --sign-key-env NAME` creates one redacted release bundle with signed attestation, offline verification, corpus, performance, security, trust, package metadata, git release gates, and runtime approval status. Strict release evidence requires a clean tagged worktree; use `--allow-dirty` or `--allow-untagged` only for local rehearsal. `doctor release-evidence verify <evidence.json> --target <path> --sign-key-env NAME` verifies a shared release evidence artifact offline against an explicit package path; the artifact target path is treated as display metadata, not trusted input. `doctor release-evidence asset <path> --tag <tag> --output <evidence.json> --sign-key-env NAME` writes a signed release evidence file and prints the `gh release upload` command; add `--upload` to run the upload through GitHub CLI with `--clobber`. `doctor inspector <path>` builds a safe MCP Inspector launch command from a packaged `.mcp.json` file without starting the Inspector proxy automatically. Use `--server <name>` when the package contains multiple MCP server entries. `doctor diff --before <path> --after <path>` compares two package roots and reports new findings, resolved findings, trust score delta, and whether risk increased. `doctor recommend <path>` turns validation, security, and compatibility signals into a prioritized action plan with blocker, high, medium, and info actions. Add `--json` for automation or `--output recommendations.json` to write the report to disk. `doctor trust <path>` creates a local trust score from package lifecycle scripts, dependency specs, and MCP security findings. Use it before release when you want supply-chain risks summarized as one score. `doctor perf <path>` profiles the shared package analysis pipeline and reports per-stage durations for validation, config, security, compatibility, trust, recommendations, and total runtime. Add `--max-total-ms <ms>` or repeatable `--max-stage-ms stage=ms` to fail CI when a budget is exceeded. `doctor mcp <path>` exposes the generic MCP static health report under the doctor command family without starting local MCP servers. `doctor export --bundle <path>` creates a redacted operator handoff bundle that includes validation JSON, security scorecard data, compatibility matrix, recommendations, and trust score in one file. `doctor snapshot` creates a redacted diagnostics bundle with environment health, client config readiness, installed plugin metadata, and next commands. Add `--json` for machine-readable output or `--output doctor-snapshot.json` to write the bundle to disk. `doctor clients` reports local Codex, Claude Desktop, Cursor, Cline, and Windsurf config readiness. `doctor --update-check` compares the installed CLI version with the latest npm version and prints the upgrade command when a newer release is available.
286
291
 
287
292
  `audit --installed` runs a local ecosystem audit against every discovered Codex plugin in the installed plugin cache. Add `--security` to include security scorecards, `--compat` to include the all-client compatibility matrix, and `--json --output local-audit.json` when you want a shareable machine-readable report. Add `--cache` to reuse unchanged plugin results between runs; add `--changed` to only report plugins whose fingerprint changed since the last cached audit. Use `--cache-file path/to/audit-cache.json` when CI or scripted runs need an explicit cache location.
288
293
 
@@ -348,9 +353,9 @@ jobs:
348
353
  runs-on: ubuntu-latest
349
354
  steps:
350
355
  - uses: actions/checkout@v5
351
- - uses: Esquetta/CodexPluginDoctor@v1.1.0
356
+ - uses: Esquetta/CodexPluginDoctor@v1.3.0
352
357
  with:
353
- version: "1.1.0"
358
+ version: "1.3.0"
354
359
  path: .
355
360
  runtime: "true"
356
361
  policy: codex-publish
@@ -71,6 +71,12 @@ const publicSchemaDefinitions = [
71
71
  outputKind: "doctor.perf",
72
72
  required: ["schemaVersion", "kind", "generatedAt", "targetPath", "status", "exitCode", "summary", "stages", "thresholds"]
73
73
  },
74
+ {
75
+ id: "doctor.runtime.plan.json",
76
+ command: "codex-plugin-doctor doctor runtime-plan <path> --json",
77
+ outputKind: "doctor.runtime.plan",
78
+ required: ["schemaVersion", "kind", "generatedAt", "version", "targetPath", "status", "exitCode", "runtimeExecution", "digest", "summary", "servers", "findings"]
79
+ },
74
80
  {
75
81
  id: "doctor.export.bundle.json",
76
82
  command: "codex-plugin-doctor doctor export --bundle <path> --json",
@@ -93,7 +99,7 @@ const publicSchemaDefinitions = [
93
99
  id: "doctor.release.evidence.json",
94
100
  command: "codex-plugin-doctor doctor release-evidence <path> --json",
95
101
  outputKind: "doctor.release.evidence",
96
- required: ["schemaVersion", "kind", "generatedAt", "version", "targetPath", "status", "exitCode", "releaseReady", "summary", "package", "git", "releaseGates", "attestation", "attestationVerification", "corpus", "performance", "security", "trust", "evidenceSignature"]
102
+ required: ["schemaVersion", "kind", "generatedAt", "version", "targetPath", "status", "exitCode", "releaseReady", "summary", "package", "git", "releaseGates", "runtimeApproval", "attestation", "attestationVerification", "corpus", "performance", "security", "trust", "evidenceSignature"]
97
103
  },
98
104
  {
99
105
  id: "doctor.release.evidence.verification.json",
@@ -3,6 +3,7 @@ import { type DoctorPerformanceReport, type DoctorPerformanceThresholdOptions }
3
3
  import { type DoctorValidationCorpusReport } from "./validation-corpus.js";
4
4
  import { type SecurityAudit } from "../security/security-audit.js";
5
5
  import { type TrustScoreReport } from "../security/trust-score.js";
6
+ import { type RuntimeApprovalReport } from "./runtime-plan.js";
6
7
  import type { CompatibilityEnvironment } from "../compatibility/compatibility-matrix.js";
7
8
  import type { CheckResult } from "../domain/types.js";
8
9
  type EvidenceStatus = "pass" | "warn" | "fail";
@@ -38,6 +39,7 @@ export interface DoctorReleaseEvidenceReport {
38
39
  corpus: EvidenceStatus;
39
40
  performance: EvidenceStatus;
40
41
  releaseGates: EvidenceStatus;
42
+ runtimeApproval: EvidenceStatus;
41
43
  security: EvidenceStatus;
42
44
  trust: EvidenceStatus;
43
45
  };
@@ -49,6 +51,7 @@ export interface DoctorReleaseEvidenceReport {
49
51
  message: string;
50
52
  }>;
51
53
  };
54
+ runtimeApproval: RuntimeApprovalReport;
52
55
  package: DoctorReleaseEvidencePackageMetadata;
53
56
  git: DoctorReleaseEvidenceGitMetadata;
54
57
  attestation: DoctorAttestation;
@@ -104,6 +107,8 @@ export interface BuildDoctorReleaseEvidenceOptions {
104
107
  signingKeyEnv: string;
105
108
  allowDirty?: boolean;
106
109
  allowUntagged?: boolean;
110
+ requireRuntimeApproval?: boolean;
111
+ runtimeApprovalDigest?: string | null;
107
112
  environment?: CompatibilityEnvironment;
108
113
  runCheck?: (targetPath: string) => Promise<CheckResult>;
109
114
  performanceThresholds?: DoctorPerformanceThresholdOptions;
@@ -9,6 +9,7 @@ import { redactValue } from "./doctor-export-bundle.js";
9
9
  import { readJsonFile } from "./read-json-file.js";
10
10
  import { buildSecurityAudit } from "../security/security-audit.js";
11
11
  import { buildTrustScore } from "../security/trust-score.js";
12
+ import { buildDoctorRuntimePlan, evaluateRuntimeApproval, runtimeApprovalPassed } from "./runtime-plan.js";
12
13
  import { packageVersion } from "../version.js";
13
14
  const execFileAsync = promisify(execFile);
14
15
  function isPlainObject(value) {
@@ -102,6 +103,7 @@ function releaseReady(report) {
102
103
  report.corpus.summary.status === "pass" &&
103
104
  report.performance.status === "pass" &&
104
105
  report.releaseGates.status === "pass" &&
106
+ runtimeApprovalPassed(report.runtimeApproval) &&
105
107
  report.security.status !== "fail" &&
106
108
  report.trust.status !== "fail";
107
109
  }
@@ -149,6 +151,7 @@ function buildReleaseEvidenceSigningPayload(report) {
149
151
  package: report.package,
150
152
  git: report.git,
151
153
  releaseGates: report.releaseGates,
154
+ runtimeApproval: report.runtimeApproval,
152
155
  attestation: {
153
156
  version: report.attestation.version,
154
157
  subject: report.attestation.subject,
@@ -190,7 +193,7 @@ function signReleaseEvidence(report, signingKey, keyHint) {
190
193
  export async function buildDoctorReleaseEvidenceReport(targetPath, options) {
191
194
  const rootPath = path.resolve(targetPath);
192
195
  const security = await buildSecurityAudit(rootPath);
193
- const [attestation, corpus, performance, trust, packageMetadata, git] = await Promise.all([
196
+ const [attestation, corpus, performance, trust, packageMetadata, git, runtimePlan] = await Promise.all([
194
197
  buildDoctorAttestation(rootPath, {
195
198
  signingKey: options.signingKey,
196
199
  signingKeyHint: `env:${options.signingKeyEnv}`,
@@ -204,7 +207,8 @@ export async function buildDoctorReleaseEvidenceReport(targetPath, options) {
204
207
  }),
205
208
  buildTrustScore(rootPath, { securityAudit: security }),
206
209
  readPackageMetadata(rootPath),
207
- readGitMetadata(rootPath)
210
+ readGitMetadata(rootPath),
211
+ buildDoctorRuntimePlan(rootPath)
208
212
  ]);
209
213
  const normalizedPackageMetadata = {
210
214
  name: packageMetadata.name ?? attestation.subject.name,
@@ -216,6 +220,10 @@ export async function buildDoctorReleaseEvidenceReport(targetPath, options) {
216
220
  artifactPath: "inline:doctor.release-evidence.attestation"
217
221
  });
218
222
  const releaseGates = buildReleaseGateReport(git, options);
223
+ const runtimeApproval = evaluateRuntimeApproval(runtimePlan, {
224
+ required: options.requireRuntimeApproval ?? false,
225
+ approvedDigest: options.runtimeApprovalDigest
226
+ });
219
227
  const partialReport = {
220
228
  schemaVersion: "1.0.0",
221
229
  kind: "doctor.release.evidence",
@@ -228,12 +236,14 @@ export async function buildDoctorReleaseEvidenceReport(targetPath, options) {
228
236
  corpus: corpus.summary.status,
229
237
  performance: performance.status,
230
238
  releaseGates: releaseGates.status,
239
+ runtimeApproval: runtimeApprovalPassed(runtimeApproval) ? "pass" : "fail",
231
240
  security: toEvidenceStatus(security.status),
232
241
  trust: toEvidenceStatus(trust.status)
233
242
  },
234
243
  package: normalizedPackageMetadata,
235
244
  git,
236
245
  releaseGates,
246
+ runtimeApproval,
237
247
  attestation,
238
248
  attestationVerification,
239
249
  corpus,
@@ -447,6 +457,7 @@ export function renderDoctorReleaseEvidence(report) {
447
457
  `Corpus: ${report.summary.corpus.toUpperCase()}`,
448
458
  `Performance: ${report.summary.performance.toUpperCase()}`,
449
459
  `Release gates: ${report.summary.releaseGates.toUpperCase()}`,
460
+ `Runtime approval: ${report.summary.runtimeApproval.toUpperCase()} (${report.runtimeApproval.status})`,
450
461
  `Security: ${report.summary.security.toUpperCase()} (${report.security.score}/100)`,
451
462
  `Trust: ${report.summary.trust.toUpperCase()} (${report.trust.score}/100)`,
452
463
  `Git commit: ${report.git.commit ?? "unknown"}`,
@@ -0,0 +1,52 @@
1
+ import { type SecurityAudit } from "../security/security-audit.js";
2
+ import type { Finding } from "../domain/types.js";
3
+ type RuntimePlanStatus = "pass" | "warn" | "fail";
4
+ type RuntimePlanRiskLevel = "low" | "medium" | "high";
5
+ type RuntimePlanTransport = "stdio" | "http";
6
+ export interface RuntimePlanServer {
7
+ name: string;
8
+ transport: RuntimePlanTransport;
9
+ command: string | null;
10
+ args: string[];
11
+ cwd: string | null;
12
+ url: string | null;
13
+ probeMethods: string[];
14
+ riskLevel: RuntimePlanRiskLevel;
15
+ riskReasons: string[];
16
+ }
17
+ export interface DoctorRuntimePlan {
18
+ schemaVersion: "1.0.0";
19
+ kind: "doctor.runtime.plan";
20
+ generatedAt: string;
21
+ version: string;
22
+ targetPath: string;
23
+ status: RuntimePlanStatus;
24
+ exitCode: 0 | 1;
25
+ runtimeExecution: "not_started";
26
+ digest: string;
27
+ summary: {
28
+ serverCount: number;
29
+ executableServerCount: number;
30
+ highRiskServerCount: number;
31
+ findings: SecurityAudit["findingCounts"];
32
+ };
33
+ servers: RuntimePlanServer[];
34
+ findings: Finding[];
35
+ }
36
+ export interface RuntimeApprovalReport {
37
+ required: boolean;
38
+ status: "approved" | "missing" | "mismatch" | "not_required";
39
+ planDigest: string;
40
+ approvedDigest: string | null;
41
+ message: string;
42
+ }
43
+ export declare function buildDoctorRuntimePlan(targetPath: string, generatedAt?: string): Promise<DoctorRuntimePlan>;
44
+ export declare function evaluateRuntimeApproval(plan: DoctorRuntimePlan, options: {
45
+ required: boolean;
46
+ approvedDigest?: string | null;
47
+ }): RuntimeApprovalReport;
48
+ export declare function runtimeApprovalPassed(approval: RuntimeApprovalReport): boolean;
49
+ export declare function renderDoctorRuntimePlanJson(plan: DoctorRuntimePlan): string;
50
+ export declare function renderDoctorRuntimePlanMarkdown(plan: DoctorRuntimePlan): string;
51
+ export declare function renderDoctorRuntimePlan(plan: DoctorRuntimePlan): string;
52
+ export {};
@@ -0,0 +1,293 @@
1
+ import { createHash } from "node:crypto";
2
+ import path from "node:path";
3
+ import { packageVersion } from "../version.js";
4
+ import { discoverPackage } from "./discover-package.js";
5
+ import { readJsonFile } from "./read-json-file.js";
6
+ import { buildSecurityAudit } from "../security/security-audit.js";
7
+ function isPlainObject(value) {
8
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9
+ }
10
+ function stableStringify(value) {
11
+ if (Array.isArray(value)) {
12
+ return `[${value.map((item) => stableStringify(item)).join(",")}]`;
13
+ }
14
+ if (isPlainObject(value)) {
15
+ return `{${Object.keys(value)
16
+ .sort()
17
+ .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`)
18
+ .join(",")}}`;
19
+ }
20
+ return JSON.stringify(value);
21
+ }
22
+ function sha256(value) {
23
+ return `sha256:${createHash("sha256").update(value).digest("hex")}`;
24
+ }
25
+ function normalizeCwd(rootPath, cwd) {
26
+ if (typeof cwd !== "string") {
27
+ return ".";
28
+ }
29
+ const resolvedCwd = path.resolve(rootPath, cwd);
30
+ const relativeCwd = path.relative(rootPath, resolvedCwd).replace(/\\/g, "/");
31
+ return relativeCwd.length === 0 ? "." : relativeCwd;
32
+ }
33
+ function buildRisk(serverName, serverConfig, findings) {
34
+ const matchingFindings = findings.filter((finding) => finding.message.includes(`\`${serverName}\``));
35
+ const riskReasons = matchingFindings.map((finding) => finding.id);
36
+ const hasFail = matchingFindings.some((finding) => finding.severity === "fail");
37
+ const hasWarn = matchingFindings.some((finding) => finding.severity === "warn");
38
+ if (typeof serverConfig.command === "string") {
39
+ riskReasons.push("runtime.executes_local_command");
40
+ }
41
+ if (typeof serverConfig.url === "string") {
42
+ riskReasons.push("runtime.connects_remote_server");
43
+ }
44
+ return {
45
+ riskLevel: hasFail ? "high" : hasWarn ? "medium" : "low",
46
+ riskReasons: [...new Set(riskReasons)]
47
+ };
48
+ }
49
+ function planDigestPayload(plan) {
50
+ return {
51
+ schemaVersion: plan.schemaVersion,
52
+ kind: "doctor.runtime.plan.digest.v1",
53
+ version: plan.version,
54
+ status: plan.status,
55
+ summary: plan.summary,
56
+ servers: plan.servers,
57
+ findings: plan.findings.map((finding) => ({
58
+ id: finding.id,
59
+ severity: finding.severity,
60
+ message: finding.message
61
+ }))
62
+ };
63
+ }
64
+ function buildRuntimePlanDigest(plan) {
65
+ return sha256(stableStringify(planDigestPayload(plan)));
66
+ }
67
+ export async function buildDoctorRuntimePlan(targetPath, generatedAt = new Date().toISOString()) {
68
+ const rootPath = path.resolve(targetPath);
69
+ const discoveredPackage = await discoverPackage(rootPath);
70
+ const security = await buildSecurityAudit(rootPath);
71
+ if (!discoveredPackage?.manifest.mcpServers) {
72
+ const partialPlan = {
73
+ schemaVersion: "1.0.0",
74
+ kind: "doctor.runtime.plan",
75
+ version: packageVersion,
76
+ targetPath: rootPath,
77
+ status: security.status,
78
+ exitCode: (security.status === "fail" ? 1 : 0),
79
+ runtimeExecution: "not_started",
80
+ summary: {
81
+ serverCount: 0,
82
+ executableServerCount: 0,
83
+ highRiskServerCount: 0,
84
+ findings: security.findingCounts
85
+ },
86
+ servers: [],
87
+ findings: security.findings
88
+ };
89
+ return {
90
+ ...partialPlan,
91
+ generatedAt,
92
+ digest: buildRuntimePlanDigest(partialPlan)
93
+ };
94
+ }
95
+ let parsedConfig;
96
+ try {
97
+ parsedConfig = await readJsonFile(path.resolve(discoveredPackage.rootPath, discoveredPackage.manifest.mcpServers));
98
+ }
99
+ catch {
100
+ parsedConfig = {};
101
+ }
102
+ const serverEntries = isPlainObject(parsedConfig) && isPlainObject(parsedConfig.mcpServers)
103
+ ? Object.entries(parsedConfig.mcpServers)
104
+ : [];
105
+ const servers = serverEntries
106
+ .filter((entry) => isPlainObject(entry[1]))
107
+ .map(([serverName, serverConfig]) => {
108
+ const command = typeof serverConfig.command === "string" ? serverConfig.command : null;
109
+ const url = typeof serverConfig.url === "string" ? serverConfig.url : null;
110
+ const { riskLevel, riskReasons } = buildRisk(serverName, serverConfig, security.findings);
111
+ return {
112
+ name: serverName,
113
+ transport: command ? "stdio" : "http",
114
+ command,
115
+ args: Array.isArray(serverConfig.args)
116
+ ? serverConfig.args.filter((arg) => typeof arg === "string")
117
+ : [],
118
+ cwd: command ? normalizeCwd(discoveredPackage.rootPath, serverConfig.cwd) : null,
119
+ url,
120
+ probeMethods: command
121
+ ? [
122
+ "initialize",
123
+ "tools/list",
124
+ "tools/call:safe-only",
125
+ "resources/list",
126
+ "resources/read:first-resource-only",
127
+ "resources/templates/list",
128
+ "prompts/list",
129
+ "prompts/get:first-prompt-only"
130
+ ]
131
+ : [],
132
+ riskLevel,
133
+ riskReasons
134
+ };
135
+ });
136
+ const highRiskServerCount = servers.filter((server) => server.riskLevel === "high").length;
137
+ const partialPlan = {
138
+ schemaVersion: "1.0.0",
139
+ kind: "doctor.runtime.plan",
140
+ version: packageVersion,
141
+ targetPath: discoveredPackage.rootPath,
142
+ status: highRiskServerCount > 0
143
+ ? "fail"
144
+ : security.status === "warn"
145
+ ? "warn"
146
+ : "pass",
147
+ exitCode: (highRiskServerCount > 0 ? 1 : 0),
148
+ runtimeExecution: "not_started",
149
+ summary: {
150
+ serverCount: servers.length,
151
+ executableServerCount: servers.filter((server) => server.command).length,
152
+ highRiskServerCount,
153
+ findings: security.findingCounts
154
+ },
155
+ servers,
156
+ findings: security.findings
157
+ };
158
+ return {
159
+ ...partialPlan,
160
+ generatedAt,
161
+ digest: buildRuntimePlanDigest(partialPlan)
162
+ };
163
+ }
164
+ export function evaluateRuntimeApproval(plan, options) {
165
+ if (!options.required) {
166
+ return {
167
+ required: false,
168
+ status: "not_required",
169
+ planDigest: plan.digest,
170
+ approvedDigest: options.approvedDigest ?? null,
171
+ message: "Runtime approval was not required for this run."
172
+ };
173
+ }
174
+ if (!options.approvedDigest) {
175
+ return {
176
+ required: true,
177
+ status: "missing",
178
+ planDigest: plan.digest,
179
+ approvedDigest: null,
180
+ message: "Runtime approval was required, but no approved plan digest was provided."
181
+ };
182
+ }
183
+ if (options.approvedDigest !== plan.digest) {
184
+ return {
185
+ required: true,
186
+ status: "mismatch",
187
+ planDigest: plan.digest,
188
+ approvedDigest: options.approvedDigest,
189
+ message: "Runtime approval digest does not match the current runtime plan."
190
+ };
191
+ }
192
+ return {
193
+ required: true,
194
+ status: "approved",
195
+ planDigest: plan.digest,
196
+ approvedDigest: options.approvedDigest,
197
+ message: "Runtime approval digest matches the current runtime plan."
198
+ };
199
+ }
200
+ export function runtimeApprovalPassed(approval) {
201
+ return approval.status === "approved" || approval.status === "not_required";
202
+ }
203
+ export function renderDoctorRuntimePlanJson(plan) {
204
+ return JSON.stringify(plan, null, 2);
205
+ }
206
+ function markdownList(items, emptyValue) {
207
+ if (items.length === 0) {
208
+ return `- ${emptyValue}`;
209
+ }
210
+ return items.map((item) => `- ${item}`).join("\n");
211
+ }
212
+ function markdownEscape(value) {
213
+ return value.replace(/\|/g, "\\|").replace(/\n/g, " ");
214
+ }
215
+ export function renderDoctorRuntimePlanMarkdown(plan) {
216
+ const lines = [
217
+ "# Doctor Runtime Review Plan",
218
+ "",
219
+ "This artifact records the intended MCP runtime probe boundary before any package-local server is started.",
220
+ "",
221
+ "## Summary",
222
+ "",
223
+ `- Target: \`${plan.targetPath}\``,
224
+ `- Status: **${plan.status.toUpperCase()}**`,
225
+ `- Runtime execution: \`${plan.runtimeExecution}\``,
226
+ `- Approval digest: \`${plan.digest}\``,
227
+ `- Servers: ${plan.summary.serverCount}`,
228
+ `- Executable servers: ${plan.summary.executableServerCount}`,
229
+ `- High-risk servers: ${plan.summary.highRiskServerCount}`,
230
+ `- Findings: ${plan.summary.findings.fail} fail, ${plan.summary.findings.warn} warn, ${plan.summary.findings.total} total`,
231
+ "",
232
+ "## Execution Boundary",
233
+ "",
234
+ "- This plan is non-executing.",
235
+ "- Runtime probes require explicit operator approval before local MCP servers are started.",
236
+ "- The approval digest changes when command, args, cwd, probe methods, risk reasons, or findings change.",
237
+ "- Runtime approval is a review gate, not an OS, VM, or container sandbox.",
238
+ "",
239
+ "## Review Checklist",
240
+ "",
241
+ "- Confirm every command, argument, and working directory is expected.",
242
+ "- Confirm remote URLs and network expectations are acceptable for the task.",
243
+ "- Confirm high-risk findings are resolved or intentionally accepted before runtime probing.",
244
+ "- Use the approval digest with `check --runtime --require-runtime-approval --runtime-approval-digest <digest>`.",
245
+ "- Preserve this artifact with release evidence when runtime execution is part of the release gate."
246
+ ];
247
+ if (plan.servers.length === 0) {
248
+ lines.push("", "## Servers", "", "No MCP runtime servers found.");
249
+ }
250
+ else {
251
+ lines.push("", "## Servers", "", "| Risk | Name | Transport | Command or URL | Cwd |", "| --- | --- | --- | --- | --- |");
252
+ for (const server of plan.servers) {
253
+ lines.push(`| ${server.riskLevel.toUpperCase()} | ${markdownEscape(server.name)} | ${server.transport} | ${markdownEscape(server.command ?? server.url ?? "not executable by runtime probe")} | ${markdownEscape(server.cwd ?? "n/a")} |`);
254
+ }
255
+ for (const server of plan.servers) {
256
+ lines.push("", `### ${server.name}`, "", "**Probe methods**", "", markdownList(server.probeMethods, "none"), "", "**Risk reasons**", "", markdownList(server.riskReasons, "none"));
257
+ }
258
+ }
259
+ if (plan.findings.length > 0) {
260
+ lines.push("", "## Findings", "");
261
+ for (const finding of plan.findings) {
262
+ lines.push(`- **${finding.severity.toUpperCase()}** \`${finding.id}\`: ${finding.message}`);
263
+ }
264
+ }
265
+ return lines.join("\n");
266
+ }
267
+ export function renderDoctorRuntimePlan(plan) {
268
+ const lines = [
269
+ "Doctor Runtime Plan",
270
+ "===================",
271
+ `Target: ${plan.targetPath}`,
272
+ `Status: ${plan.status.toUpperCase()}`,
273
+ `Runtime execution: ${plan.runtimeExecution}`,
274
+ `Digest: ${plan.digest}`,
275
+ `Servers: ${plan.summary.serverCount}`,
276
+ `Executable servers: ${plan.summary.executableServerCount}`,
277
+ `High-risk servers: ${plan.summary.highRiskServerCount}`
278
+ ];
279
+ if (plan.servers.length === 0) {
280
+ lines.push("", "No MCP runtime servers found.");
281
+ return lines.join("\n");
282
+ }
283
+ lines.push("", "Servers", "-------");
284
+ for (const server of plan.servers) {
285
+ lines.push(`${server.riskLevel.toUpperCase()} ${server.name}`);
286
+ lines.push(` Transport: ${server.transport}`);
287
+ lines.push(` Command: ${server.command ?? server.url ?? "not executable by runtime probe"}`);
288
+ lines.push(` Cwd: ${server.cwd ?? "n/a"}`);
289
+ lines.push(` Probes: ${server.probeMethods.length > 0 ? server.probeMethods.join(", ") : "none"}`);
290
+ lines.push(` Risk reasons: ${server.riskReasons.length > 0 ? server.riskReasons.join(", ") : "none"}`);
291
+ }
292
+ return lines.join("\n");
293
+ }
package/dist/index.d.ts CHANGED
@@ -9,6 +9,7 @@ export { buildDoctorOutputContract, renderDoctorOutputContract, renderDoctorOutp
9
9
  export { buildDoctorValidationCorpusReport, renderDoctorValidationCorpusJson, renderDoctorValidationCorpusReport, type BuildDoctorValidationCorpusOptions, type DoctorValidationCorpusReport, type ValidationCorpusCaseDefinition, type ValidationCorpusCaseResult } from "./core/validation-corpus.js";
10
10
  export { buildDoctorExportBundleFromAnalysis, buildDoctorRecommendationsFromAnalysis, buildPackageAnalysis, type PackageAnalysis, type PackageAnalysisOptions, type PackageAnalysisStage, type PackageAnalysisTiming } from "./core/package-analysis.js";
11
11
  export { buildDoctorPerformanceReport, renderDoctorPerformanceReport, renderDoctorPerformanceReportJson, type BuildDoctorPerformanceReportOptions, type DoctorPerformanceReport, type DoctorPerformanceStage, type DoctorPerformanceStageName } from "./core/performance-report.js";
12
+ export { buildDoctorRuntimePlan, evaluateRuntimeApproval, renderDoctorRuntimePlan, renderDoctorRuntimePlanMarkdown, renderDoctorRuntimePlanJson, runtimeApprovalPassed, type DoctorRuntimePlan, type RuntimeApprovalReport, type RuntimePlanServer } from "./core/runtime-plan.js";
12
13
  export { buildDoctorReleaseEvidenceAssetReport, buildDoctorReleaseEvidenceReport, renderDoctorReleaseEvidenceAsset, renderDoctorReleaseEvidenceAssetJson, renderDoctorReleaseEvidence, renderDoctorReleaseEvidenceJson, renderDoctorReleaseEvidenceVerification, renderDoctorReleaseEvidenceVerificationJson, verifyDoctorReleaseEvidence, type BuildDoctorReleaseEvidenceOptions, type DoctorReleaseEvidenceAssetReport, type DoctorReleaseEvidenceGitMetadata, type DoctorReleaseEvidencePackageMetadata, type DoctorReleaseEvidenceReport, type DoctorReleaseEvidenceVerificationReport } from "./core/release-evidence.js";
13
14
  export { buildDoctorNpmPackageReport, renderDoctorNpmPackageReport, renderDoctorNpmPackageReportJson, type BuildDoctorNpmPackageReportOptions, type DoctorNpmPackageReport } from "./core/npm-package-doctor.js";
14
15
  export { buildDoctorRiskDiffReport, renderDoctorRiskDiffReport, renderDoctorRiskDiffReportJson, type BuildDoctorRiskDiffReportOptions, type DoctorRiskDiffReport, type RiskDiffFinding, type RiskFindingCategory } from "./core/risk-diff.js";
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ export { buildDoctorOutputContract, renderDoctorOutputContract, renderDoctorOutp
9
9
  export { buildDoctorValidationCorpusReport, renderDoctorValidationCorpusJson, renderDoctorValidationCorpusReport } from "./core/validation-corpus.js";
10
10
  export { buildDoctorExportBundleFromAnalysis, buildDoctorRecommendationsFromAnalysis, buildPackageAnalysis } from "./core/package-analysis.js";
11
11
  export { buildDoctorPerformanceReport, renderDoctorPerformanceReport, renderDoctorPerformanceReportJson } from "./core/performance-report.js";
12
+ export { buildDoctorRuntimePlan, evaluateRuntimeApproval, renderDoctorRuntimePlan, renderDoctorRuntimePlanMarkdown, renderDoctorRuntimePlanJson, runtimeApprovalPassed } from "./core/runtime-plan.js";
12
13
  export { buildDoctorReleaseEvidenceAssetReport, buildDoctorReleaseEvidenceReport, renderDoctorReleaseEvidenceAsset, renderDoctorReleaseEvidenceAssetJson, renderDoctorReleaseEvidence, renderDoctorReleaseEvidenceJson, renderDoctorReleaseEvidenceVerification, renderDoctorReleaseEvidenceVerificationJson, verifyDoctorReleaseEvidence } from "./core/release-evidence.js";
13
14
  export { buildDoctorNpmPackageReport, renderDoctorNpmPackageReport, renderDoctorNpmPackageReportJson } from "./core/npm-package-doctor.js";
14
15
  export { buildDoctorRiskDiffReport, renderDoctorRiskDiffReport, renderDoctorRiskDiffReportJson } from "./core/risk-diff.js";
package/dist/run-cli.js CHANGED
@@ -20,6 +20,7 @@ import { buildDoctorAttestation, renderDoctorAttestation, renderDoctorAttestatio
20
20
  import { buildDoctorOutputContract, renderDoctorOutputContract, renderDoctorOutputContractJson } from "./core/output-contract.js";
21
21
  import { buildDoctorValidationCorpusReport, renderDoctorValidationCorpusJson, renderDoctorValidationCorpusReport } from "./core/validation-corpus.js";
22
22
  import { buildDoctorPerformanceReport, renderDoctorPerformanceReport, renderDoctorPerformanceReportJson } from "./core/performance-report.js";
23
+ import { buildDoctorRuntimePlan, evaluateRuntimeApproval, renderDoctorRuntimePlan, renderDoctorRuntimePlanMarkdown, renderDoctorRuntimePlanJson, runtimeApprovalPassed } from "./core/runtime-plan.js";
23
24
  import { buildDoctorReleaseEvidenceAssetReport, buildDoctorReleaseEvidenceReport, renderDoctorReleaseEvidenceAsset, renderDoctorReleaseEvidenceAssetJson, renderDoctorReleaseEvidence, renderDoctorReleaseEvidenceJson, renderDoctorReleaseEvidenceVerification, renderDoctorReleaseEvidenceVerificationJson, verifyDoctorReleaseEvidence } from "./core/release-evidence.js";
24
25
  import { buildDoctorNpmPackageReport, renderDoctorNpmPackageReport, renderDoctorNpmPackageReportJson } from "./core/npm-package-doctor.js";
25
26
  import { buildDoctorRiskDiffReport, renderDoctorRiskDiffReport, renderDoctorRiskDiffReportJson } from "./core/risk-diff.js";
@@ -70,7 +71,7 @@ const defaultIo = {
70
71
  }
71
72
  };
72
73
  function printUsage(io) {
73
- 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] [--verbose-runtime] [--explain] [--no-animations] [--ascii]\n codex-plugin-doctor audit --installed [filter] [--policy codex-publish|mcp-strict|security] [--security] [--compat] [--json] [--output <path>] [--cache] [--changed]\n codex-plugin-doctor mcp <path> [--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 fix <path> (--dry-run|--interactive --backup|--apply --backup)\n codex-plugin-doctor history <history.jsonl> [--json] [--fail-on-regression]\n codex-plugin-doctor doctor [npm <package>|contract|corpus|attest <path> [--sign-key-env NAME]|attest verify <attestation.json> --target <path> --sign-key-env NAME|release-evidence <path> --sign-key-env NAME [--allow-dirty] [--allow-untagged]|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 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");
74
+ 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] [--require-runtime-approval --runtime-approval-digest <digest>] [--verbose-runtime] [--explain] [--no-animations] [--ascii]\n codex-plugin-doctor audit --installed [filter] [--policy codex-publish|mcp-strict|security] [--security] [--compat] [--json] [--output <path>] [--cache] [--changed]\n codex-plugin-doctor mcp <path> [--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 fix <path> (--dry-run|--interactive --backup|--apply --backup)\n codex-plugin-doctor history <history.jsonl> [--json] [--fail-on-regression]\n codex-plugin-doctor doctor [npm <package>|contract|corpus|runtime-plan <path> [--json|--markdown] [--output <path>]|attest <path> [--sign-key-env NAME]|attest verify <attestation.json> --target <path> --sign-key-env NAME|release-evidence <path> --sign-key-env NAME [--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 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");
74
75
  }
75
76
  const performanceStageNames = new Set([
76
77
  "validation",
@@ -351,6 +352,39 @@ export async function runCli(args, io = defaultIo, options = {}) {
351
352
  io.writeStdout(renderedReport);
352
353
  return report.exitCode;
353
354
  }
355
+ if (maybePath === "runtime-plan") {
356
+ const targetPath = remainingArgs[0] && !remainingArgs[0].startsWith("--")
357
+ ? remainingArgs[0]
358
+ : null;
359
+ const runtimePlanFlags = targetPath ? remainingArgs.slice(1) : remainingArgs;
360
+ const jsonOutput = runtimePlanFlags.includes("--json");
361
+ const markdownOutput = runtimePlanFlags.includes("--markdown");
362
+ const outputIndex = runtimePlanFlags.indexOf("--output");
363
+ const outputPath = outputIndex === -1 ? null : runtimePlanFlags[outputIndex + 1];
364
+ if (!targetPath) {
365
+ io.writeStderr("Missing target path for runtime plan.");
366
+ return 2;
367
+ }
368
+ if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
369
+ io.writeStderr("Missing path after --output.");
370
+ return 2;
371
+ }
372
+ if (jsonOutput && markdownOutput) {
373
+ io.writeStderr("Use either --json or --markdown, not both.");
374
+ return 2;
375
+ }
376
+ const plan = await buildDoctorRuntimePlan(targetPath);
377
+ const renderedPlan = jsonOutput
378
+ ? renderDoctorRuntimePlanJson(plan)
379
+ : markdownOutput
380
+ ? renderDoctorRuntimePlanMarkdown(plan)
381
+ : renderDoctorRuntimePlan(plan);
382
+ if (outputPath) {
383
+ await writeFile(outputPath, markdownOutput ? renderDoctorRuntimePlanMarkdown(plan) : renderDoctorRuntimePlanJson(plan), "utf8");
384
+ }
385
+ io.writeStdout(renderedPlan);
386
+ return plan.exitCode;
387
+ }
354
388
  if (maybePath === "release-evidence") {
355
389
  if (remainingArgs[0] === "asset") {
356
390
  const targetPath = remainingArgs[1] && !remainingArgs[1].startsWith("--")
@@ -368,6 +402,11 @@ export async function runCli(args, io = defaultIo, options = {}) {
368
402
  const signKeyEnv = signKeyEnvIndex === -1 ? null : assetFlags[signKeyEnvIndex + 1];
369
403
  const allowDirty = assetFlags.includes("--allow-dirty");
370
404
  const allowUntagged = assetFlags.includes("--allow-untagged");
405
+ const requireRuntimeApproval = assetFlags.includes("--require-runtime-approval");
406
+ const runtimeApprovalDigestIndex = assetFlags.indexOf("--runtime-approval-digest");
407
+ const runtimeApprovalDigest = runtimeApprovalDigestIndex === -1
408
+ ? null
409
+ : assetFlags[runtimeApprovalDigestIndex + 1];
371
410
  if (!targetPath) {
372
411
  io.writeStderr("Missing target path for release evidence asset.");
373
412
  return 2;
@@ -400,6 +439,11 @@ export async function runCli(args, io = defaultIo, options = {}) {
400
439
  io.writeStderr("Missing environment variable name after --sign-key-env.");
401
440
  return 2;
402
441
  }
442
+ if (runtimeApprovalDigestIndex !== -1 &&
443
+ (!runtimeApprovalDigest || runtimeApprovalDigest.startsWith("--"))) {
444
+ io.writeStderr("Missing digest after --runtime-approval-digest.");
445
+ return 2;
446
+ }
403
447
  const signingKey = terminalContext.env[signKeyEnv];
404
448
  if (!signingKey) {
405
449
  io.writeStderr(`Environment variable ${signKeyEnv} is not set.`);
@@ -416,6 +460,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
416
460
  signingKeyEnv: signKeyEnv,
417
461
  allowDirty,
418
462
  allowUntagged,
463
+ requireRuntimeApproval,
464
+ runtimeApprovalDigest,
419
465
  environment: {
420
466
  env: terminalContext.env,
421
467
  platform: terminalContext.platform
@@ -516,6 +562,11 @@ export async function runCli(args, io = defaultIo, options = {}) {
516
562
  const signKeyEnv = signKeyEnvIndex === -1 ? null : evidenceFlags[signKeyEnvIndex + 1];
517
563
  const allowDirty = evidenceFlags.includes("--allow-dirty");
518
564
  const allowUntagged = evidenceFlags.includes("--allow-untagged");
565
+ const requireRuntimeApproval = evidenceFlags.includes("--require-runtime-approval");
566
+ const runtimeApprovalDigestIndex = evidenceFlags.indexOf("--runtime-approval-digest");
567
+ const runtimeApprovalDigest = runtimeApprovalDigestIndex === -1
568
+ ? null
569
+ : evidenceFlags[runtimeApprovalDigestIndex + 1];
519
570
  if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
520
571
  io.writeStderr("Missing path after --output.");
521
572
  return 2;
@@ -532,6 +583,11 @@ export async function runCli(args, io = defaultIo, options = {}) {
532
583
  io.writeStderr("Missing environment variable name after --sign-key-env.");
533
584
  return 2;
534
585
  }
586
+ if (runtimeApprovalDigestIndex !== -1 &&
587
+ (!runtimeApprovalDigest || runtimeApprovalDigest.startsWith("--"))) {
588
+ io.writeStderr("Missing digest after --runtime-approval-digest.");
589
+ return 2;
590
+ }
535
591
  const signingKey = terminalContext.env[signKeyEnv];
536
592
  if (!signingKey) {
537
593
  io.writeStderr(`Environment variable ${signKeyEnv} is not set.`);
@@ -547,6 +603,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
547
603
  signingKeyEnv: signKeyEnv,
548
604
  allowDirty,
549
605
  allowUntagged,
606
+ requireRuntimeApproval,
607
+ runtimeApprovalDigest,
550
608
  environment: {
551
609
  env: terminalContext.env,
552
610
  platform: terminalContext.platform
@@ -1328,6 +1386,11 @@ export async function runCli(args, io = defaultIo, options = {}) {
1328
1386
  const policy = parsePolicyPack(policyName);
1329
1387
  const historyIndex = normalizedFlags.indexOf("--history");
1330
1388
  const historyPath = historyIndex === -1 ? null : normalizedFlags[historyIndex + 1];
1389
+ const requireRuntimeApproval = normalizedFlags.includes("--require-runtime-approval");
1390
+ const runtimeApprovalDigestIndex = normalizedFlags.indexOf("--runtime-approval-digest");
1391
+ const runtimeApprovalDigest = runtimeApprovalDigestIndex === -1
1392
+ ? null
1393
+ : normalizedFlags[runtimeApprovalDigestIndex + 1];
1331
1394
  if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
1332
1395
  io.writeStderr("Missing path after --output.");
1333
1396
  return 2;
@@ -1356,6 +1419,11 @@ export async function runCli(args, io = defaultIo, options = {}) {
1356
1419
  io.writeStderr("Missing path after --history.");
1357
1420
  return 2;
1358
1421
  }
1422
+ if (runtimeApprovalDigestIndex !== -1 &&
1423
+ (!runtimeApprovalDigest || runtimeApprovalDigest.startsWith("--"))) {
1424
+ io.writeStderr("Missing digest after --runtime-approval-digest.");
1425
+ return 2;
1426
+ }
1359
1427
  if (checkInstalled && (badgeJsonOutput || badgeMarkdownOutput)) {
1360
1428
  io.writeStderr("Badge output requires a single package target.");
1361
1429
  return 2;
@@ -1367,6 +1435,14 @@ export async function runCli(args, io = defaultIo, options = {}) {
1367
1435
  const effectiveRuntimeProbeEnabled = runtimeProbeEnabled ||
1368
1436
  checkProfile === "publish" ||
1369
1437
  policyEnablesRuntime(policy);
1438
+ if (requireRuntimeApproval && !effectiveRuntimeProbeEnabled) {
1439
+ io.writeStderr("Runtime approval requires runtime probing. Add --runtime, --profile publish, or a runtime-enabled policy.");
1440
+ return 2;
1441
+ }
1442
+ if (checkInstalled && requireRuntimeApproval) {
1443
+ io.writeStderr("Runtime approval gating requires a single package target, not --installed.");
1444
+ return 2;
1445
+ }
1370
1446
  const outputPolicy = determineOutputPolicy({
1371
1447
  jsonOutput: jsonOutput || badgeJsonOutput,
1372
1448
  markdownOutput: markdownOutput || badgeMarkdownOutput,
@@ -1378,6 +1454,17 @@ export async function runCli(args, io = defaultIo, options = {}) {
1378
1454
  env: terminalContext.env
1379
1455
  });
1380
1456
  const runCheckImpl = options.runCheckImpl ?? runCheck;
1457
+ if (!checkInstalled && effectiveRuntimeProbeEnabled && requireRuntimeApproval) {
1458
+ const runtimePlan = await buildDoctorRuntimePlan(targetPath);
1459
+ const approval = evaluateRuntimeApproval(runtimePlan, {
1460
+ required: true,
1461
+ approvedDigest: runtimeApprovalDigest
1462
+ });
1463
+ if (!runtimeApprovalPassed(approval)) {
1464
+ io.writeStderr(`${approval.message}\nCurrent runtime plan digest: ${runtimePlan.digest}`);
1465
+ return 1;
1466
+ }
1467
+ }
1381
1468
  if (checkInstalled) {
1382
1469
  const installedPlugins = filterInstalledPlugins(await discoverInstalledPlugins({ env: terminalContext.env }), installedFilter);
1383
1470
  if (installedPlugins.length === 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-plugin-doctor",
3
- "version": "1.1.0",
3
+ "version": "1.3.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",