codex-plugin-doctor 1.46.1 → 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.
@@ -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;
@@ -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";
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";