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.
@@ -1,9 +1,83 @@
1
+ import { readFile } from "node:fs/promises";
1
2
  import path from "node:path";
2
3
  import { validatePlugin } from "./validate-plugin.js";
3
4
  import { buildSecurityAudit } from "../security/security-audit.js";
4
5
  import { buildDepAudit } from "./dep-audit.js";
5
6
  import { buildCompatibilityMatrix } from "../compatibility/compatibility-matrix.js";
6
7
  import { buildTrustScore } from "../security/trust-score.js";
8
+ async function readOptionalFile(filePath) {
9
+ try {
10
+ return await readFile(filePath, "utf8");
11
+ }
12
+ catch (error) {
13
+ if (error.code === "ENOENT") {
14
+ return null;
15
+ }
16
+ throw error;
17
+ }
18
+ }
19
+ export async function buildReleaseMetadataCheck(targetPath) {
20
+ const packageContent = await readOptionalFile(path.join(targetPath, "package.json"));
21
+ if (packageContent === null) {
22
+ return {
23
+ status: "skipped",
24
+ packageVersion: null,
25
+ lockfileVersion: null,
26
+ changelogVersion: null,
27
+ findings: []
28
+ };
29
+ }
30
+ const findings = [];
31
+ let packageVersion = null;
32
+ let lockfileVersion = null;
33
+ try {
34
+ const packageJson = JSON.parse(packageContent);
35
+ packageVersion = typeof packageJson.version === "string" ? packageJson.version : null;
36
+ }
37
+ catch {
38
+ findings.push("package.json is not valid JSON.");
39
+ }
40
+ if (!packageVersion) {
41
+ findings.push("package.json must contain a version.");
42
+ }
43
+ const lockContent = await readOptionalFile(path.join(targetPath, "package-lock.json"));
44
+ if (lockContent !== null) {
45
+ try {
46
+ const lock = JSON.parse(lockContent);
47
+ const rootVersion = lock.packages?.[""]?.version ?? lock.version;
48
+ lockfileVersion = typeof rootVersion === "string" ? rootVersion : null;
49
+ }
50
+ catch {
51
+ findings.push("package-lock.json is not valid JSON.");
52
+ }
53
+ if (!lockfileVersion) {
54
+ findings.push("package-lock.json must contain the root package version.");
55
+ }
56
+ else if (packageVersion && lockfileVersion !== packageVersion) {
57
+ findings.push(`package-lock.json version ${lockfileVersion} does not match package.json ${packageVersion}.`);
58
+ }
59
+ }
60
+ const changelog = await readOptionalFile(path.join(targetPath, "CHANGELOG.md"));
61
+ const changelogVersion = changelog === null || !packageVersion
62
+ ? null
63
+ : new RegExp(`^## \\[${packageVersion.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\](?:\\s|$)`, "m").test(changelog);
64
+ if (changelogVersion === false) {
65
+ findings.push(`CHANGELOG.md does not contain a ${packageVersion} release section.`);
66
+ }
67
+ return {
68
+ status: findings.length === 0 ? "pass" : "fail",
69
+ packageVersion,
70
+ lockfileVersion,
71
+ changelogVersion,
72
+ findings
73
+ };
74
+ }
75
+ export async function assertReleaseMetadataSync(targetPath) {
76
+ const metadata = await buildReleaseMetadataCheck(path.resolve(targetPath));
77
+ if (metadata.status === "fail") {
78
+ throw new Error(`Release metadata is inconsistent: ${metadata.findings.join(" ")}`);
79
+ }
80
+ }
7
81
  function matrixOverallStatus(matrix) {
8
82
  const statuses = matrix.results.map((r) => r.status);
9
83
  const passCount = statuses.filter((s) => s === "pass").length;
@@ -29,11 +103,16 @@ export async function buildReleaseCheck(targetPath, options = {}) {
29
103
  const resolvedPath = path.resolve(targetPath);
30
104
  const env = options.env ?? process.env;
31
105
  const platform = options.platform ?? process.platform;
32
- const validationResult = await validatePlugin(resolvedPath, { runtime: true });
106
+ const runtimeProbeEnabled = options.runtime ?? false;
107
+ const validationResult = await (options.runCheck ?? validatePlugin)(resolvedPath, {
108
+ runtime: runtimeProbeEnabled,
109
+ ...(options.runtimeSandbox ? { runtimeSandbox: options.runtimeSandbox } : {})
110
+ });
33
111
  const securityResult = await buildSecurityAudit(resolvedPath);
34
112
  const depResult = await buildDepAudit(resolvedPath);
35
113
  const compatResult = await buildCompatibilityMatrix(resolvedPath, { env, platform });
36
114
  const trustResult = await buildTrustScore(resolvedPath);
115
+ const metadataResult = await buildReleaseMetadataCheck(resolvedPath);
37
116
  const checks = {
38
117
  validation: {
39
118
  status: validationResult.status,
@@ -56,16 +135,19 @@ export async function buildReleaseCheck(targetPath, options = {}) {
56
135
  trust: {
57
136
  status: trustResult.status,
58
137
  score: trustResult.score
59
- }
138
+ },
139
+ metadata: metadataResult
60
140
  };
61
141
  const ready = checks.validation.status === "pass" &&
62
142
  checks.security.status === "pass" &&
63
143
  checks.dependencies.status !== "fail" &&
64
144
  checks.compatibility.status !== "fail" &&
65
- checks.trust.status === "pass";
145
+ checks.trust.status === "pass" &&
146
+ checks.metadata.status !== "fail";
66
147
  return {
67
148
  targetPath: resolvedPath,
68
149
  status: ready ? "pass" : "fail",
150
+ runtimeProbeEnabled,
69
151
  checks,
70
152
  ready
71
153
  };
@@ -77,6 +159,7 @@ export function renderReleaseCheck(report) {
77
159
  "==================================",
78
160
  `Path: ${report.targetPath}`,
79
161
  `Ready: ${report.ready ? "YES" : "NO"}`,
162
+ `Runtime probe: ${report.runtimeProbeEnabled ? "enabled" : "disabled"}`,
80
163
  "",
81
164
  "Checks",
82
165
  "------",
@@ -85,6 +168,9 @@ export function renderReleaseCheck(report) {
85
168
  ` ${icon(report.checks.dependencies.status === "pass")} dependencies ${report.checks.dependencies.status} (${report.checks.dependencies.vulnerabilities} vulns)`,
86
169
  ` ${icon(report.checks.compatibility.status === "pass")} compatibility ${report.checks.compatibility.status} (score: ${report.checks.compatibility.score})`,
87
170
  ` ${icon(report.checks.trust.status === "pass")} trust ${report.checks.trust.status} (score: ${report.checks.trust.score})`,
171
+ ...(report.checks.metadata
172
+ ? [` ${icon(report.checks.metadata.status !== "fail")} metadata ${report.checks.metadata.status} (${report.checks.metadata.findings.length} findings)`]
173
+ : []),
88
174
  "",
89
175
  report.ready
90
176
  ? "All release checks passed. Ready to publish."
@@ -97,6 +183,7 @@ export function renderReleaseCheckJson(report) {
97
183
  schemaVersion: "1.0.0",
98
184
  targetPath: report.targetPath,
99
185
  status: report.status,
186
+ runtimeProbeEnabled: report.runtimeProbeEnabled ?? false,
100
187
  ready: report.ready,
101
188
  checks: report.checks
102
189
  }, null, 2);
@@ -5,7 +5,7 @@ import { type SecurityAudit } from "../security/security-audit.js";
5
5
  import { type TrustScoreReport } from "../security/trust-score.js";
6
6
  import { type RuntimeApprovalReport } from "./runtime-plan.js";
7
7
  import type { CompatibilityEnvironment } from "../compatibility/compatibility-matrix.js";
8
- import type { CheckResult } from "../domain/types.js";
8
+ import type { CheckOptions, CheckResult, RuntimeExecutionEvidence, RuntimeSandboxMode } from "../domain/types.js";
9
9
  type EvidenceStatus = "pass" | "warn" | "fail";
10
10
  export interface DoctorReleaseEvidenceSignature {
11
11
  status: "signed";
@@ -52,6 +52,7 @@ export interface DoctorReleaseEvidenceReport {
52
52
  }>;
53
53
  };
54
54
  runtimeApproval: RuntimeApprovalReport;
55
+ execution?: RuntimeExecutionEvidence;
55
56
  package: DoctorReleaseEvidencePackageMetadata;
56
57
  git: DoctorReleaseEvidenceGitMetadata;
57
58
  attestation: DoctorAttestation;
@@ -109,8 +110,10 @@ export interface BuildDoctorReleaseEvidenceOptions {
109
110
  allowUntagged?: boolean;
110
111
  requireRuntimeApproval?: boolean;
111
112
  runtimeApprovalDigest?: string | null;
113
+ runtime?: boolean;
114
+ sandbox?: RuntimeSandboxMode;
112
115
  environment?: CompatibilityEnvironment;
113
- runCheck?: (targetPath: string) => Promise<CheckResult>;
116
+ runCheck?: (targetPath: string, options?: CheckOptions) => Promise<CheckResult>;
114
117
  performanceThresholds?: DoctorPerformanceThresholdOptions;
115
118
  }
116
119
  export declare function buildDoctorReleaseEvidenceReport(targetPath: string, options: BuildDoctorReleaseEvidenceOptions): Promise<DoctorReleaseEvidenceReport>;
@@ -10,6 +10,7 @@ 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
12
  import { buildDoctorRuntimePlan, evaluateRuntimeApproval, runtimeApprovalPassed } from "./runtime-plan.js";
13
+ import { validatePlugin } from "./validate-plugin.js";
13
14
  import { packageVersion } from "../version.js";
14
15
  const execFileAsync = promisify(execFile);
15
16
  function isPlainObject(value) {
@@ -152,6 +153,7 @@ function buildReleaseEvidenceSigningPayload(report) {
152
153
  git: report.git,
153
154
  releaseGates: report.releaseGates,
154
155
  runtimeApproval: report.runtimeApproval,
156
+ ...(report.execution ? { execution: report.execution } : {}),
155
157
  attestation: {
156
158
  version: report.attestation.version,
157
159
  subject: report.attestation.subject,
@@ -193,6 +195,13 @@ function signReleaseEvidence(report, signingKey, keyHint) {
193
195
  export async function buildDoctorReleaseEvidenceReport(targetPath, options) {
194
196
  const rootPath = path.resolve(targetPath);
195
197
  const security = await buildSecurityAudit(rootPath);
198
+ const runCheck = options.runCheck ?? validatePlugin;
199
+ const checkOptions = options.runtime
200
+ ? {
201
+ runtime: true,
202
+ ...(options.sandbox ? { runtimeSandbox: options.sandbox } : {})
203
+ }
204
+ : {};
196
205
  const [attestation, corpus, performance, trust, packageMetadata, git, runtimePlan] = await Promise.all([
197
206
  buildDoctorAttestation(rootPath, {
198
207
  signingKey: options.signingKey,
@@ -202,13 +211,13 @@ export async function buildDoctorReleaseEvidenceReport(targetPath, options) {
202
211
  buildDoctorValidationCorpusReport({ environment: options.environment }),
203
212
  buildDoctorPerformanceReport(rootPath, {
204
213
  environment: options.environment,
205
- runCheck: options.runCheck,
214
+ runCheck: (pathToCheck) => runCheck(pathToCheck, checkOptions),
206
215
  thresholds: options.performanceThresholds
207
216
  }),
208
217
  buildTrustScore(rootPath, { securityAudit: security }),
209
218
  readPackageMetadata(rootPath),
210
219
  readGitMetadata(rootPath),
211
- buildDoctorRuntimePlan(rootPath)
220
+ buildDoctorRuntimePlan(rootPath, new Date().toISOString(), options.sandbox ? { sandbox: options.sandbox } : {})
212
221
  ]);
213
222
  const normalizedPackageMetadata = {
214
223
  name: packageMetadata.name ?? attestation.subject.name,
@@ -244,6 +253,7 @@ export async function buildDoctorReleaseEvidenceReport(targetPath, options) {
244
253
  git,
245
254
  releaseGates,
246
255
  runtimeApproval,
256
+ ...(options.runtime ? { execution: runtimePlan.execution } : {}),
247
257
  attestation,
248
258
  attestationVerification,
249
259
  corpus,
@@ -1,5 +1,6 @@
1
1
  import { type SecurityAudit } from "../security/security-audit.js";
2
2
  import type { Finding } from "../domain/types.js";
3
+ import type { RuntimeExecutionEvidence, RuntimeSandboxMode } from "../domain/types.js";
3
4
  type RuntimePlanStatus = "pass" | "warn" | "fail";
4
5
  type RuntimePlanRiskLevel = "low" | "medium" | "high";
5
6
  type RuntimePlanTransport = "stdio" | "http";
@@ -23,6 +24,7 @@ export interface DoctorRuntimePlan {
23
24
  status: RuntimePlanStatus;
24
25
  exitCode: 0 | 1;
25
26
  runtimeExecution: "not_started";
27
+ execution: RuntimeExecutionEvidence;
26
28
  digest: string;
27
29
  summary: {
28
30
  serverCount: number;
@@ -40,7 +42,9 @@ export interface RuntimeApprovalReport {
40
42
  approvedDigest: string | null;
41
43
  message: string;
42
44
  }
43
- export declare function buildDoctorRuntimePlan(targetPath: string, generatedAt?: string): Promise<DoctorRuntimePlan>;
45
+ export declare function buildDoctorRuntimePlan(targetPath: string, generatedAt?: string, options?: {
46
+ sandbox?: RuntimeSandboxMode;
47
+ }): Promise<DoctorRuntimePlan>;
44
48
  export declare function evaluateRuntimeApproval(plan: DoctorRuntimePlan, options: {
45
49
  required: boolean;
46
50
  approvedDigest?: string | null;
@@ -4,6 +4,7 @@ import { packageVersion } from "../version.js";
4
4
  import { discoverPackage } from "./discover-package.js";
5
5
  import { readJsonFile } from "./read-json-file.js";
6
6
  import { buildSecurityAudit } from "../security/security-audit.js";
7
+ import { DOCKER_RUNTIME_IMAGE } from "./runtime-sandbox.js";
7
8
  function isPlainObject(value) {
8
9
  return typeof value === "object" && value !== null && !Array.isArray(value);
9
10
  }
@@ -51,6 +52,7 @@ function planDigestPayload(plan) {
51
52
  schemaVersion: plan.schemaVersion,
52
53
  kind: "doctor.runtime.plan.digest.v1",
53
54
  version: plan.version,
55
+ execution: plan.execution,
54
56
  status: plan.status,
55
57
  summary: plan.summary,
56
58
  servers: plan.servers,
@@ -64,10 +66,18 @@ function planDigestPayload(plan) {
64
66
  function buildRuntimePlanDigest(plan) {
65
67
  return sha256(stableStringify(planDigestPayload(plan)));
66
68
  }
67
- export async function buildDoctorRuntimePlan(targetPath, generatedAt = new Date().toISOString()) {
69
+ export async function buildDoctorRuntimePlan(targetPath, generatedAt = new Date().toISOString(), options = {}) {
68
70
  const rootPath = path.resolve(targetPath);
69
71
  const discoveredPackage = await discoverPackage(rootPath);
70
72
  const security = await buildSecurityAudit(rootPath);
73
+ const execution = options.sandbox === "docker"
74
+ ? {
75
+ backend: "docker",
76
+ image: DOCKER_RUNTIME_IMAGE,
77
+ network: "none",
78
+ packageMount: "read_only"
79
+ }
80
+ : { backend: "native", image: null, network: "host", packageMount: "host" };
71
81
  if (!discoveredPackage?.manifest.mcpServers) {
72
82
  const partialPlan = {
73
83
  schemaVersion: "1.0.0",
@@ -77,6 +87,7 @@ export async function buildDoctorRuntimePlan(targetPath, generatedAt = new Date(
77
87
  status: security.status,
78
88
  exitCode: (security.status === "fail" ? 1 : 0),
79
89
  runtimeExecution: "not_started",
90
+ execution,
80
91
  summary: {
81
92
  serverCount: 0,
82
93
  executableServerCount: 0,
@@ -146,6 +157,7 @@ export async function buildDoctorRuntimePlan(targetPath, generatedAt = new Date(
146
157
  : "pass",
147
158
  exitCode: (highRiskServerCount > 0 ? 1 : 0),
148
159
  runtimeExecution: "not_started",
160
+ execution,
149
161
  summary: {
150
162
  serverCount: servers.length,
151
163
  executableServerCount: servers.filter((server) => server.command).length,
@@ -1,3 +1,4 @@
1
+ import type { RuntimeExecutionEvidence, RuntimeSandboxMode } from "../domain/types.js";
1
2
  import { type DoctorRuntimePlan, type RuntimePlanServer } from "./runtime-plan.js";
2
3
  export type RuntimePolicyDecision = "allow" | "review" | "sandbox_recommended" | "deny";
3
4
  export interface RuntimePolicyRecommendation {
@@ -14,6 +15,7 @@ export interface DoctorRuntimePolicyReport {
14
15
  status: "pass" | "warn" | "fail";
15
16
  exitCode: 0 | 1;
16
17
  runtimeExecution: "not_started";
18
+ execution: RuntimeExecutionEvidence;
17
19
  planDigest: string;
18
20
  recommendation: RuntimePolicyRecommendation;
19
21
  summary: {
@@ -29,6 +31,8 @@ export interface DoctorRuntimePolicyReport {
29
31
  reasons: string[];
30
32
  }>;
31
33
  }
32
- export declare function buildDoctorRuntimePolicyReport(targetPath: string, generatedAt?: string): Promise<DoctorRuntimePolicyReport>;
34
+ export declare function buildDoctorRuntimePolicyReport(targetPath: string, generatedAt?: string, options?: {
35
+ sandbox?: RuntimeSandboxMode;
36
+ }): Promise<DoctorRuntimePolicyReport>;
33
37
  export declare function renderDoctorRuntimePolicyJson(report: DoctorRuntimePolicyReport): string;
34
38
  export declare function renderDoctorRuntimePolicy(report: DoctorRuntimePolicyReport): string;
@@ -91,8 +91,8 @@ function buildRecommendation(plan, decision, reasons) {
91
91
  ]
92
92
  };
93
93
  }
94
- export async function buildDoctorRuntimePolicyReport(targetPath, generatedAt = new Date().toISOString()) {
95
- const plan = await buildDoctorRuntimePlan(targetPath, generatedAt);
94
+ export async function buildDoctorRuntimePolicyReport(targetPath, generatedAt = new Date().toISOString(), options = {}) {
95
+ const plan = await buildDoctorRuntimePlan(targetPath, generatedAt, options);
96
96
  const servers = plan.servers.map((server) => ({
97
97
  name: server.name,
98
98
  decision: serverDecision(server),
@@ -118,6 +118,7 @@ export async function buildDoctorRuntimePolicyReport(targetPath, generatedAt = n
118
118
  status,
119
119
  exitCode: status === "fail" ? 1 : 0,
120
120
  runtimeExecution: "not_started",
121
+ execution: plan.execution,
121
122
  planDigest: plan.digest,
122
123
  recommendation,
123
124
  summary: {
@@ -1,5 +1,6 @@
1
- import type { DiscoveredPackage, RuntimeProbeResult } from "../domain/types.js";
1
+ import type { DiscoveredPackage, RuntimeProbeResult, RuntimeSandboxMode } from "../domain/types.js";
2
2
  export declare function probeRuntime(discoveredPackage: DiscoveredPackage, options?: {
3
3
  startupTimeoutMs?: number;
4
+ sandbox?: RuntimeSandboxMode;
4
5
  transcript?: (line: string) => void;
5
6
  }): Promise<RuntimeProbeResult>;
@@ -1,7 +1,9 @@
1
- import { spawn } from "node:child_process";
1
+ import { execFile, spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
2
3
  import { readFile, stat } from "node:fs/promises";
3
4
  import path from "node:path";
4
5
  import readline from "node:readline";
6
+ import { buildRuntimeLaunch, DOCKER_RUNTIME_STARTUP_TIMEOUT_MS, RuntimeSandboxError } from "./runtime-sandbox.js";
5
7
  import { formatRequestTranscript as formatRequestTranscriptForLog, formatResponseTranscript as formatResponseTranscriptForLog } from "./runtime-transcript.js";
6
8
  const MCP_PROTOCOL_VERSION = "2025-11-25";
7
9
  const PROMPT_PROBE_PLACEHOLDER = "codex-plugin-doctor-probe";
@@ -9,6 +11,19 @@ const METHOD_NOT_FOUND = -32601;
9
11
  const MAX_TOOL_CALL_CONTENT_LENGTH = 4096;
10
12
  const MAX_RESOURCE_READ_CONTENT_LENGTH = 4096;
11
13
  const MAX_PROMPT_GET_CONTENT_LENGTH = 4096;
14
+ const DOCKER_CLEANUP_TIMEOUT_MS = 5_000;
15
+ function buildNativeRuntimeLaunch(input) {
16
+ return {
17
+ ...input,
18
+ containerName: null,
19
+ evidence: {
20
+ backend: "native",
21
+ image: null,
22
+ network: "host",
23
+ packageMount: "host"
24
+ }
25
+ };
26
+ }
12
27
  function createRuntimeScorecard() {
13
28
  return {
14
29
  initialize: "skipped",
@@ -72,6 +87,21 @@ function withRuntimeEvidence(finding, serverName) {
72
87
  }
73
88
  };
74
89
  }
90
+ function cleanupDockerContainer(containerName) {
91
+ return new Promise((resolve) => {
92
+ execFile("docker", ["rm", "-f", containerName], {
93
+ encoding: "utf8",
94
+ timeout: DOCKER_CLEANUP_TIMEOUT_MS,
95
+ windowsHide: true
96
+ }, (error, _stdout, stderr) => {
97
+ if (!error || /no such container/i.test(stderr)) {
98
+ resolve(null);
99
+ return;
100
+ }
101
+ resolve(buildFailure("plugin.runtime.sandbox.cleanup_failed", "The Docker runtime container could not be removed after probing.", "A leftover runtime container weakens cleanup guarantees and may continue consuming local resources.", "Verify the Docker daemon is available, then remove the codex-doctor container before retrying."));
102
+ });
103
+ });
104
+ }
75
105
  function isPlainObject(value) {
76
106
  return typeof value === "object" && value !== null && !Array.isArray(value);
77
107
  }
@@ -605,16 +635,42 @@ function collectOversizedPromptGetWarnings(message, evidence) {
605
635
  ];
606
636
  }
607
637
  async function probeCommandServer(input) {
608
- const { serverName, command, args, cwd, startupTimeoutMs, transcript } = input;
638
+ const { serverName, packageRoot, command, args, cwd, startupTimeoutMs, sandbox, transcript } = input;
639
+ let launch;
640
+ try {
641
+ launch = sandbox === "docker"
642
+ ? buildRuntimeLaunch({
643
+ sandbox,
644
+ packageRoot,
645
+ command,
646
+ args,
647
+ cwd,
648
+ containerName: `codex-doctor-${randomUUID()}`
649
+ })
650
+ : buildNativeRuntimeLaunch({ command, args, cwd });
651
+ }
652
+ catch (error) {
653
+ const scorecard = createRuntimeScorecard();
654
+ scorecard.initialize = "fail";
655
+ return {
656
+ findings: [
657
+ withRuntimeEvidence(buildFailure("plugin.runtime.startup.failed", `The MCP server \`${serverName}\` could not be started in the requested runtime backend.`, "The configured stdio server is not supported by the selected runtime backend, so runtime validation cannot proceed.", error instanceof RuntimeSandboxError
658
+ ? error.message
659
+ : "Verify the runtime command and selected sandbox before retrying."), serverName)
660
+ ],
661
+ scorecard
662
+ };
663
+ }
609
664
  return new Promise((resolve) => {
610
665
  const scorecard = createRuntimeScorecard();
611
666
  const warnings = [];
612
667
  let settled = false;
668
+ let runtimeExecuted = false;
613
669
  let stderrPreview = "";
614
670
  let finalizeRequested = false;
615
671
  let nextRequestId = 1;
616
- const child = spawn(command, args, {
617
- cwd,
672
+ const child = spawn(launch.command, launch.args, {
673
+ cwd: launch.cwd,
618
674
  stdio: ["pipe", "pipe", "pipe"]
619
675
  });
620
676
  const stdoutReader = readline.createInterface({
@@ -637,21 +693,33 @@ async function probeCommandServer(input) {
637
693
  child.stdin.end();
638
694
  child.kill("SIGTERM");
639
695
  }
640
- resolve({
641
- findings: [
642
- ...warnings.map((warning) => withRuntimeEvidence(warning, serverName)),
643
- ...(finding ? [withRuntimeEvidence(finding, serverName)] : [])
644
- ],
645
- scorecard
646
- });
696
+ void (async () => {
697
+ const cleanupFinding = launch.containerName && runtimeExecuted
698
+ ? await cleanupDockerContainer(launch.containerName)
699
+ : null;
700
+ resolve({
701
+ findings: [
702
+ ...warnings.map((warning) => withRuntimeEvidence(warning, serverName)),
703
+ ...(finding ? [withRuntimeEvidence(finding, serverName)] : []),
704
+ ...(cleanupFinding
705
+ ? [withRuntimeEvidence(cleanupFinding, serverName)]
706
+ : [])
707
+ ],
708
+ scorecard,
709
+ ...(runtimeExecuted ? { execution: launch.evidence } : {})
710
+ });
711
+ })();
647
712
  };
648
713
  const sendRequest = (method, params, timeoutFinding) => new Promise((requestResolve, requestReject) => {
649
714
  const id = nextRequestId++;
650
715
  transcript?.(formatRequestTranscriptForLog(method, params));
716
+ const requestTimeoutMs = launch.evidence.backend === "docker" && method === "initialize"
717
+ ? DOCKER_RUNTIME_STARTUP_TIMEOUT_MS
718
+ : startupTimeoutMs;
651
719
  const timer = setTimeout(() => {
652
720
  pendingRequests.delete(id);
653
721
  requestReject(timeoutFinding);
654
- }, startupTimeoutMs);
722
+ }, requestTimeoutMs);
655
723
  pendingRequests.set(id, {
656
724
  method,
657
725
  resolve: requestResolve,
@@ -701,6 +769,9 @@ async function probeCommandServer(input) {
701
769
  }
702
770
  stderrPreview += chunk.toString();
703
771
  });
772
+ child.on("spawn", () => {
773
+ runtimeExecuted = true;
774
+ });
704
775
  stdoutReader.on("line", (line) => {
705
776
  if (settled) {
706
777
  return;
@@ -731,7 +802,9 @@ async function probeCommandServer(input) {
731
802
  });
732
803
  child.on("error", () => {
733
804
  scorecard.initialize = "fail";
734
- settle(buildFailure("plugin.runtime.startup.failed", `The MCP server \`${serverName}\` could not be started.`, "The configured stdio server could not be launched, so runtime validation cannot proceed.", `Verify the command \`${command}\` is installed and executable from \`${cwd}\`.`));
805
+ settle(buildFailure("plugin.runtime.startup.failed", `The MCP server \`${serverName}\` could not be started.`, "The configured stdio server could not be launched, so runtime validation cannot proceed.", launch.evidence.backend === "docker"
806
+ ? "Verify Docker is installed and the Docker daemon is available."
807
+ : `Verify the command \`${command}\` is installed and executable from \`${cwd}\`.`));
735
808
  });
736
809
  child.on("exit", () => {
737
810
  if (settled || finalizeRequested) {
@@ -740,9 +813,11 @@ async function probeCommandServer(input) {
740
813
  if (scorecard.initialize === "skipped") {
741
814
  scorecard.initialize = "fail";
742
815
  }
743
- settle(buildFailure("plugin.runtime.exited_early", `The MCP server \`${serverName}\` exited before the startup probe completed.`, "A server that exits immediately is unlikely to remain available for Codex during normal use.", stderrPreview.trim().length > 0
816
+ settle(buildFailure("plugin.runtime.exited_early", `The MCP server \`${serverName}\` exited before the startup probe completed.`, "A server that exits immediately is unlikely to remain available for Codex during normal use.", launch.evidence.backend !== "docker" && stderrPreview.trim().length > 0
744
817
  ? `Inspect the startup error output: ${stderrPreview.trim()}`
745
- : `Keep the \`${serverName}\` process running after startup and inspect its command or arguments.`));
818
+ : launch.evidence.backend === "docker"
819
+ ? "Verify Docker can start the pinned runtime image and keep the server process running."
820
+ : `Keep the \`${serverName}\` process running after startup and inspect its command or arguments.`));
746
821
  });
747
822
  const runProtocolProbe = async () => {
748
823
  const initializeResponse = await sendRequest("initialize", {
@@ -917,9 +992,11 @@ async function probeCommandServer(input) {
917
992
  if (scorecard.initialize === "skipped") {
918
993
  scorecard.initialize = "fail";
919
994
  }
920
- settle(buildFailure("plugin.runtime.protocol.unhandled", `The MCP server \`${serverName}\` triggered an unexpected protocol probe failure.`, "Unexpected probe failures reduce confidence in runtime validation results.", stderrPreview.trim().length > 0
995
+ settle(buildFailure("plugin.runtime.protocol.unhandled", `The MCP server \`${serverName}\` triggered an unexpected protocol probe failure.`, "Unexpected probe failures reduce confidence in runtime validation results.", launch.evidence.backend !== "docker" && stderrPreview.trim().length > 0
921
996
  ? `Inspect stderr for details: ${stderrPreview.trim()}`
922
- : "Inspect the server output and protocol implementation for unexpected runtime errors."));
997
+ : launch.evidence.backend === "docker"
998
+ ? "Inspect Docker availability and the server protocol implementation without exposing raw runtime output."
999
+ : "Inspect the server output and protocol implementation for unexpected runtime errors."));
923
1000
  });
924
1001
  });
925
1002
  }
@@ -934,6 +1011,7 @@ export async function probeRuntime(discoveredPackage, options = {}) {
934
1011
  }
935
1012
  const findings = [];
936
1013
  let scorecard = createRuntimeScorecard();
1014
+ let execution;
937
1015
  for (const [serverName, config] of Object.entries(servers)) {
938
1016
  if (!isPlainObject(config)) {
939
1017
  continue;
@@ -950,19 +1028,23 @@ export async function probeRuntime(discoveredPackage, options = {}) {
950
1028
  : discoveredPackage.rootPath;
951
1029
  const result = await probeCommandServer({
952
1030
  serverName,
1031
+ packageRoot: discoveredPackage.rootPath,
953
1032
  command,
954
1033
  args,
955
1034
  cwd,
956
1035
  startupTimeoutMs,
1036
+ sandbox: options.sandbox,
957
1037
  transcript: options.transcript
958
1038
  });
959
1039
  scorecard = result.scorecard;
1040
+ execution = result.execution ?? execution;
960
1041
  if (result.findings.length > 0) {
961
1042
  findings.push(...result.findings);
962
1043
  }
963
1044
  }
964
1045
  return {
965
1046
  findings,
966
- scorecard
1047
+ scorecard,
1048
+ ...(execution ? { execution } : {})
967
1049
  };
968
1050
  }
@@ -0,0 +1,21 @@
1
+ import type { RuntimeExecutionEvidence, RuntimeSandboxMode } from "../domain/types.js";
2
+ export interface RuntimeLaunchInput {
3
+ sandbox: RuntimeSandboxMode;
4
+ packageRoot: string;
5
+ cwd: string;
6
+ command: string;
7
+ args: string[];
8
+ containerName: string;
9
+ }
10
+ export interface RuntimeLaunchSpec {
11
+ command: string;
12
+ args: string[];
13
+ cwd: string;
14
+ containerName: string;
15
+ evidence: RuntimeExecutionEvidence;
16
+ }
17
+ export declare class RuntimeSandboxError extends Error {
18
+ }
19
+ export declare const DOCKER_RUNTIME_STARTUP_TIMEOUT_MS = 60000;
20
+ export declare const DOCKER_RUNTIME_IMAGE = "node:22-bookworm-slim@sha256:6c74791e557ce11fc957704f6d4fe134a7bc8d6f5ca4403205b2966bd488f6b3";
21
+ export declare function buildRuntimeLaunch(input: RuntimeLaunchInput): RuntimeLaunchSpec;