codex-plugin-doctor 1.46.0 → 1.47.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.
@@ -0,0 +1,68 @@
1
+ import path from "node:path";
2
+ export class RuntimeSandboxError extends Error {
3
+ }
4
+ export const DOCKER_RUNTIME_STARTUP_TIMEOUT_MS = 60_000;
5
+ export const DOCKER_RUNTIME_IMAGE = "node:22-bookworm-slim@sha256:6c74791e557ce11fc957704f6d4fe134a7bc8d6f5ca4403205b2966bd488f6b3";
6
+ export function buildRuntimeLaunch(input) {
7
+ if (input.sandbox !== "docker") {
8
+ throw new RuntimeSandboxError("Unsupported sandbox mode.");
9
+ }
10
+ if (!/^(node|node\.exe)$/i.test(path.basename(input.command))) {
11
+ throw new RuntimeSandboxError("Docker sandbox supports Node.js stdio commands only.");
12
+ }
13
+ const relativeCwd = path.relative(input.packageRoot, input.cwd);
14
+ if (relativeCwd === ".." ||
15
+ relativeCwd.startsWith(`..${path.sep}`) ||
16
+ path.isAbsolute(relativeCwd)) {
17
+ throw new RuntimeSandboxError("Runtime cwd escapes the package root.");
18
+ }
19
+ if (input.packageRoot.includes(",")) {
20
+ throw new RuntimeSandboxError("Docker sandbox does not support commas in package paths.");
21
+ }
22
+ const workdir = relativeCwd
23
+ ? `/workspace/${relativeCwd.split(path.sep).join("/")}`
24
+ : "/workspace";
25
+ return {
26
+ command: "docker",
27
+ args: [
28
+ "run",
29
+ "--rm",
30
+ "-i",
31
+ "--init",
32
+ "--name",
33
+ input.containerName,
34
+ "--network",
35
+ "none",
36
+ "--read-only",
37
+ "--cap-drop",
38
+ "ALL",
39
+ "--security-opt",
40
+ "no-new-privileges",
41
+ "--user",
42
+ "65532:65532",
43
+ "--pids-limit",
44
+ "64",
45
+ "--memory",
46
+ "256m",
47
+ "--cpus",
48
+ "1",
49
+ "--tmpfs",
50
+ "/tmp:rw,noexec,nosuid,size=16m",
51
+ "--mount",
52
+ `type=bind,source=${input.packageRoot},target=/workspace,readonly`,
53
+ "--workdir",
54
+ workdir,
55
+ DOCKER_RUNTIME_IMAGE,
56
+ "node",
57
+ ...input.args
58
+ ],
59
+ cwd: input.packageRoot,
60
+ containerName: input.containerName,
61
+ evidence: {
62
+ backend: "docker",
63
+ image: DOCKER_RUNTIME_IMAGE,
64
+ network: "none",
65
+ packageMount: "read_only"
66
+ }
67
+ };
68
+ }
@@ -404,18 +404,23 @@ export async function validatePlugin(targetPath, options = {}) {
404
404
  findings: withFindingFingerprints([await buildMissingManifestFailure(rootPath)], rootPath)
405
405
  };
406
406
  }
407
- const runtimeResult = options.runtime
407
+ const staticFindings = [
408
+ ...validateRequiredManifestFields(discoveredPackage),
409
+ ...(await validateSkillsDirectory(discoveredPackage)),
410
+ ...(await validateSkillDefinitions(discoveredPackage)),
411
+ ...(await validateMcpConfig(discoveredPackage))
412
+ ];
413
+ const staticFailed = staticFindings.some((finding) => finding.severity === "fail");
414
+ const runtimeResult = options.runtime && !staticFailed
408
415
  ? await probeRuntime(discoveredPackage, {
409
416
  startupTimeoutMs: options.runtimeStartupTimeoutMs,
417
+ sandbox: options.runtimeSandbox,
410
418
  transcript: options.runtimeTranscript
411
419
  })
412
420
  : null;
413
421
  const findings = [
414
- ...validateRequiredManifestFields(discoveredPackage),
415
- ...(await validateSkillsDirectory(discoveredPackage)),
416
- ...(await validateSkillDefinitions(discoveredPackage)),
417
- ...(await validateMcpConfig(discoveredPackage)),
418
- ...(runtimeResult ? runtimeResult.findings : [])
422
+ ...staticFindings,
423
+ ...(runtimeResult?.findings ?? [])
419
424
  ];
420
425
  const fingerprintedFindings = withFindingFingerprints(findings, discoveredPackage.rootPath);
421
426
  const hasFailures = fingerprintedFindings.some((finding) => finding.severity === "fail");
@@ -426,7 +431,10 @@ export async function validatePlugin(targetPath, options = {}) {
426
431
  status: "pass",
427
432
  exitCode: 0,
428
433
  findings: [],
429
- ...(runtimeResult ? { runtimeScorecard: runtimeResult.scorecard } : {})
434
+ ...(runtimeResult ? { runtimeScorecard: runtimeResult.scorecard } : {}),
435
+ ...(runtimeResult?.execution
436
+ ? { runtimeExecution: runtimeResult.execution }
437
+ : {})
430
438
  };
431
439
  }
432
440
  return {
@@ -434,6 +442,9 @@ export async function validatePlugin(targetPath, options = {}) {
434
442
  status: hasFailures ? "fail" : "warn",
435
443
  exitCode: hasFailures ? 1 : 0,
436
444
  findings: fingerprintedFindings,
437
- ...(runtimeResult ? { runtimeScorecard: runtimeResult.scorecard } : {})
445
+ ...(runtimeResult ? { runtimeScorecard: runtimeResult.scorecard } : {}),
446
+ ...(runtimeResult?.execution
447
+ ? { runtimeExecution: runtimeResult.execution }
448
+ : {})
438
449
  };
439
450
  }
@@ -1,4 +1,5 @@
1
1
  import { type PackageAnalysisOptions } from "./package-analysis.js";
2
+ import type { ExternalValidationCorpusReport } from "./external-validation-corpus.js";
2
3
  type ValidationStatus = "pass" | "warn" | "fail";
3
4
  export interface ValidationCorpusCaseDefinition {
4
5
  id: string;
@@ -36,6 +37,7 @@ export interface ValidationCorpusCaseResult {
36
37
  export interface DoctorValidationCorpusReport {
37
38
  schemaVersion: "1.0.0";
38
39
  kind: "doctor.validation.corpus";
40
+ corpusType?: "bundled";
39
41
  generatedAt: string;
40
42
  version: string;
41
43
  summary: {
@@ -51,8 +53,8 @@ export interface BuildDoctorValidationCorpusOptions {
51
53
  environment?: PackageAnalysisOptions["environment"];
52
54
  }
53
55
  export declare function buildDoctorValidationCorpusReport(options?: BuildDoctorValidationCorpusOptions): Promise<DoctorValidationCorpusReport>;
54
- export declare function renderDoctorValidationCorpusJson(report: DoctorValidationCorpusReport): string;
55
- export declare function renderDoctorValidationCorpusReport(report: DoctorValidationCorpusReport, options?: {
56
+ export declare function renderDoctorValidationCorpusJson(report: DoctorValidationCorpusReport | ExternalValidationCorpusReport): string;
57
+ export declare function renderDoctorValidationCorpusReport(report: DoctorValidationCorpusReport | ExternalValidationCorpusReport, options?: {
56
58
  outputPath?: string | null;
57
59
  }): string;
58
60
  export {};
@@ -163,8 +163,19 @@ export function renderDoctorValidationCorpusReport(report, options = {}) {
163
163
  }
164
164
  lines.push("", "Cases", "-----");
165
165
  for (const caseResult of report.cases) {
166
+ const actual = caseResult.actual;
167
+ let status;
168
+ let findingCount;
169
+ if ("status" in actual) {
170
+ status = actual.status;
171
+ findingCount = actual.findings.length;
172
+ }
173
+ else {
174
+ status = actual.validationStatus;
175
+ findingCount = actual.findingIds.length;
176
+ }
166
177
  lines.push(`${caseResult.id}: ${caseResult.expectationMatched ? "PASS" : "FAIL"} ` +
167
- `(${caseResult.actual.validationStatus.toUpperCase()}, ${caseResult.actual.findingIds.length} findings)`);
178
+ `(${status.toUpperCase()}, ${findingCount} findings)`);
168
179
  }
169
180
  return lines.join("\n");
170
181
  }
@@ -30,6 +30,7 @@ export interface CheckResult {
30
30
  suppressedFindings?: SuppressedFinding[];
31
31
  suppressionSummary?: SuppressionSummary;
32
32
  runtimeScorecard?: RuntimeScorecard;
33
+ runtimeExecution?: RuntimeExecutionEvidence;
33
34
  baselinedFindings?: Finding[];
34
35
  baselineSummary?: BaselineSummary;
35
36
  }
@@ -42,6 +43,7 @@ export interface CheckOptions {
42
43
  runtime?: boolean;
43
44
  runtimeTranscript?: (line: string) => void;
44
45
  runtimeStartupTimeoutMs?: number;
46
+ runtimeSandbox?: RuntimeSandboxMode;
45
47
  }
46
48
  export interface PluginManifest {
47
49
  name?: string;
@@ -61,6 +63,7 @@ export interface JsonReportSummary {
61
63
  exitCode: 0 | 1;
62
64
  runtimeProbeEnabled: boolean;
63
65
  runtimeScorecard?: RuntimeScorecard;
66
+ runtimeExecution?: RuntimeExecutionEvidence;
64
67
  findingCounts: {
65
68
  fail: number;
66
69
  warn: number;
@@ -91,4 +94,12 @@ export interface RuntimeScorecard {
91
94
  export interface RuntimeProbeResult {
92
95
  findings: Finding[];
93
96
  scorecard: RuntimeScorecard;
97
+ execution?: RuntimeExecutionEvidence;
98
+ }
99
+ export type RuntimeSandboxMode = "docker";
100
+ export interface RuntimeExecutionEvidence {
101
+ backend: "native" | "docker";
102
+ image: string | null;
103
+ network: "host" | "none";
104
+ packageMount: "host" | "read_only";
94
105
  }
package/dist/index.d.ts CHANGED
@@ -10,6 +10,7 @@ export { buildDoctorSnapshot, renderDoctorSnapshot, renderDoctorSnapshotJson, ty
10
10
  export { buildDoctorRecommendations, renderDoctorRecommendations, renderDoctorRecommendationsJson, type DoctorRecommendationAction, type DoctorRecommendationsReport } from "./core/doctor-recommendations.js";
11
11
  export { buildDoctorExportBundle, renderDoctorExportBundle, renderDoctorExportBundleJson, type DoctorExportBundle } from "./core/doctor-export-bundle.js";
12
12
  export { buildDoctorAttestation, renderDoctorAttestation, renderDoctorAttestationJson, renderDoctorAttestationVerification, renderDoctorAttestationVerificationJson, verifyDoctorAttestation, type DoctorAttestation, type DoctorAttestationVerificationCheck, type DoctorAttestationVerificationReport, type Digest, type PackageFingerprint } from "./core/attestation.js";
13
+ export { buildExternalValidationCorpusReport, ExternalCorpusManifestError, loadExternalCorpusManifest, type BuildExternalValidationCorpusOptions, type ExternalCorpusCaseResult, type ExternalCorpusClassificationCounts, type ExternalCorpusFindingReview, type ExternalCorpusManifest, type ExternalCorpusMode, type ExternalCorpusProfile, type ExternalCorpusSourceType, type ExternalValidationCorpusReport, type FindingReviewClassification } from "./core/external-validation-corpus.js";
13
14
  export { buildDoctorOutputContract, renderDoctorOutputContract, renderDoctorOutputContractJson, type DoctorOutputContract, type OutputContractRule, type OutputContractSchema } from "./core/output-contract.js";
14
15
  export { buildDoctorValidationCorpusReport, renderDoctorValidationCorpusJson, renderDoctorValidationCorpusReport, type BuildDoctorValidationCorpusOptions, type DoctorValidationCorpusReport, type ValidationCorpusCaseDefinition, type ValidationCorpusCaseResult } from "./core/validation-corpus.js";
15
16
  export { buildDoctorExportBundleFromAnalysis, buildDoctorRecommendationsFromAnalysis, buildPackageAnalysis, type PackageAnalysis, type PackageAnalysisOptions, type PackageAnalysisStage, type PackageAnalysisTiming } from "./core/package-analysis.js";
@@ -29,5 +30,5 @@ export { buildDepAudit, buildDepAuditRecommendations, renderDepAudit, renderDepA
29
30
  export { initGitHooks, removeGitHooks, type InitGitHooksResult, type RemoveGitHooksResult } from "./core/init-git-hooks.js";
30
31
  export { generateCompletion, type CompletionShell } from "./core/shell-completion.js";
31
32
  export { validateConfigFile, renderConfigValidation, renderConfigValidationJson, type ConfigValidationFinding, type ConfigValidationReport } from "./core/config-validate.js";
32
- export { buildReleaseCheck, renderReleaseCheck, renderReleaseCheckJson, type ReleaseCheckReport } from "./core/release-check.js";
33
+ export { assertReleaseMetadataSync, buildReleaseCheck, buildReleaseMetadataCheck, renderReleaseCheck, renderReleaseCheckJson, type BuildReleaseCheckOptions, type ReleaseMetadataCheck, type ReleaseCheckReport } from "./core/release-check.js";
33
34
  export declare function runCheck(targetPath: string, options?: CheckOptions): Promise<CheckResult>;
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ export { buildDoctorSnapshot, renderDoctorSnapshot, renderDoctorSnapshotJson } f
10
10
  export { buildDoctorRecommendations, renderDoctorRecommendations, renderDoctorRecommendationsJson } from "./core/doctor-recommendations.js";
11
11
  export { buildDoctorExportBundle, renderDoctorExportBundle, renderDoctorExportBundleJson } from "./core/doctor-export-bundle.js";
12
12
  export { buildDoctorAttestation, renderDoctorAttestation, renderDoctorAttestationJson, renderDoctorAttestationVerification, renderDoctorAttestationVerificationJson, verifyDoctorAttestation } from "./core/attestation.js";
13
+ export { buildExternalValidationCorpusReport, ExternalCorpusManifestError, loadExternalCorpusManifest } from "./core/external-validation-corpus.js";
13
14
  export { buildDoctorOutputContract, renderDoctorOutputContract, renderDoctorOutputContractJson } from "./core/output-contract.js";
14
15
  export { buildDoctorValidationCorpusReport, renderDoctorValidationCorpusJson, renderDoctorValidationCorpusReport } from "./core/validation-corpus.js";
15
16
  export { buildDoctorExportBundleFromAnalysis, buildDoctorRecommendationsFromAnalysis, buildPackageAnalysis } from "./core/package-analysis.js";
@@ -29,7 +30,7 @@ export { buildDepAudit, buildDepAuditRecommendations, renderDepAudit, renderDepA
29
30
  export { initGitHooks, removeGitHooks } from "./core/init-git-hooks.js";
30
31
  export { generateCompletion } from "./core/shell-completion.js";
31
32
  export { validateConfigFile, renderConfigValidation, renderConfigValidationJson } from "./core/config-validate.js";
32
- export { buildReleaseCheck, renderReleaseCheck, renderReleaseCheckJson } from "./core/release-check.js";
33
+ export { assertReleaseMetadataSync, buildReleaseCheck, buildReleaseMetadataCheck, renderReleaseCheck, renderReleaseCheckJson } from "./core/release-check.js";
33
34
  export async function runCheck(targetPath, options = {}) {
34
35
  return validatePlugin(targetPath, options);
35
36
  }
@@ -12,6 +12,9 @@ export function buildJsonReport(result, options) {
12
12
  ...(result.runtimeScorecard
13
13
  ? { runtimeScorecard: result.runtimeScorecard }
14
14
  : {}),
15
+ ...(result.runtimeExecution
16
+ ? { runtimeExecution: result.runtimeExecution }
17
+ : {}),
15
18
  findingCounts: {
16
19
  fail: failCount,
17
20
  warn: warnCount,
@@ -23,6 +23,9 @@ export function buildMarkdownReport(result, options) {
23
23
  if (result.baselineSummary) {
24
24
  lines.push(`| Baseline New | ${result.baselineSummary.new} |`, `| Baseline Matched | ${result.baselineSummary.matched} |`, `| Baseline Resolved | ${result.baselineSummary.resolved} |`);
25
25
  }
26
+ if (result.runtimeExecution) {
27
+ lines.push("", `Runtime backend: ${result.runtimeExecution.backend.toUpperCase()}`, `Runtime isolation: network=${result.runtimeExecution.network}, package=${result.runtimeExecution.packageMount}`);
28
+ }
26
29
  if (result.findings.length === 0 && !result.suppressedFindings?.length) {
27
30
  lines.push("", result.baselineSummary ? "No new findings." : "No findings.");
28
31
  return lines.join("\n");
@@ -42,6 +42,9 @@ export function renderTextReport(result, options = {}) {
42
42
  if (result.baselineSummary) {
43
43
  lines.push(`Baseline: ${result.baselineSummary.new} new, ${result.baselineSummary.matched} matched, ${result.baselineSummary.resolved} resolved`);
44
44
  }
45
+ if (result.runtimeExecution) {
46
+ lines.push(`Runtime backend: ${result.runtimeExecution.backend.toUpperCase()}`, `Runtime isolation: network=${result.runtimeExecution.network}, package=${result.runtimeExecution.packageMount}`);
47
+ }
45
48
  if (result.findings.length === 0 && !result.suppressedFindings?.length) {
46
49
  if (result.runtimeScorecard) {
47
50
  lines.push("", "Runtime Scorecard", "----------------");
package/dist/run-cli.js CHANGED
@@ -20,6 +20,7 @@ import { buildDoctorExportBundle, renderDoctorExportBundle, renderDoctorExportBu
20
20
  import { buildDoctorAttestation, renderDoctorAttestation, renderDoctorAttestationJson, renderDoctorAttestationVerification, renderDoctorAttestationVerificationJson, verifyDoctorAttestation } from "./core/attestation.js";
21
21
  import { buildDoctorOutputContract, renderDoctorOutputContract, renderDoctorOutputContractJson } from "./core/output-contract.js";
22
22
  import { buildDoctorValidationCorpusReport, renderDoctorValidationCorpusJson, renderDoctorValidationCorpusReport } from "./core/validation-corpus.js";
23
+ import { buildExternalValidationCorpusReport, ExternalCorpusManifestError } from "./core/external-validation-corpus.js";
23
24
  import { buildDoctorPerformanceReport, renderDoctorPerformanceReport, renderDoctorPerformanceReportJson } from "./core/performance-report.js";
24
25
  import { buildDoctorRuntimePlan, evaluateRuntimeApproval, renderDoctorRuntimePlan, renderDoctorRuntimePlanMarkdown, renderDoctorRuntimePlanJson, runtimeApprovalPassed } from "./core/runtime-plan.js";
25
26
  import { buildDoctorRuntimePolicyReport, renderDoctorRuntimePolicy, renderDoctorRuntimePolicyJson } from "./core/runtime-policy.js";
@@ -81,8 +82,24 @@ const defaultIo = {
81
82
  }
82
83
  }
83
84
  };
85
+ class CliUsageError extends Error {
86
+ }
87
+ function parseRuntimeSandbox(flags, options = { requireRuntime: true }) {
88
+ const index = flags.indexOf("--sandbox");
89
+ if (index === -1) {
90
+ return null;
91
+ }
92
+ const value = flags[index + 1];
93
+ if (value !== "docker") {
94
+ throw new CliUsageError("Expected --sandbox docker.");
95
+ }
96
+ if (options.requireRuntime !== false && !flags.includes("--runtime")) {
97
+ throw new CliUsageError("--sandbox docker requires --runtime.");
98
+ }
99
+ return value;
100
+ }
84
101
  function printUsage(io) {
85
- 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] [--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> [--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|runtime-plan <path> [--json|--markdown] [--output <path>]|runtime-policy <path> [--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 [--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]\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");
102
+ 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> [--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>]|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");
86
103
  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]");
87
104
  io.writeStderr("Baseline gating: codex-plugin-doctor baseline create <path> --output <path> [--runtime]\n codex-plugin-doctor check <path> --baseline <path>");
88
105
  }
@@ -805,6 +822,17 @@ function parseSelectedFixActionIndexes(answer, actionCount) {
805
822
  }
806
823
  export async function runCli(args, io = defaultIo, options = {}) {
807
824
  const [command, maybePath, ...remainingArgs] = args;
825
+ const runtimeSandboxSupported = (command === "check" &&
826
+ !args.includes("--installed") &&
827
+ !args.includes("--changed-since")) ||
828
+ (command === "release" && maybePath === "check") ||
829
+ (command === "doctor" &&
830
+ (["runtime-plan", "runtime-policy"].includes(maybePath ?? "") ||
831
+ (maybePath === "release-evidence" && remainingArgs[0] !== "verify")));
832
+ if (args.includes("--sandbox") && !runtimeSandboxSupported) {
833
+ io.writeStderr("--sandbox is supported only by single-package check, release check, runtime-plan, runtime-policy, and release-evidence.");
834
+ return 2;
835
+ }
808
836
  if (command === "--version" || command === "-v" || command === "version") {
809
837
  io.writeStdout(packageVersion);
810
838
  return 0;
@@ -933,6 +961,14 @@ export async function runCli(args, io = defaultIo, options = {}) {
933
961
  const markdownOutput = runtimePlanFlags.includes("--markdown");
934
962
  const outputIndex = runtimePlanFlags.indexOf("--output");
935
963
  const outputPath = outputIndex === -1 ? null : runtimePlanFlags[outputIndex + 1];
964
+ let runtimeSandbox;
965
+ try {
966
+ runtimeSandbox = parseRuntimeSandbox(runtimePlanFlags, { requireRuntime: false });
967
+ }
968
+ catch (error) {
969
+ io.writeStderr(error.message);
970
+ return 2;
971
+ }
936
972
  if (!targetPath) {
937
973
  io.writeStderr("Missing target path for runtime plan.");
938
974
  return 2;
@@ -945,7 +981,7 @@ export async function runCli(args, io = defaultIo, options = {}) {
945
981
  io.writeStderr("Use either --json or --markdown, not both.");
946
982
  return 2;
947
983
  }
948
- const plan = await buildDoctorRuntimePlan(targetPath);
984
+ const plan = await buildDoctorRuntimePlan(targetPath, new Date().toISOString(), runtimeSandbox ? { sandbox: runtimeSandbox } : {});
949
985
  const renderedPlan = jsonOutput
950
986
  ? renderDoctorRuntimePlanJson(plan)
951
987
  : markdownOutput
@@ -965,6 +1001,14 @@ export async function runCli(args, io = defaultIo, options = {}) {
965
1001
  const jsonOutput = runtimePolicyFlags.includes("--json");
966
1002
  const outputIndex = runtimePolicyFlags.indexOf("--output");
967
1003
  const outputPath = outputIndex === -1 ? null : runtimePolicyFlags[outputIndex + 1];
1004
+ let runtimeSandbox;
1005
+ try {
1006
+ runtimeSandbox = parseRuntimeSandbox(runtimePolicyFlags, { requireRuntime: false });
1007
+ }
1008
+ catch (error) {
1009
+ io.writeStderr(error.message);
1010
+ return 2;
1011
+ }
968
1012
  if (!targetPath) {
969
1013
  io.writeStderr("Missing target path for runtime policy.");
970
1014
  return 2;
@@ -973,7 +1017,7 @@ export async function runCli(args, io = defaultIo, options = {}) {
973
1017
  io.writeStderr("Missing path after --output.");
974
1018
  return 2;
975
1019
  }
976
- const report = await buildDoctorRuntimePolicyReport(targetPath);
1020
+ const report = await buildDoctorRuntimePolicyReport(targetPath, new Date().toISOString(), runtimeSandbox ? { sandbox: runtimeSandbox } : {});
977
1021
  const renderedReport = jsonOutput
978
1022
  ? renderDoctorRuntimePolicyJson(report)
979
1023
  : renderDoctorRuntimePolicy(report);
@@ -1138,6 +1182,15 @@ export async function runCli(args, io = defaultIo, options = {}) {
1138
1182
  const runtimeApprovalDigest = runtimeApprovalDigestIndex === -1
1139
1183
  ? null
1140
1184
  : assetFlags[runtimeApprovalDigestIndex + 1];
1185
+ const runtime = assetFlags.includes("--runtime");
1186
+ let runtimeSandbox;
1187
+ try {
1188
+ runtimeSandbox = parseRuntimeSandbox(assetFlags);
1189
+ }
1190
+ catch (error) {
1191
+ io.writeStderr(error.message);
1192
+ return 2;
1193
+ }
1141
1194
  if (!targetPath) {
1142
1195
  io.writeStderr("Missing target path for release evidence asset.");
1143
1196
  return 2;
@@ -1193,13 +1246,13 @@ export async function runCli(args, io = defaultIo, options = {}) {
1193
1246
  allowUntagged,
1194
1247
  requireRuntimeApproval,
1195
1248
  runtimeApprovalDigest,
1249
+ runtime,
1250
+ ...(runtimeSandbox ? { sandbox: runtimeSandbox } : {}),
1196
1251
  environment: {
1197
1252
  env: terminalContext.env,
1198
1253
  platform: terminalContext.platform
1199
1254
  },
1200
- runCheck: options.runCheckImpl
1201
- ? (pathToCheck) => options.runCheckImpl(pathToCheck)
1202
- : undefined,
1255
+ runCheck: options.runCheckImpl ?? runCheck,
1203
1256
  performanceThresholds: parsedThresholds.thresholds
1204
1257
  });
1205
1258
  await writeFile(resolvedOutputPath, renderDoctorReleaseEvidenceJson(evidence), "utf8");
@@ -1298,6 +1351,15 @@ export async function runCli(args, io = defaultIo, options = {}) {
1298
1351
  const runtimeApprovalDigest = runtimeApprovalDigestIndex === -1
1299
1352
  ? null
1300
1353
  : evidenceFlags[runtimeApprovalDigestIndex + 1];
1354
+ const runtime = evidenceFlags.includes("--runtime");
1355
+ let runtimeSandbox;
1356
+ try {
1357
+ runtimeSandbox = parseRuntimeSandbox(evidenceFlags);
1358
+ }
1359
+ catch (error) {
1360
+ io.writeStderr(error.message);
1361
+ return 2;
1362
+ }
1301
1363
  if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
1302
1364
  io.writeStderr("Missing path after --output.");
1303
1365
  return 2;
@@ -1336,13 +1398,13 @@ export async function runCli(args, io = defaultIo, options = {}) {
1336
1398
  allowUntagged,
1337
1399
  requireRuntimeApproval,
1338
1400
  runtimeApprovalDigest,
1401
+ runtime,
1402
+ ...(runtimeSandbox ? { sandbox: runtimeSandbox } : {}),
1339
1403
  environment: {
1340
1404
  env: terminalContext.env,
1341
1405
  platform: terminalContext.platform
1342
1406
  },
1343
- runCheck: options.runCheckImpl
1344
- ? (pathToCheck) => options.runCheckImpl(pathToCheck)
1345
- : undefined,
1407
+ runCheck: options.runCheckImpl ?? runCheck,
1346
1408
  performanceThresholds: parsedThresholds.thresholds
1347
1409
  });
1348
1410
  const reportJson = renderDoctorReleaseEvidenceJson(report);
@@ -1357,16 +1419,39 @@ export async function runCli(args, io = defaultIo, options = {}) {
1357
1419
  const jsonOutput = remainingArgs.includes("--json");
1358
1420
  const outputIndex = remainingArgs.indexOf("--output");
1359
1421
  const outputPath = outputIndex === -1 ? null : remainingArgs[outputIndex + 1];
1422
+ const manifestIndex = remainingArgs.indexOf("--manifest");
1423
+ const manifestPath = manifestIndex === -1 ? null : remainingArgs[manifestIndex + 1];
1360
1424
  if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
1361
1425
  io.writeStderr("Missing path after --output.");
1362
1426
  return 2;
1363
1427
  }
1364
- const report = await buildDoctorValidationCorpusReport({
1365
- environment: {
1366
- env: terminalContext.env,
1367
- platform: terminalContext.platform
1428
+ if (manifestIndex !== -1 && (!manifestPath || manifestPath.startsWith("--"))) {
1429
+ io.writeStderr("Missing path after --manifest.");
1430
+ return 2;
1431
+ }
1432
+ let report;
1433
+ try {
1434
+ report = manifestPath
1435
+ ? await buildExternalValidationCorpusReport(manifestPath, {
1436
+ environment: {
1437
+ env: terminalContext.env,
1438
+ platform: terminalContext.platform
1439
+ }
1440
+ })
1441
+ : await buildDoctorValidationCorpusReport({
1442
+ environment: {
1443
+ env: terminalContext.env,
1444
+ platform: terminalContext.platform
1445
+ }
1446
+ });
1447
+ }
1448
+ catch (error) {
1449
+ if (error instanceof ExternalCorpusManifestError) {
1450
+ io.writeStderr(error.message);
1451
+ return 2;
1368
1452
  }
1369
- });
1453
+ throw error;
1454
+ }
1370
1455
  const reportJson = renderDoctorValidationCorpusJson(report);
1371
1456
  if (outputPath) {
1372
1457
  await writeFile(outputPath, reportJson, "utf8");
@@ -2253,7 +2338,7 @@ export async function runCli(args, io = defaultIo, options = {}) {
2253
2338
  if (command === "release") {
2254
2339
  const subCommand = maybePath;
2255
2340
  if (subCommand !== "check") {
2256
- io.writeStderr("Usage: codex-plugin-doctor release check <path> [--json]");
2341
+ io.writeStderr("Usage: codex-plugin-doctor release check <path> [--json] [--runtime] [--require-runtime-approval --runtime-approval-digest <digest>]");
2257
2342
  return 2;
2258
2343
  }
2259
2344
  const targetPath = remainingArgs[0] && !remainingArgs[0].startsWith("--") ? remainingArgs[0] : ".";
@@ -2261,9 +2346,46 @@ export async function runCli(args, io = defaultIo, options = {}) {
2261
2346
  ? remainingArgs
2262
2347
  : remainingArgs.slice(1);
2263
2348
  const jsonOutput = releaseFlags.includes("--json");
2349
+ const runtimeProbeEnabled = releaseFlags.includes("--runtime");
2350
+ let runtimeSandbox;
2351
+ try {
2352
+ runtimeSandbox = parseRuntimeSandbox(releaseFlags);
2353
+ }
2354
+ catch (error) {
2355
+ io.writeStderr(error.message);
2356
+ return 2;
2357
+ }
2358
+ const requireRuntimeApproval = releaseFlags.includes("--require-runtime-approval");
2359
+ const runtimeApprovalDigestIndex = releaseFlags.indexOf("--runtime-approval-digest");
2360
+ const runtimeApprovalDigest = runtimeApprovalDigestIndex === -1
2361
+ ? null
2362
+ : releaseFlags[runtimeApprovalDigestIndex + 1];
2363
+ if (requireRuntimeApproval && !runtimeProbeEnabled) {
2364
+ io.writeStderr("Runtime approval requires --runtime.");
2365
+ return 2;
2366
+ }
2367
+ if (runtimeApprovalDigestIndex !== -1 &&
2368
+ (!runtimeApprovalDigest || runtimeApprovalDigest.startsWith("--"))) {
2369
+ io.writeStderr("Missing digest after --runtime-approval-digest.");
2370
+ return 2;
2371
+ }
2372
+ if (requireRuntimeApproval) {
2373
+ const runtimePlan = await buildDoctorRuntimePlan(targetPath, new Date().toISOString(), runtimeSandbox ? { sandbox: runtimeSandbox } : {});
2374
+ const approval = evaluateRuntimeApproval(runtimePlan, {
2375
+ required: true,
2376
+ approvedDigest: runtimeApprovalDigest
2377
+ });
2378
+ if (!runtimeApprovalPassed(approval)) {
2379
+ io.writeStderr(`${approval.message}\nCurrent runtime plan digest: ${runtimePlan.digest}`);
2380
+ return 1;
2381
+ }
2382
+ }
2264
2383
  const report = await buildReleaseCheck(targetPath, {
2265
2384
  env: terminalContext.env,
2266
- platform: terminalContext.platform
2385
+ platform: terminalContext.platform,
2386
+ runtime: runtimeProbeEnabled,
2387
+ ...(runtimeSandbox ? { runtimeSandbox } : {}),
2388
+ runCheck: options.runCheckImpl
2267
2389
  });
2268
2390
  const output = jsonOutput
2269
2391
  ? renderReleaseCheckJson(report)
@@ -2303,6 +2425,14 @@ export async function runCli(args, io = defaultIo, options = {}) {
2303
2425
  : maybePath && maybePath.startsWith("--")
2304
2426
  ? [maybePath, ...remainingArgs]
2305
2427
  : remainingArgs;
2428
+ let runtimeSandbox;
2429
+ try {
2430
+ runtimeSandbox = parseRuntimeSandbox(normalizedFlags);
2431
+ }
2432
+ catch (error) {
2433
+ io.writeStderr(error.message);
2434
+ return 2;
2435
+ }
2306
2436
  const jsonOutput = normalizedFlags.includes("--json");
2307
2437
  const markdownOutput = normalizedFlags.includes("--markdown");
2308
2438
  const badgeJsonOutput = normalizedFlags.includes("--badge-json");
@@ -2430,7 +2560,7 @@ export async function runCli(args, io = defaultIo, options = {}) {
2430
2560
  });
2431
2561
  const runCheckImpl = options.runCheckImpl ?? runCheck;
2432
2562
  if (!checkInstalled && effectiveRuntimeProbeEnabled && requireRuntimeApproval) {
2433
- const runtimePlan = await buildDoctorRuntimePlan(targetPath);
2563
+ const runtimePlan = await buildDoctorRuntimePlan(targetPath, new Date().toISOString(), runtimeSandbox ? { sandbox: runtimeSandbox } : {});
2434
2564
  const approval = evaluateRuntimeApproval(runtimePlan, {
2435
2565
  required: true,
2436
2566
  approvedDigest: runtimeApprovalDigest
@@ -2552,9 +2682,10 @@ export async function runCli(args, io = defaultIo, options = {}) {
2552
2682
  const doctorConfig = applyPolicyToDoctorConfig(applyCheckProfile(await loadDoctorConfig(targetPath, configPath), checkProfile), policy);
2553
2683
  const configuredResult = applyDoctorConfig(await runCheckImpl(targetPath, {
2554
2684
  runtime: effectiveRuntimeProbeEnabled,
2555
- runtimeTranscript: effectiveRuntimeProbeEnabled && verboseRuntime
2556
- ? (line) => io.writeStderr(line)
2557
- : undefined
2685
+ ...(runtimeSandbox ? { runtimeSandbox } : {}),
2686
+ ...(effectiveRuntimeProbeEnabled && verboseRuntime
2687
+ ? { runtimeTranscript: (line) => io.writeStderr(line) }
2688
+ : {})
2558
2689
  }), doctorConfig);
2559
2690
  const result = baseline
2560
2691
  ? applyValidationBaseline(configuredResult, baseline, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-plugin-doctor",
3
- "version": "1.46.0",
3
+ "version": "1.47.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",