codex-plugin-doctor 1.49.0 → 1.51.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/LICENSE +21 -21
- package/README.md +3 -3
- package/dist/core/mcp-conformance.d.ts +2 -0
- package/dist/core/mcp-conformance.js +215 -0
- package/dist/core/npm-package-doctor.d.ts +11 -0
- package/dist/core/npm-package-doctor.js +12 -2
- package/dist/core/output-contract.js +82 -5
- package/dist/core/runtime-plan.js +2 -0
- package/dist/core/runtime-probe.d.ts +4 -2
- package/dist/core/runtime-probe.js +326 -64
- package/dist/core/runtime-transcript.js +13 -3
- package/dist/domain/types.d.ts +34 -0
- package/dist/mcp/generic-mcp-doctor.d.ts +8 -2
- package/dist/mcp/generic-mcp-doctor.js +35 -11
- package/dist/reporting/render-markdown-report.js +31 -13
- package/dist/reporting/render-text-report.js +28 -22
- package/dist/rules/rule-catalog.js +72 -0
- package/dist/run-cli.js +43 -8
- package/examples/README.md +94 -94
- package/examples/codex-doctor-generic-mcp/.mcp.json +10 -10
- package/examples/codex-doctor-generic-mcp/server.js +1 -1
- package/examples/codex-doctor-risky/.codex-plugin/plugin.json +7 -7
- package/examples/codex-doctor-risky/.mcp.json +12 -12
- package/examples/codex-doctor-risky/mock-server.js +1 -1
- package/examples/codex-doctor-runtime/.codex-plugin/plugin.json +8 -8
- package/examples/codex-doctor-runtime/.mcp.json +9 -9
- package/examples/codex-doctor-runtime/mock-server.js +237 -237
- package/examples/codex-doctor-runtime/skills/context-check/SKILL.md +7 -7
- package/examples/codex-doctor-starter/.codex-plugin/plugin.json +7 -7
- package/examples/codex-doctor-starter/skills/repo-scan/SKILL.md +7 -7
- package/package.json +1 -1
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Furkan Tasci
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Furkan Tasci
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -427,9 +427,9 @@ jobs:
|
|
|
427
427
|
runs-on: ubuntu-latest
|
|
428
428
|
steps:
|
|
429
429
|
- uses: actions/checkout@v5
|
|
430
|
-
- uses: Esquetta/CodexPluginDoctor@v1.
|
|
430
|
+
- uses: Esquetta/CodexPluginDoctor@v1.51.0
|
|
431
431
|
with:
|
|
432
|
-
version: "1.
|
|
432
|
+
version: "1.51.0"
|
|
433
433
|
path: .
|
|
434
434
|
runtime: "true"
|
|
435
435
|
policy: codex-publish
|
|
@@ -441,7 +441,7 @@ jobs:
|
|
|
441
441
|
review-bundle-verify: "true"
|
|
442
442
|
```
|
|
443
443
|
|
|
444
|
-
The action writes `codex-plugin-doctor-summary.md`, `codex-plugin-doctor-report.json`, `codex-plugin-doctor-action-manifest.json`, optional `codex-plugin-doctor.sarif`, optional
|
|
444
|
+
The action writes `codex-plugin-doctor-summary.md`, `codex-plugin-doctor-report.json`, `codex-plugin-doctor-action-manifest.json`, optional `codex-plugin-doctor.sarif`, optional validation corpus and quality metrics reports, optional `output-contract.json`, and optional signed `review-bundle/` files to `codex-plugin-doctor-reports`, appends the Markdown report to the GitHub Actions step summary, uploads the report directory as an artifact, and then returns the real validation exit code. Review bundle generation requires a signing key environment variable such as `CODEX_PLUGIN_DOCTOR_SIGNING_KEY`. For runtime probing, SARIF output, corpus quality regression gates, corpus and contract artifacts, review bundle artifacts, installed plugin cache checks, CI policy presets, and pinned release examples, see [GitHub Action Usage](./docs/guides/github-action.md).
|
|
445
445
|
|
|
446
446
|
To self-test this repository after cloning it:
|
|
447
447
|
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
const LATEST_PROTOCOL_VERSION = "2025-11-25";
|
|
2
|
+
const LEGACY_PROTOCOL_VERSIONS = new Set([
|
|
3
|
+
"2024-11-05",
|
|
4
|
+
"2025-03-26",
|
|
5
|
+
"2025-06-18"
|
|
6
|
+
]);
|
|
7
|
+
const CANONICAL_SCHEMA_DIALECT = "https://json-schema.org/draft/2020-12/schema";
|
|
8
|
+
function isPlainObject(value) {
|
|
9
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
function buildFinding(id, severity, message, impact, suggestedFix, evidence) {
|
|
12
|
+
return {
|
|
13
|
+
id,
|
|
14
|
+
severity,
|
|
15
|
+
message,
|
|
16
|
+
impact,
|
|
17
|
+
suggestedFix,
|
|
18
|
+
...(evidence ? { evidence } : {})
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function classifyProtocolVersion(protocolVersion) {
|
|
22
|
+
if (LEGACY_PROTOCOL_VERSIONS.has(protocolVersion)) {
|
|
23
|
+
return "legacy";
|
|
24
|
+
}
|
|
25
|
+
if (protocolVersion === LATEST_PROTOCOL_VERSION) {
|
|
26
|
+
return "2025-11-25";
|
|
27
|
+
}
|
|
28
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(protocolVersion) && protocolVersion > LATEST_PROTOCOL_VERSION) {
|
|
29
|
+
return "future-compatible";
|
|
30
|
+
}
|
|
31
|
+
return "legacy";
|
|
32
|
+
}
|
|
33
|
+
function addTaskCapabilityFailure(findings, field) {
|
|
34
|
+
findings.push(buildFinding("mcp.conformance.tasks.capability_invalid", "fail", `The Tasks capability field \`${field}\` must be an object when present.`, "Codex cannot safely determine whether the server supports task-aware tool calls.", `Return an object for \`${field}\`, or omit the capability until it is implemented.`, { field }));
|
|
35
|
+
}
|
|
36
|
+
function getTaskToolsCallCapabilityState(capabilities, findings) {
|
|
37
|
+
if (!isPlainObject(capabilities)) {
|
|
38
|
+
addTaskCapabilityFailure(findings, "capabilities");
|
|
39
|
+
return "malformed";
|
|
40
|
+
}
|
|
41
|
+
const tasks = capabilities.tasks;
|
|
42
|
+
if (tasks === undefined) {
|
|
43
|
+
return "missing";
|
|
44
|
+
}
|
|
45
|
+
if (!isPlainObject(tasks)) {
|
|
46
|
+
addTaskCapabilityFailure(findings, "capabilities.tasks");
|
|
47
|
+
return "malformed";
|
|
48
|
+
}
|
|
49
|
+
for (const key of ["list", "cancel"]) {
|
|
50
|
+
if (tasks[key] !== undefined && !isPlainObject(tasks[key])) {
|
|
51
|
+
addTaskCapabilityFailure(findings, `capabilities.tasks.${key}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const requests = tasks.requests;
|
|
55
|
+
if (requests === undefined) {
|
|
56
|
+
return "missing";
|
|
57
|
+
}
|
|
58
|
+
if (!isPlainObject(requests)) {
|
|
59
|
+
addTaskCapabilityFailure(findings, "capabilities.tasks.requests");
|
|
60
|
+
return "malformed";
|
|
61
|
+
}
|
|
62
|
+
const tools = requests.tools;
|
|
63
|
+
if (tools === undefined) {
|
|
64
|
+
return "missing";
|
|
65
|
+
}
|
|
66
|
+
if (!isPlainObject(tools)) {
|
|
67
|
+
addTaskCapabilityFailure(findings, "capabilities.tasks.requests.tools");
|
|
68
|
+
return "malformed";
|
|
69
|
+
}
|
|
70
|
+
const call = tools.call;
|
|
71
|
+
if (call === undefined) {
|
|
72
|
+
return "missing";
|
|
73
|
+
}
|
|
74
|
+
if (!isPlainObject(call)) {
|
|
75
|
+
addTaskCapabilityFailure(findings, "capabilities.tasks.requests.tools.call");
|
|
76
|
+
return "malformed";
|
|
77
|
+
}
|
|
78
|
+
return "present-valid";
|
|
79
|
+
}
|
|
80
|
+
function collectTaskDeclarationFindings(observation, taskCallCapabilityState, findings) {
|
|
81
|
+
for (const tool of observation.tools) {
|
|
82
|
+
let taskSupport = "forbidden";
|
|
83
|
+
if (tool.execution !== undefined) {
|
|
84
|
+
if (!isPlainObject(tool.execution)) {
|
|
85
|
+
findings.push(buildFinding("mcp.conformance.tasks.task_support_invalid", "fail", `Tool \`${tool.name}\` has a non-object execution declaration.`, "Codex cannot safely interpret the tool's task support contract.", "Return an object for `execution` with taskSupport set to required, optional, or forbidden.", { toolName: tool.name }));
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
taskSupport = tool.execution.taskSupport ?? "forbidden";
|
|
89
|
+
}
|
|
90
|
+
if (taskSupport !== "required" &&
|
|
91
|
+
taskSupport !== "optional" &&
|
|
92
|
+
taskSupport !== "forbidden") {
|
|
93
|
+
findings.push(buildFinding("mcp.conformance.tasks.task_support_invalid", "fail", `Tool \`${tool.name}\` declares an unsupported taskSupport value.`, "Codex cannot determine whether task-aware tool calls are required or optional.", "Set execution.taskSupport to required, optional, or forbidden.", { toolName: tool.name }));
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (taskSupport !== "forbidden" && taskCallCapabilityState === "missing") {
|
|
97
|
+
findings.push(buildFinding("mcp.conformance.tasks.capability_mismatch", "fail", `Tool \`${tool.name}\` declares task support without tasks.requests.tools.call capability.`, "Codex may attempt task-aware tool calls that the server did not advertise as supported.", "Advertise capabilities.tasks.requests.tools.call as an object, or set execution.taskSupport to forbidden.", { toolName: tool.name, taskSupport }));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function isAbsoluteUri(value) {
|
|
102
|
+
try {
|
|
103
|
+
return new URL(value).protocol.length > 0;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function collectSchemaFindings(observation, findings) {
|
|
110
|
+
for (const tool of observation.tools) {
|
|
111
|
+
const schemas = [
|
|
112
|
+
["inputSchema", tool.inputSchema]
|
|
113
|
+
];
|
|
114
|
+
if (tool.outputSchema !== undefined) {
|
|
115
|
+
schemas.push(["outputSchema", tool.outputSchema]);
|
|
116
|
+
}
|
|
117
|
+
for (const [schemaName, schema] of schemas) {
|
|
118
|
+
if (!isPlainObject(schema)) {
|
|
119
|
+
findings.push(buildFinding("mcp.conformance.schema.dialect_invalid", "fail", `Tool \`${tool.name}\` has a non-object ${schemaName} container.`, "Codex cannot interpret a tool schema that is not represented as an object.", `Return an object for ${schemaName}.`, { toolName: tool.name, schema: schemaName }));
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const dialect = schema.$schema;
|
|
123
|
+
if (dialect === undefined) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (typeof dialect !== "string" || !isAbsoluteUri(dialect)) {
|
|
127
|
+
findings.push(buildFinding("mcp.conformance.schema.dialect_invalid", "fail", `Tool \`${tool.name}\` has an invalid ${schemaName} $schema URI.`, "Codex cannot reliably select a schema dialect for this tool.", `Omit $schema or provide an absolute URI in ${schemaName}.`, { toolName: tool.name, schema: schemaName }));
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (dialect !== CANONICAL_SCHEMA_DIALECT && dialect !== `${CANONICAL_SCHEMA_DIALECT}#`) {
|
|
131
|
+
findings.push(buildFinding("mcp.conformance.schema.dialect_unsupported", "warn", `Tool \`${tool.name}\` uses a non-canonical ${schemaName} $schema dialect.`, "The schema may be valid, but its dialect is outside the validator's latest compatibility baseline.", `Use ${CANONICAL_SCHEMA_DIALECT} or omit $schema.`, { toolName: tool.name, schema: schemaName }));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function collectTasksListFinding(observation, findings) {
|
|
137
|
+
if (!observation || observation.status === "skipped") {
|
|
138
|
+
return "skipped";
|
|
139
|
+
}
|
|
140
|
+
if (observation.status === "pass") {
|
|
141
|
+
return "pass";
|
|
142
|
+
}
|
|
143
|
+
const timeout = observation.failure === "timeout";
|
|
144
|
+
findings.push(buildFinding(timeout
|
|
145
|
+
? "mcp.conformance.tasks_list.timeout"
|
|
146
|
+
: "mcp.conformance.tasks_list.invalid", "fail", timeout
|
|
147
|
+
? "The server did not complete tasks/list within the observation window."
|
|
148
|
+
: "The server returned an invalid tasks/list observation.", "Codex cannot rely on task discovery when tasks/list cannot complete with a valid response.", timeout
|
|
149
|
+
? "Reduce tasks/list latency and verify pagination completes."
|
|
150
|
+
: "Return a valid tasks/list response with well-formed task items and pagination."));
|
|
151
|
+
return "fail";
|
|
152
|
+
}
|
|
153
|
+
function statusForFindings(findings) {
|
|
154
|
+
if (findings.some((finding) => finding.severity === "fail")) {
|
|
155
|
+
return "fail";
|
|
156
|
+
}
|
|
157
|
+
if (findings.some((finding) => finding.severity === "warn")) {
|
|
158
|
+
return "warn";
|
|
159
|
+
}
|
|
160
|
+
return "pass";
|
|
161
|
+
}
|
|
162
|
+
function overallStatus(scorecard, findings) {
|
|
163
|
+
const statuses = [
|
|
164
|
+
scorecard.capabilityConsistency,
|
|
165
|
+
scorecard.taskDeclarations,
|
|
166
|
+
scorecard.tasksList,
|
|
167
|
+
scorecard.schemaDialect
|
|
168
|
+
];
|
|
169
|
+
if (statuses.includes("fail") || findings.some((finding) => finding.severity === "fail")) {
|
|
170
|
+
return "fail";
|
|
171
|
+
}
|
|
172
|
+
if (statuses.includes("warn") || findings.some((finding) => finding.severity === "warn")) {
|
|
173
|
+
return "warn";
|
|
174
|
+
}
|
|
175
|
+
return "pass";
|
|
176
|
+
}
|
|
177
|
+
export function evaluateMcpConformance(observation) {
|
|
178
|
+
const profile = classifyProtocolVersion(observation.protocolVersion);
|
|
179
|
+
const findings = [];
|
|
180
|
+
const scorecard = {
|
|
181
|
+
protocolVersion: observation.protocolVersion,
|
|
182
|
+
profile,
|
|
183
|
+
capabilityConsistency: "skipped",
|
|
184
|
+
taskDeclarations: "skipped",
|
|
185
|
+
tasksList: "skipped",
|
|
186
|
+
schemaDialect: "skipped",
|
|
187
|
+
overall: "pass"
|
|
188
|
+
};
|
|
189
|
+
if (profile === "future-compatible") {
|
|
190
|
+
findings.push(buildFinding("mcp.conformance.protocol.unknown_newer", "warn", `The server advertises newer MCP protocol version \`${observation.protocolVersion}\`.`, "The validator applies the 2025-11-25 structural baseline, which may not cover newer protocol requirements.", "Confirm the server's newer protocol changes remain compatible with the 2025-11-25 MCP contract.", { protocolVersion: observation.protocolVersion }));
|
|
191
|
+
}
|
|
192
|
+
if (profile === "legacy") {
|
|
193
|
+
scorecard.overall = overallStatus(scorecard, findings);
|
|
194
|
+
return { findings, scorecard };
|
|
195
|
+
}
|
|
196
|
+
if (!observation.skipToolDerivedChecks) {
|
|
197
|
+
const capabilityFindings = [];
|
|
198
|
+
const taskCallCapabilityState = getTaskToolsCallCapabilityState(observation.capabilities, capabilityFindings);
|
|
199
|
+
findings.push(...capabilityFindings);
|
|
200
|
+
scorecard.capabilityConsistency = statusForFindings(capabilityFindings);
|
|
201
|
+
const declarationFindings = [];
|
|
202
|
+
collectTaskDeclarationFindings(observation, taskCallCapabilityState, declarationFindings);
|
|
203
|
+
findings.push(...declarationFindings);
|
|
204
|
+
scorecard.taskDeclarations = statusForFindings(declarationFindings);
|
|
205
|
+
}
|
|
206
|
+
scorecard.tasksList = collectTasksListFinding(observation.tasksList, findings);
|
|
207
|
+
if (!observation.skipToolDerivedChecks) {
|
|
208
|
+
const schemaFindings = [];
|
|
209
|
+
collectSchemaFindings(observation, schemaFindings);
|
|
210
|
+
findings.push(...schemaFindings);
|
|
211
|
+
scorecard.schemaDialect = statusForFindings(schemaFindings);
|
|
212
|
+
}
|
|
213
|
+
scorecard.overall = overallStatus(scorecard, findings);
|
|
214
|
+
return { findings, scorecard };
|
|
215
|
+
}
|
|
@@ -38,6 +38,17 @@ export interface DoctorNpmPackageReport {
|
|
|
38
38
|
export interface BuildDoctorNpmPackageReportOptions {
|
|
39
39
|
environment?: CompatibilityEnvironment;
|
|
40
40
|
}
|
|
41
|
+
export interface NpmPackEntry {
|
|
42
|
+
filename?: string;
|
|
43
|
+
name?: string;
|
|
44
|
+
version?: string;
|
|
45
|
+
files?: unknown[];
|
|
46
|
+
integrity?: string;
|
|
47
|
+
shasum?: string;
|
|
48
|
+
size?: number;
|
|
49
|
+
unpackedSize?: number;
|
|
50
|
+
}
|
|
51
|
+
export declare function parseNpmPackOutput(stdout: string): NpmPackEntry[];
|
|
41
52
|
export declare function buildDoctorNpmPackageReport(packageSpec: string, options?: BuildDoctorNpmPackageReportOptions): Promise<DoctorNpmPackageReport>;
|
|
42
53
|
export declare function renderDoctorNpmPackageReportJson(report: DoctorNpmPackageReport): string;
|
|
43
54
|
export declare function renderDoctorNpmPackageReport(report: DoctorNpmPackageReport, options?: {
|
|
@@ -7,6 +7,16 @@ import { gunzip } from "node:zlib";
|
|
|
7
7
|
import { buildDoctorRecommendationsFromAnalysis, buildPackageAnalysis } from "./package-analysis.js";
|
|
8
8
|
const execFileAsync = promisify(execFile);
|
|
9
9
|
const gunzipAsync = promisify(gunzip);
|
|
10
|
+
export function parseNpmPackOutput(stdout) {
|
|
11
|
+
const parsed = JSON.parse(stdout);
|
|
12
|
+
if (Array.isArray(parsed)) {
|
|
13
|
+
return parsed.filter((entry) => typeof entry === "object" && entry !== null);
|
|
14
|
+
}
|
|
15
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
16
|
+
return Object.values(parsed).filter((entry) => typeof entry === "object" && entry !== null);
|
|
17
|
+
}
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
10
20
|
function npmCommand(args) {
|
|
11
21
|
if (process.platform === "win32") {
|
|
12
22
|
const npmCliPath = process.env.npm_execpath ??
|
|
@@ -108,8 +118,8 @@ async function packNpmPackage(packageSpec, destinationPath) {
|
|
|
108
118
|
env: npmPackEnvironment(),
|
|
109
119
|
maxBuffer: 10 * 1024 * 1024
|
|
110
120
|
});
|
|
111
|
-
const packEntries =
|
|
112
|
-
const metadata = packEntries
|
|
121
|
+
const packEntries = parseNpmPackOutput(stdout);
|
|
122
|
+
const metadata = packEntries.find((entry) => typeof entry.filename === "string");
|
|
113
123
|
if (!metadata?.filename) {
|
|
114
124
|
throw new Error(`npm pack did not return a tarball for ${packageSpec}`);
|
|
115
125
|
}
|
|
@@ -1,11 +1,79 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { packageVersion } from "../version.js";
|
|
3
3
|
import { ruleCatalog } from "../rules/rule-catalog.js";
|
|
4
|
+
const runtimeCapabilityStatusSchema = {
|
|
5
|
+
type: "string",
|
|
6
|
+
enum: ["pass", "fail", "warn", "skipped", "unsupported"]
|
|
7
|
+
};
|
|
8
|
+
const runtimeConformanceSchema = {
|
|
9
|
+
type: "object",
|
|
10
|
+
properties: {
|
|
11
|
+
protocolVersion: {
|
|
12
|
+
type: ["string", "null"]
|
|
13
|
+
},
|
|
14
|
+
profile: {
|
|
15
|
+
type: ["string", "null"],
|
|
16
|
+
enum: ["legacy", "2025-11-25", "future-compatible", null]
|
|
17
|
+
},
|
|
18
|
+
capabilityConsistency: runtimeCapabilityStatusSchema,
|
|
19
|
+
taskDeclarations: runtimeCapabilityStatusSchema,
|
|
20
|
+
tasksList: runtimeCapabilityStatusSchema,
|
|
21
|
+
schemaDialect: runtimeCapabilityStatusSchema,
|
|
22
|
+
overall: {
|
|
23
|
+
type: "string",
|
|
24
|
+
enum: ["pass", "warn", "fail", "skipped"]
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
required: [
|
|
28
|
+
"protocolVersion",
|
|
29
|
+
"profile",
|
|
30
|
+
"capabilityConsistency",
|
|
31
|
+
"taskDeclarations",
|
|
32
|
+
"tasksList",
|
|
33
|
+
"schemaDialect",
|
|
34
|
+
"overall"
|
|
35
|
+
],
|
|
36
|
+
additionalProperties: false
|
|
37
|
+
};
|
|
38
|
+
const runtimeScorecardSchema = {
|
|
39
|
+
type: "object",
|
|
40
|
+
properties: {
|
|
41
|
+
initialize: runtimeCapabilityStatusSchema,
|
|
42
|
+
toolsList: runtimeCapabilityStatusSchema,
|
|
43
|
+
toolsCall: runtimeCapabilityStatusSchema,
|
|
44
|
+
resourcesList: runtimeCapabilityStatusSchema,
|
|
45
|
+
resourceRead: runtimeCapabilityStatusSchema,
|
|
46
|
+
resourceTemplatesList: runtimeCapabilityStatusSchema,
|
|
47
|
+
promptsList: runtimeCapabilityStatusSchema,
|
|
48
|
+
promptGet: runtimeCapabilityStatusSchema,
|
|
49
|
+
conformance: runtimeConformanceSchema
|
|
50
|
+
},
|
|
51
|
+
required: [
|
|
52
|
+
"initialize",
|
|
53
|
+
"toolsList",
|
|
54
|
+
"toolsCall",
|
|
55
|
+
"resourcesList",
|
|
56
|
+
"resourceRead",
|
|
57
|
+
"resourceTemplatesList",
|
|
58
|
+
"promptsList",
|
|
59
|
+
"promptGet"
|
|
60
|
+
],
|
|
61
|
+
additionalProperties: false
|
|
62
|
+
};
|
|
4
63
|
const publicSchemaDefinitions = [
|
|
5
64
|
{
|
|
6
65
|
id: "doctor.check.json",
|
|
7
66
|
command: "codex-plugin-doctor check <path> --json",
|
|
8
|
-
required: ["schemaVersion", "generatedAt", "summary", "findings"]
|
|
67
|
+
required: ["schemaVersion", "generatedAt", "summary", "findings"],
|
|
68
|
+
properties: {
|
|
69
|
+
summary: {
|
|
70
|
+
type: "object",
|
|
71
|
+
properties: {
|
|
72
|
+
runtimeScorecard: runtimeScorecardSchema
|
|
73
|
+
},
|
|
74
|
+
additionalProperties: true
|
|
75
|
+
}
|
|
76
|
+
}
|
|
9
77
|
},
|
|
10
78
|
{
|
|
11
79
|
id: "doctor.installed.check.json",
|
|
@@ -27,7 +95,13 @@ const publicSchemaDefinitions = [
|
|
|
27
95
|
id: "doctor.mcp.json",
|
|
28
96
|
command: "codex-plugin-doctor mcp <path> --json",
|
|
29
97
|
outputKind: "doctor.mcp.healthcheck",
|
|
30
|
-
required: ["schemaVersion", "kind", "generatedAt", "targetPath", "status", "serverCount", "findings", "security", "compatibility"]
|
|
98
|
+
required: ["schemaVersion", "kind", "generatedAt", "targetPath", "status", "serverCount", "findings", "security", "compatibility"],
|
|
99
|
+
properties: {
|
|
100
|
+
runtimeScorecard: {
|
|
101
|
+
...runtimeScorecardSchema,
|
|
102
|
+
description: "Present only when codex-plugin-doctor mcp <path> --runtime is used."
|
|
103
|
+
}
|
|
104
|
+
}
|
|
31
105
|
},
|
|
32
106
|
{
|
|
33
107
|
id: "doctor.audit.json",
|
|
@@ -271,7 +345,7 @@ function contractRules(rules) {
|
|
|
271
345
|
}))
|
|
272
346
|
.sort((left, right) => left.id.localeCompare(right.id));
|
|
273
347
|
}
|
|
274
|
-
function buildSchema(id, outputKind, required) {
|
|
348
|
+
function buildSchema(id, outputKind, required, schemaProperties = {}) {
|
|
275
349
|
const properties = {
|
|
276
350
|
schemaVersion: {
|
|
277
351
|
const: "1.0.0"
|
|
@@ -288,7 +362,10 @@ function buildSchema(id, outputKind, required) {
|
|
|
288
362
|
title: id,
|
|
289
363
|
type: "object",
|
|
290
364
|
required: [...new Set(["schemaVersion", ...required])],
|
|
291
|
-
properties
|
|
365
|
+
properties: {
|
|
366
|
+
...properties,
|
|
367
|
+
...schemaProperties
|
|
368
|
+
},
|
|
292
369
|
additionalProperties: true
|
|
293
370
|
};
|
|
294
371
|
}
|
|
@@ -301,7 +378,7 @@ function buildSchemas() {
|
|
|
301
378
|
schemaVersion: "1.0.0",
|
|
302
379
|
stability: "stable-through-1.0",
|
|
303
380
|
outputKind,
|
|
304
|
-
schema: buildSchema(definition.id, outputKind, definition.required ?? [])
|
|
381
|
+
schema: buildSchema(definition.id, outputKind, definition.required ?? [], definition.properties)
|
|
305
382
|
};
|
|
306
383
|
});
|
|
307
384
|
}
|
|
@@ -133,6 +133,7 @@ export async function buildDoctorRuntimePlan(targetPath, generatedAt = new Date(
|
|
|
133
133
|
"initialize",
|
|
134
134
|
"tools/list",
|
|
135
135
|
"tools/call:safe-only",
|
|
136
|
+
"tasks/list:declared-2025-11-only",
|
|
136
137
|
"resources/list",
|
|
137
138
|
"resources/read:first-resource-only",
|
|
138
139
|
"resources/templates/list",
|
|
@@ -244,6 +245,7 @@ export function renderDoctorRuntimePlanMarkdown(plan) {
|
|
|
244
245
|
"## Execution Boundary",
|
|
245
246
|
"",
|
|
246
247
|
"- This plan is non-executing.",
|
|
248
|
+
"- Probe methods explicitly exclude task create, get, result, and cancel operations, plus sampling and elicitation requests.",
|
|
247
249
|
"- Runtime probes require explicit operator approval before local MCP servers are started.",
|
|
248
250
|
"- The approval digest changes when command, args, cwd, probe methods, risk reasons, or findings change.",
|
|
249
251
|
"- Runtime approval is a review gate, not an OS, VM, or container sandbox.",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { DiscoveredPackage, RuntimeProbeResult, RuntimeSandboxMode } from "../domain/types.js";
|
|
2
|
-
export declare function probeRuntime(discoveredPackage: DiscoveredPackage, options?:
|
|
2
|
+
export declare function probeRuntime(discoveredPackage: DiscoveredPackage, options?: RuntimeProbeOptions): Promise<RuntimeProbeResult>;
|
|
3
|
+
export interface RuntimeProbeOptions {
|
|
3
4
|
startupTimeoutMs?: number;
|
|
4
5
|
sandbox?: RuntimeSandboxMode;
|
|
5
6
|
transcript?: (line: string) => void;
|
|
6
|
-
}
|
|
7
|
+
}
|
|
8
|
+
export declare function probeRuntimeConfig(rootPath: string, mcpConfigPath: string, options?: RuntimeProbeOptions): Promise<RuntimeProbeResult>;
|