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.
- package/README.md +23 -14
- package/dist/core/attestation.d.ts +1 -0
- package/dist/core/attestation.js +1 -1
- package/dist/core/external-validation-corpus.d.ts +88 -0
- package/dist/core/external-validation-corpus.js +271 -0
- package/dist/core/init-git-hooks.js +43 -4
- package/dist/core/output-contract.js +8 -2
- package/dist/core/release-check.d.ts +2 -1
- package/dist/core/release-check.js +2 -1
- package/dist/core/release-evidence.d.ts +5 -2
- package/dist/core/release-evidence.js +12 -2
- package/dist/core/runtime-plan.d.ts +5 -1
- package/dist/core/runtime-plan.js +13 -1
- package/dist/core/runtime-policy.d.ts +5 -1
- package/dist/core/runtime-policy.js +3 -2
- package/dist/core/runtime-probe.d.ts +2 -1
- package/dist/core/runtime-probe.js +100 -18
- package/dist/core/runtime-sandbox.d.ts +21 -0
- package/dist/core/runtime-sandbox.js +68 -0
- package/dist/core/validate-plugin.js +19 -8
- package/dist/core/validation-corpus.d.ts +4 -2
- package/dist/core/validation-corpus.js +12 -1
- package/dist/domain/types.d.ts +11 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/reporting/render-json-report.js +3 -0
- package/dist/reporting/render-markdown-report.js +3 -0
- package/dist/reporting/render-text-report.js +3 -0
- package/dist/run-cli.js +122 -19
- package/package.json +1 -1
|
@@ -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:
|
|
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
|
|
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
|
|
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
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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
|
-
},
|
|
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.",
|
|
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
|
-
:
|
|
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
|
-
:
|
|
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
|
|
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
|
-
...
|
|
415
|
-
...(
|
|
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
|
-
`(${
|
|
178
|
+
`(${status.toUpperCase()}, ${findingCount} findings)`);
|
|
168
179
|
}
|
|
169
180
|
return lines.join("\n");
|
|
170
181
|
}
|
package/dist/domain/types.d.ts
CHANGED
|
@@ -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";
|