codex-plugin-doctor 1.0.0 → 1.0.2
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 +16 -10
- package/dist/core/attestation.d.ts +48 -4
- package/dist/core/attestation.js +204 -16
- package/dist/core/output-contract.js +9 -2
- package/dist/core/performance-report.d.ts +15 -2
- package/dist/core/performance-report.js +44 -4
- package/dist/core/validation-corpus.d.ts +1 -0
- package/dist/core/validation-corpus.js +43 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mcp/generic-mcp-doctor.js +1 -0
- package/dist/run-cli.js +183 -17
- package/examples/codex-doctor-generic-mcp/.mcp.json +10 -0
- package/examples/codex-doctor-generic-mcp/server.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -196,9 +196,12 @@ codex-plugin-doctor doctor corpus
|
|
|
196
196
|
codex-plugin-doctor doctor corpus --json --output validation-corpus.json
|
|
197
197
|
codex-plugin-doctor doctor npm <published-plugin-package>
|
|
198
198
|
codex-plugin-doctor doctor npm <published-plugin-package> --json --output npm-preinstall.json
|
|
199
|
-
codex-plugin-doctor doctor attest .
|
|
200
|
-
codex-plugin-doctor doctor attest . --json --output attestation.json
|
|
201
|
-
codex-plugin-doctor doctor
|
|
199
|
+
codex-plugin-doctor doctor attest .
|
|
200
|
+
codex-plugin-doctor doctor attest . --json --output attestation.json
|
|
201
|
+
codex-plugin-doctor doctor attest . --json --sign-key-env CODEX_PLUGIN_DOCTOR_SIGNING_KEY --output attestation.json
|
|
202
|
+
codex-plugin-doctor doctor attest verify attestation.json --target . --sign-key-env CODEX_PLUGIN_DOCTOR_SIGNING_KEY
|
|
203
|
+
codex-plugin-doctor doctor attest verify attestation.json --target . --json --sign-key-env CODEX_PLUGIN_DOCTOR_SIGNING_KEY
|
|
204
|
+
codex-plugin-doctor doctor inspector .
|
|
202
205
|
codex-plugin-doctor doctor inspector . --server context7 --json --output inspector-command.json
|
|
203
206
|
codex-plugin-doctor doctor diff --before ./old-plugin --after ./new-plugin
|
|
204
207
|
codex-plugin-doctor doctor diff --before ./old-plugin --after ./new-plugin --json --output risk-diff.json
|
|
@@ -206,9 +209,12 @@ codex-plugin-doctor doctor recommend .
|
|
|
206
209
|
codex-plugin-doctor doctor recommend . --json --output recommendations.json
|
|
207
210
|
codex-plugin-doctor doctor trust .
|
|
208
211
|
codex-plugin-doctor doctor trust . --json --output trust-score.json
|
|
209
|
-
codex-plugin-doctor doctor perf .
|
|
210
|
-
codex-plugin-doctor doctor perf . --json --output perf.json
|
|
211
|
-
codex-plugin-doctor doctor
|
|
212
|
+
codex-plugin-doctor doctor perf .
|
|
213
|
+
codex-plugin-doctor doctor perf . --json --output perf.json
|
|
214
|
+
codex-plugin-doctor doctor perf . --max-total-ms 2500 --max-stage-ms validation=500
|
|
215
|
+
codex-plugin-doctor doctor mcp .
|
|
216
|
+
codex-plugin-doctor doctor mcp . --json --output mcp-healthcheck.json
|
|
217
|
+
codex-plugin-doctor doctor export --bundle .
|
|
212
218
|
codex-plugin-doctor doctor export --bundle . --output doctor-bundle.json
|
|
213
219
|
codex-plugin-doctor doctor snapshot
|
|
214
220
|
codex-plugin-doctor doctor snapshot --json
|
|
@@ -276,7 +282,7 @@ codex-plugin-doctor check . --json --runtime --verbose-runtime
|
|
|
276
282
|
|
|
277
283
|
`self-test` runs the bundled runtime-complete sample through static validation, runtime MCP probes, and the compatibility scorecard. It is the fastest post-install check after `npm install -g codex-plugin-doctor`.
|
|
278
284
|
|
|
279
|
-
`doctor` checks the local environment, including package version, platform, Node version, npm global prefix, Codex home, and Codex plugin cache visibility. The text output also includes recommended next commands for self-test, installed plugin discovery, runtime checks, compatibility scoring, and CI setup. `doctor contract` publishes the machine-readable output contract, including public JSON schema surfaces, stable-through-1.0 compatibility metadata, and a frozen rule catalog digest. Add `--json` for automation or `--output output-contract.json` to write the contract to disk. `doctor corpus` runs the bundled validation corpus against healthy runtime, risky security,
|
|
285
|
+
`doctor` checks the local environment, including package version, platform, Node version, npm global prefix, Codex home, and Codex plugin cache visibility. The text output also includes recommended next commands for self-test, installed plugin discovery, runtime checks, compatibility scoring, and CI setup. `doctor contract` publishes the machine-readable output contract, including public JSON schema surfaces, stable-through-1.0 compatibility metadata, and a frozen rule catalog digest. Add `--json` for automation or `--output output-contract.json` to write the contract to disk. `doctor corpus` runs the bundled validation corpus against healthy runtime, risky security, starter skill, and generic MCP packages, then reports whether each case matched its expected outcome. Add `--json` for automation or `--output validation-corpus.json` to write the corpus report to disk. `doctor npm <package>` runs a preinstall scan by packing the npm package with scripts disabled, extracting the publish tarball, and running validation, security, trust, and recommendation checks against the shipped contents. Use a published Codex plugin package as the target; scanning `codex-plugin-doctor` itself intentionally reports a missing plugin manifest because this CLI package is not a plugin package. Add `--json` for automation or `--output npm-preinstall.json` to write the report to disk. `doctor attest <path>` creates a local attestation with stable package/report digests, validation/security/compatibility/trust summary, and verification metadata. Add `--sign-key-env NAME` to attach a local HMAC-SHA256 signature without printing the secret, or `--json --output attestation.json` to write the artifact to disk. `doctor attest verify <attestation.json> --target <path> --sign-key-env NAME` recomputes the package fingerprint, report digest, and HMAC signature offline; verification intentionally treats `generatedAt`, `targetPath`, `verification`, and `signature.keyHint` as unsigned display metadata. `doctor inspector <path>` builds a safe MCP Inspector launch command from a packaged `.mcp.json` file without starting the Inspector proxy automatically. Use `--server <name>` when the package contains multiple MCP server entries. `doctor diff --before <path> --after <path>` compares two package roots and reports new findings, resolved findings, trust score delta, and whether risk increased. `doctor recommend <path>` turns validation, security, and compatibility signals into a prioritized action plan with blocker, high, medium, and info actions. Add `--json` for automation or `--output recommendations.json` to write the report to disk. `doctor trust <path>` creates a local trust score from package lifecycle scripts, dependency specs, and MCP security findings. Use it before release when you want supply-chain risks summarized as one score. `doctor perf <path>` profiles the shared package analysis pipeline and reports per-stage durations for validation, config, security, compatibility, trust, recommendations, and total runtime. Add `--max-total-ms <ms>` or repeatable `--max-stage-ms stage=ms` to fail CI when a budget is exceeded. `doctor mcp <path>` exposes the generic MCP static health report under the doctor command family without starting local MCP servers. `doctor export --bundle <path>` creates a redacted operator handoff bundle that includes validation JSON, security scorecard data, compatibility matrix, recommendations, and trust score in one file. `doctor snapshot` creates a redacted diagnostics bundle with environment health, client config readiness, installed plugin metadata, and next commands. Add `--json` for machine-readable output or `--output doctor-snapshot.json` to write the bundle to disk. `doctor clients` reports local Codex, Claude Desktop, Cursor, Cline, and Windsurf config readiness. `doctor --update-check` compares the installed CLI version with the latest npm version and prints the upgrade command when a newer release is available.
|
|
280
286
|
|
|
281
287
|
`audit --installed` runs a local ecosystem audit against every discovered Codex plugin in the installed plugin cache. Add `--security` to include security scorecards, `--compat` to include the all-client compatibility matrix, and `--json --output local-audit.json` when you want a shareable machine-readable report. Add `--cache` to reuse unchanged plugin results between runs; add `--changed` to only report plugins whose fingerprint changed since the last cached audit. Use `--cache-file path/to/audit-cache.json` when CI or scripted runs need an explicit cache location.
|
|
282
288
|
|
|
@@ -342,9 +348,9 @@ jobs:
|
|
|
342
348
|
runs-on: ubuntu-latest
|
|
343
349
|
steps:
|
|
344
350
|
- uses: actions/checkout@v5
|
|
345
|
-
- uses: Esquetta/CodexPluginDoctor@v1.0.
|
|
346
|
-
with:
|
|
347
|
-
version: "1.0.
|
|
351
|
+
- uses: Esquetta/CodexPluginDoctor@v1.0.2
|
|
352
|
+
with:
|
|
353
|
+
version: "1.0.2"
|
|
348
354
|
path: .
|
|
349
355
|
runtime: "true"
|
|
350
356
|
policy: codex-publish
|
|
@@ -50,13 +50,57 @@ export interface DoctorAttestation {
|
|
|
50
50
|
recomputeCommand: string;
|
|
51
51
|
notes: string[];
|
|
52
52
|
};
|
|
53
|
-
signature:
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
signature: DoctorAttestationSignature;
|
|
54
|
+
}
|
|
55
|
+
export type DoctorAttestationSignature = {
|
|
56
|
+
status: "unsigned";
|
|
57
|
+
reason: string;
|
|
58
|
+
} | {
|
|
59
|
+
status: "signed";
|
|
60
|
+
algorithm: "hmac-sha256";
|
|
61
|
+
digest: string;
|
|
62
|
+
payloadDigest: string;
|
|
63
|
+
keyHint: string;
|
|
64
|
+
};
|
|
65
|
+
export interface BuildDoctorAttestationOptions {
|
|
66
|
+
signingKey?: string;
|
|
67
|
+
signingKeyHint?: string;
|
|
68
|
+
recomputeKeyEnv?: string;
|
|
69
|
+
versionOverride?: string;
|
|
70
|
+
}
|
|
71
|
+
export interface DoctorAttestationVerificationReport {
|
|
72
|
+
schemaVersion: "1.0.0";
|
|
73
|
+
kind: "doctor.attestation.verification";
|
|
74
|
+
generatedAt: string;
|
|
75
|
+
artifactPath: string;
|
|
76
|
+
targetPath: string;
|
|
77
|
+
status: "pass" | "fail";
|
|
78
|
+
exitCode: 0 | 1;
|
|
79
|
+
summary: {
|
|
80
|
+
packageFingerprint: "pass" | "fail";
|
|
81
|
+
reportDigest: "pass" | "fail";
|
|
82
|
+
signature: "pass" | "fail";
|
|
56
83
|
};
|
|
84
|
+
unsignedFields: string[];
|
|
85
|
+
checks: DoctorAttestationVerificationCheck[];
|
|
86
|
+
}
|
|
87
|
+
export interface DoctorAttestationVerificationCheck {
|
|
88
|
+
id: string;
|
|
89
|
+
status: "pass" | "fail";
|
|
90
|
+
message: string;
|
|
91
|
+
expected?: string;
|
|
92
|
+
actual?: string;
|
|
93
|
+
}
|
|
94
|
+
export interface VerifyDoctorAttestationOptions {
|
|
95
|
+
signingKey: string;
|
|
57
96
|
}
|
|
58
|
-
export declare function buildDoctorAttestation(targetPath: string): Promise<DoctorAttestation>;
|
|
97
|
+
export declare function buildDoctorAttestation(targetPath: string, options?: BuildDoctorAttestationOptions): Promise<DoctorAttestation>;
|
|
98
|
+
export declare function verifyDoctorAttestation(artifactPath: string, targetPath: string, options: VerifyDoctorAttestationOptions): Promise<DoctorAttestationVerificationReport>;
|
|
59
99
|
export declare function renderDoctorAttestationJson(attestation: DoctorAttestation): string;
|
|
100
|
+
export declare function renderDoctorAttestationVerificationJson(report: DoctorAttestationVerificationReport): string;
|
|
101
|
+
export declare function renderDoctorAttestationVerification(report: DoctorAttestationVerificationReport, options?: {
|
|
102
|
+
outputPath?: string | null;
|
|
103
|
+
}): string;
|
|
60
104
|
export declare function renderDoctorAttestation(attestation: DoctorAttestation, options?: {
|
|
61
105
|
outputPath?: string | null;
|
|
62
106
|
}): string;
|
package/dist/core/attestation.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
1
|
+
import { createHash, createHmac, timingSafeEqual } from "node:crypto";
|
|
2
2
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { buildDoctorRecommendationsFromAnalysis, buildPackageAnalysis } from "./package-analysis.js";
|
|
@@ -116,9 +116,9 @@ async function readManifestSubject(rootPath) {
|
|
|
116
116
|
description: discoveredPackage.manifest.description
|
|
117
117
|
};
|
|
118
118
|
}
|
|
119
|
-
function buildReportDigestPayload(analysis, packageFingerprint) {
|
|
119
|
+
function buildReportDigestPayload(analysis, packageFingerprint, version) {
|
|
120
120
|
return {
|
|
121
|
-
version
|
|
121
|
+
version,
|
|
122
122
|
packageFingerprint,
|
|
123
123
|
validation: {
|
|
124
124
|
status: analysis.validation.status,
|
|
@@ -139,7 +139,13 @@ function buildReportDigestPayload(analysis, packageFingerprint) {
|
|
|
139
139
|
score: analysis.trust.score,
|
|
140
140
|
findings: analysis.trust.findings
|
|
141
141
|
},
|
|
142
|
-
recommendations: buildDoctorRecommendationsFromAnalysis(analysis).actions
|
|
142
|
+
recommendations: buildDoctorRecommendationsFromAnalysis(analysis).actions.map((action) => ({
|
|
143
|
+
priority: action.priority,
|
|
144
|
+
category: action.category,
|
|
145
|
+
title: action.title,
|
|
146
|
+
reason: action.reason,
|
|
147
|
+
findingId: action.findingId
|
|
148
|
+
}))
|
|
143
149
|
};
|
|
144
150
|
}
|
|
145
151
|
function buildSummary(analysis) {
|
|
@@ -181,43 +187,225 @@ function buildSummary(analysis) {
|
|
|
181
187
|
}
|
|
182
188
|
};
|
|
183
189
|
}
|
|
184
|
-
export async function buildDoctorAttestation(targetPath) {
|
|
190
|
+
export async function buildDoctorAttestation(targetPath, options = {}) {
|
|
185
191
|
const analysis = await buildPackageAnalysis(targetPath);
|
|
186
192
|
const [packageFingerprint, packageJson, manifest] = await Promise.all([
|
|
187
193
|
buildPackageFingerprint(analysis.targetPath),
|
|
188
194
|
readPackageSubject(analysis.targetPath),
|
|
189
195
|
readManifestSubject(analysis.targetPath)
|
|
190
196
|
]);
|
|
191
|
-
const
|
|
197
|
+
const attestationVersion = options.versionOverride ?? packageVersion;
|
|
198
|
+
const reportDigest = sha256(stableStringify(buildReportDigestPayload(analysis, packageFingerprint, attestationVersion)));
|
|
199
|
+
const subject = buildSubject(analysis, packageJson, manifest);
|
|
200
|
+
const summary = buildSummary(analysis);
|
|
201
|
+
const signingPayload = buildSigningPayload({
|
|
202
|
+
schemaVersion: "1.0.0",
|
|
203
|
+
kind: "doctor.attestation.signature.v1",
|
|
204
|
+
version: attestationVersion,
|
|
205
|
+
subject,
|
|
206
|
+
packageFingerprint,
|
|
207
|
+
reportDigest,
|
|
208
|
+
summary
|
|
209
|
+
});
|
|
210
|
+
const signature = options.signingKey
|
|
211
|
+
? {
|
|
212
|
+
status: "signed",
|
|
213
|
+
algorithm: "hmac-sha256",
|
|
214
|
+
digest: `sha256:${createHmac("sha256", options.signingKey)
|
|
215
|
+
.update(stableStringify(signingPayload))
|
|
216
|
+
.digest("hex")}`,
|
|
217
|
+
payloadDigest: sha256(stableStringify(signingPayload)),
|
|
218
|
+
keyHint: options.signingKeyHint ?? "inline"
|
|
219
|
+
}
|
|
220
|
+
: {
|
|
221
|
+
status: "unsigned",
|
|
222
|
+
reason: "No signing key was provided. Use --sign-key-env for reproducible local signing."
|
|
223
|
+
};
|
|
224
|
+
const recomputeCommand = signature.status === "signed"
|
|
225
|
+
? `codex-plugin-doctor doctor attest ${analysis.targetPath} --json --sign-key-env ${options.recomputeKeyEnv ?? "CODEX_PLUGIN_DOCTOR_SIGNING_KEY"}`
|
|
226
|
+
: `codex-plugin-doctor doctor attest ${analysis.targetPath} --json`;
|
|
192
227
|
return {
|
|
193
228
|
schemaVersion: "1.0.0",
|
|
194
229
|
kind: "doctor.attestation",
|
|
195
230
|
generatedAt: analysis.generatedAt,
|
|
196
|
-
version:
|
|
231
|
+
version: attestationVersion,
|
|
197
232
|
targetPath: analysis.targetPath,
|
|
198
|
-
subject
|
|
233
|
+
subject,
|
|
199
234
|
packageFingerprint,
|
|
200
235
|
reportDigest: {
|
|
201
236
|
algorithm: "sha256",
|
|
202
237
|
digest: reportDigest
|
|
203
238
|
},
|
|
204
|
-
summary
|
|
239
|
+
summary,
|
|
205
240
|
verification: {
|
|
206
|
-
recomputeCommand
|
|
241
|
+
recomputeCommand,
|
|
207
242
|
notes: [
|
|
208
243
|
"Compare packageFingerprint.digest to confirm the same local package contents.",
|
|
209
|
-
"Compare reportDigest.digest to confirm the same validation, security, compatibility, trust, and recommendation signals."
|
|
244
|
+
"Compare reportDigest.digest to confirm the same validation, security, compatibility, trust, and recommendation signals.",
|
|
245
|
+
signature.status === "signed"
|
|
246
|
+
? "Recompute the signed attestation with the same HMAC key stored in an environment variable."
|
|
247
|
+
: "Signing is optional and local; unsigned attestations remain deterministic without key material."
|
|
210
248
|
]
|
|
211
249
|
},
|
|
212
|
-
signature
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
250
|
+
signature
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function buildSigningPayload(attestation) {
|
|
254
|
+
return {
|
|
255
|
+
schemaVersion: attestation.schemaVersion,
|
|
256
|
+
kind: "doctor.attestation.signature.v1",
|
|
257
|
+
version: attestation.version,
|
|
258
|
+
subject: attestation.subject,
|
|
259
|
+
packageFingerprint: attestation.packageFingerprint,
|
|
260
|
+
reportDigest: typeof attestation.reportDigest === "string"
|
|
261
|
+
? attestation.reportDigest
|
|
262
|
+
: attestation.reportDigest.digest,
|
|
263
|
+
summary: attestation.summary
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function signPayload(payload, signingKey) {
|
|
267
|
+
const serializedPayload = stableStringify(payload);
|
|
268
|
+
return {
|
|
269
|
+
digest: `sha256:${createHmac("sha256", signingKey)
|
|
270
|
+
.update(serializedPayload)
|
|
271
|
+
.digest("hex")}`,
|
|
272
|
+
payloadDigest: sha256(serializedPayload)
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
function isDoctorAttestation(value) {
|
|
276
|
+
return isPlainObject(value) &&
|
|
277
|
+
value.schemaVersion === "1.0.0" &&
|
|
278
|
+
value.kind === "doctor.attestation" &&
|
|
279
|
+
typeof value.version === "string" &&
|
|
280
|
+
isPlainObject(value.packageFingerprint) &&
|
|
281
|
+
isPlainObject(value.reportDigest) &&
|
|
282
|
+
isPlainObject(value.signature);
|
|
283
|
+
}
|
|
284
|
+
function digestMatches(expected, actual) {
|
|
285
|
+
const expectedBuffer = Buffer.from(expected);
|
|
286
|
+
const actualBuffer = Buffer.from(actual);
|
|
287
|
+
return expectedBuffer.length === actualBuffer.length &&
|
|
288
|
+
timingSafeEqual(expectedBuffer, actualBuffer);
|
|
289
|
+
}
|
|
290
|
+
function createDigestVerificationCheck(id, message, expected, actual, includeDigests = true) {
|
|
291
|
+
return {
|
|
292
|
+
id,
|
|
293
|
+
status: digestMatches(expected, actual) ? "pass" : "fail",
|
|
294
|
+
message,
|
|
295
|
+
...(includeDigests ? { expected, actual } : {})
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
export async function verifyDoctorAttestation(artifactPath, targetPath, options) {
|
|
299
|
+
const resolvedArtifactPath = path.resolve(artifactPath);
|
|
300
|
+
const artifact = await readJsonFile(resolvedArtifactPath);
|
|
301
|
+
if (!isDoctorAttestation(artifact)) {
|
|
302
|
+
return {
|
|
303
|
+
schemaVersion: "1.0.0",
|
|
304
|
+
kind: "doctor.attestation.verification",
|
|
305
|
+
generatedAt: new Date().toISOString(),
|
|
306
|
+
artifactPath: resolvedArtifactPath,
|
|
307
|
+
targetPath: path.resolve(targetPath),
|
|
308
|
+
status: "fail",
|
|
309
|
+
exitCode: 1,
|
|
310
|
+
summary: {
|
|
311
|
+
packageFingerprint: "fail",
|
|
312
|
+
reportDigest: "fail",
|
|
313
|
+
signature: "fail"
|
|
314
|
+
},
|
|
315
|
+
unsignedFields: [
|
|
316
|
+
"generatedAt",
|
|
317
|
+
"targetPath",
|
|
318
|
+
"verification",
|
|
319
|
+
"signature.keyHint"
|
|
320
|
+
],
|
|
321
|
+
checks: [
|
|
322
|
+
{
|
|
323
|
+
id: "attestation.artifact.invalid",
|
|
324
|
+
status: "fail",
|
|
325
|
+
message: "The attestation artifact is not a valid doctor attestation."
|
|
326
|
+
}
|
|
327
|
+
]
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
const expected = await buildDoctorAttestation(targetPath, {
|
|
331
|
+
signingKey: options.signingKey,
|
|
332
|
+
signingKeyHint: "verification",
|
|
333
|
+
versionOverride: artifact.version
|
|
334
|
+
});
|
|
335
|
+
const checks = [
|
|
336
|
+
createDigestVerificationCheck("attestation.package_fingerprint", "Package fingerprint matches the target package contents.", expected.packageFingerprint.digest, artifact.packageFingerprint.digest),
|
|
337
|
+
createDigestVerificationCheck("attestation.report_digest", "Report digest matches the target validation evidence.", expected.reportDigest.digest, artifact.reportDigest.digest)
|
|
338
|
+
];
|
|
339
|
+
if (artifact.signature.status !== "signed") {
|
|
340
|
+
checks.push({
|
|
341
|
+
id: "attestation.signature.unsigned",
|
|
342
|
+
status: "fail",
|
|
343
|
+
message: "The attestation artifact is unsigned and cannot be verified with a signing key."
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
const expectedSignature = signPayload(buildSigningPayload(artifact), options.signingKey);
|
|
348
|
+
checks.push(createDigestVerificationCheck("attestation.signature.payload", "Signature payload digest matches the canonical attestation payload.", expectedSignature.payloadDigest, artifact.signature.payloadDigest, false), createDigestVerificationCheck("attestation.signature.mismatch", "HMAC-SHA256 signature matches the canonical attestation payload and signing key.", expectedSignature.digest, artifact.signature.digest, false));
|
|
349
|
+
}
|
|
350
|
+
const failedChecks = checks.filter((check) => check.status === "fail");
|
|
351
|
+
const packageFingerprint = checks.find((check) => check.id === "attestation.package_fingerprint")?.status ?? "fail";
|
|
352
|
+
const reportDigest = checks.find((check) => check.id === "attestation.report_digest")?.status ?? "fail";
|
|
353
|
+
const signature = checks
|
|
354
|
+
.filter((check) => check.id.startsWith("attestation.signature."))
|
|
355
|
+
.every((check) => check.status === "pass")
|
|
356
|
+
? "pass"
|
|
357
|
+
: "fail";
|
|
358
|
+
return {
|
|
359
|
+
schemaVersion: "1.0.0",
|
|
360
|
+
kind: "doctor.attestation.verification",
|
|
361
|
+
generatedAt: new Date().toISOString(),
|
|
362
|
+
artifactPath: resolvedArtifactPath,
|
|
363
|
+
targetPath: expected.targetPath,
|
|
364
|
+
status: failedChecks.length === 0 ? "pass" : "fail",
|
|
365
|
+
exitCode: failedChecks.length === 0 ? 0 : 1,
|
|
366
|
+
summary: {
|
|
367
|
+
packageFingerprint,
|
|
368
|
+
reportDigest,
|
|
369
|
+
signature
|
|
370
|
+
},
|
|
371
|
+
unsignedFields: [
|
|
372
|
+
"generatedAt",
|
|
373
|
+
"targetPath",
|
|
374
|
+
"verification",
|
|
375
|
+
"signature.keyHint"
|
|
376
|
+
],
|
|
377
|
+
checks
|
|
216
378
|
};
|
|
217
379
|
}
|
|
218
380
|
export function renderDoctorAttestationJson(attestation) {
|
|
219
381
|
return JSON.stringify(attestation, null, 2);
|
|
220
382
|
}
|
|
383
|
+
export function renderDoctorAttestationVerificationJson(report) {
|
|
384
|
+
return JSON.stringify(report, null, 2);
|
|
385
|
+
}
|
|
386
|
+
export function renderDoctorAttestationVerification(report, options = {}) {
|
|
387
|
+
const lines = [
|
|
388
|
+
"Doctor Attestation Verification",
|
|
389
|
+
"===============================",
|
|
390
|
+
`Artifact: ${report.artifactPath}`,
|
|
391
|
+
`Target: ${report.targetPath}`,
|
|
392
|
+
`Status: ${report.status.toUpperCase()}`,
|
|
393
|
+
`Package fingerprint: ${report.summary.packageFingerprint.toUpperCase()}`,
|
|
394
|
+
`Report digest: ${report.summary.reportDigest.toUpperCase()}`,
|
|
395
|
+
`Signature: ${report.summary.signature.toUpperCase()}`
|
|
396
|
+
];
|
|
397
|
+
if (options.outputPath) {
|
|
398
|
+
lines.push(`Output: ${options.outputPath}`);
|
|
399
|
+
}
|
|
400
|
+
lines.push("", "Checks", "------");
|
|
401
|
+
for (const check of report.checks) {
|
|
402
|
+
lines.push(`${check.status === "pass" ? "PASS" : "FAIL"} ${check.id}`);
|
|
403
|
+
lines.push(` ${check.message}`);
|
|
404
|
+
}
|
|
405
|
+
lines.push("", "Unsigned metadata", "-----------------");
|
|
406
|
+
lines.push(report.unsignedFields.join(", "));
|
|
407
|
+
return lines.join("\n");
|
|
408
|
+
}
|
|
221
409
|
export function renderDoctorAttestation(attestation, options = {}) {
|
|
222
410
|
const lines = [
|
|
223
411
|
"Doctor Attestation",
|
|
@@ -227,7 +415,7 @@ export function renderDoctorAttestation(attestation, options = {}) {
|
|
|
227
415
|
`Status: ${attestation.summary.status.toUpperCase()}`,
|
|
228
416
|
`Package fingerprint: ${attestation.packageFingerprint.digest}`,
|
|
229
417
|
`Report digest: ${attestation.reportDigest.digest}`,
|
|
230
|
-
`Signature: ${attestation.signature.status}`
|
|
418
|
+
`Signature: ${attestation.signature.status}${attestation.signature.status === "signed" ? ` (${attestation.signature.algorithm})` : ""}`
|
|
231
419
|
];
|
|
232
420
|
if (options.outputPath) {
|
|
233
421
|
lines.push(`Output: ${options.outputPath}`);
|
|
@@ -26,7 +26,8 @@ const publicSchemaDefinitions = [
|
|
|
26
26
|
{
|
|
27
27
|
id: "doctor.mcp.json",
|
|
28
28
|
command: "codex-plugin-doctor mcp <path> --json",
|
|
29
|
-
|
|
29
|
+
outputKind: "doctor.mcp.healthcheck",
|
|
30
|
+
required: ["schemaVersion", "kind", "generatedAt", "targetPath", "status", "serverCount", "findings", "security", "compatibility"]
|
|
30
31
|
},
|
|
31
32
|
{
|
|
32
33
|
id: "doctor.audit.json",
|
|
@@ -68,7 +69,7 @@ const publicSchemaDefinitions = [
|
|
|
68
69
|
id: "doctor.performance.json",
|
|
69
70
|
command: "codex-plugin-doctor doctor perf <path> --json",
|
|
70
71
|
outputKind: "doctor.perf",
|
|
71
|
-
required: ["schemaVersion", "kind", "generatedAt", "targetPath", "summary", "stages"]
|
|
72
|
+
required: ["schemaVersion", "kind", "generatedAt", "targetPath", "status", "exitCode", "summary", "stages", "thresholds"]
|
|
72
73
|
},
|
|
73
74
|
{
|
|
74
75
|
id: "doctor.export.bundle.json",
|
|
@@ -82,6 +83,12 @@ const publicSchemaDefinitions = [
|
|
|
82
83
|
outputKind: "doctor.attestation",
|
|
83
84
|
required: ["schemaVersion", "kind", "generatedAt", "targetPath", "subject", "packageFingerprint", "reportDigest", "summary", "verification", "signature"]
|
|
84
85
|
},
|
|
86
|
+
{
|
|
87
|
+
id: "doctor.attestation.verification.json",
|
|
88
|
+
command: "codex-plugin-doctor doctor attest verify <attestation.json> --target <path> --json",
|
|
89
|
+
outputKind: "doctor.attestation.verification",
|
|
90
|
+
required: ["schemaVersion", "kind", "generatedAt", "artifactPath", "targetPath", "status", "exitCode", "summary", "unsignedFields", "checks"]
|
|
91
|
+
},
|
|
85
92
|
{
|
|
86
93
|
id: "doctor.npm.json",
|
|
87
94
|
command: "codex-plugin-doctor doctor npm <package> --json",
|
|
@@ -13,8 +13,8 @@ export interface DoctorPerformanceReport {
|
|
|
13
13
|
generatedAt: string;
|
|
14
14
|
kind: "doctor.perf";
|
|
15
15
|
targetPath: string;
|
|
16
|
-
status: "pass";
|
|
17
|
-
exitCode: 0;
|
|
16
|
+
status: "pass" | "fail";
|
|
17
|
+
exitCode: 0 | 1;
|
|
18
18
|
summary: {
|
|
19
19
|
stageCount: number;
|
|
20
20
|
slowestStage: DoctorPerformanceStageName;
|
|
@@ -23,12 +23,25 @@ export interface DoctorPerformanceReport {
|
|
|
23
23
|
securityStatus: "pass" | "warn" | "fail";
|
|
24
24
|
trustScore: number;
|
|
25
25
|
compatibilityFailures: number;
|
|
26
|
+
thresholdFailures: number;
|
|
26
27
|
};
|
|
27
28
|
stages: DoctorPerformanceStage[];
|
|
29
|
+
thresholds: DoctorPerformanceThresholdResult[];
|
|
28
30
|
}
|
|
29
31
|
export interface BuildDoctorPerformanceReportOptions {
|
|
30
32
|
environment?: CompatibilityEnvironment;
|
|
31
33
|
runCheck?: (targetPath: string) => Promise<CheckResult>;
|
|
34
|
+
thresholds?: DoctorPerformanceThresholdOptions;
|
|
35
|
+
}
|
|
36
|
+
export interface DoctorPerformanceThresholdOptions {
|
|
37
|
+
totalMs?: number;
|
|
38
|
+
stages?: Partial<Record<DoctorPerformanceStageName, number>>;
|
|
39
|
+
}
|
|
40
|
+
export interface DoctorPerformanceThresholdResult {
|
|
41
|
+
stage: DoctorPerformanceStageName;
|
|
42
|
+
limitMs: number;
|
|
43
|
+
actualMs: number;
|
|
44
|
+
status: "pass" | "fail";
|
|
32
45
|
}
|
|
33
46
|
export declare function buildDoctorPerformanceReport(targetPath: string, options?: BuildDoctorPerformanceReportOptions): Promise<DoctorPerformanceReport>;
|
|
34
47
|
export declare function renderDoctorPerformanceReportJson(report: DoctorPerformanceReport): string;
|
|
@@ -24,6 +24,34 @@ function slowestStage(stages) {
|
|
|
24
24
|
.filter((stage) => stage.name !== "total")
|
|
25
25
|
.reduce((slowest, stage) => (stage.durationMs > slowest.durationMs ? stage : slowest), stages[0]).name;
|
|
26
26
|
}
|
|
27
|
+
function evaluateThresholds(stages, thresholds = {}) {
|
|
28
|
+
const thresholdResults = [];
|
|
29
|
+
const stageByName = new Map(stages.map((stage) => [stage.name, stage]));
|
|
30
|
+
if (thresholds.totalMs !== undefined) {
|
|
31
|
+
const totalStage = stageByName.get("total");
|
|
32
|
+
if (totalStage) {
|
|
33
|
+
thresholdResults.push({
|
|
34
|
+
stage: "total",
|
|
35
|
+
limitMs: thresholds.totalMs,
|
|
36
|
+
actualMs: totalStage.durationMs,
|
|
37
|
+
status: totalStage.durationMs > thresholds.totalMs ? "fail" : "pass"
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
for (const [stageName, limitMs] of Object.entries(thresholds.stages ?? {})) {
|
|
42
|
+
const stage = stageByName.get(stageName);
|
|
43
|
+
if (!stage) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
thresholdResults.push({
|
|
47
|
+
stage: stageName,
|
|
48
|
+
limitMs,
|
|
49
|
+
actualMs: stage.durationMs,
|
|
50
|
+
status: stage.durationMs > limitMs ? "fail" : "pass"
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return thresholdResults;
|
|
54
|
+
}
|
|
27
55
|
export async function buildDoctorPerformanceReport(targetPath, options = {}) {
|
|
28
56
|
const timings = [];
|
|
29
57
|
const startedAt = performance.now();
|
|
@@ -94,13 +122,16 @@ export async function buildDoctorPerformanceReport(targetPath, options = {}) {
|
|
|
94
122
|
durationMs: roundDuration(totalDurationMs)
|
|
95
123
|
};
|
|
96
124
|
});
|
|
125
|
+
const thresholdResults = evaluateThresholds(stages, options.thresholds);
|
|
126
|
+
const thresholdFailures = thresholdResults
|
|
127
|
+
.filter((threshold) => threshold.status === "fail").length;
|
|
97
128
|
return {
|
|
98
129
|
schemaVersion: "1.0.0",
|
|
99
130
|
generatedAt: analysis.generatedAt,
|
|
100
131
|
kind: "doctor.perf",
|
|
101
132
|
targetPath: analysis.targetPath,
|
|
102
|
-
status: "pass",
|
|
103
|
-
exitCode: 0,
|
|
133
|
+
status: thresholdFailures > 0 ? "fail" : "pass",
|
|
134
|
+
exitCode: thresholdFailures > 0 ? 1 : 0,
|
|
104
135
|
summary: {
|
|
105
136
|
stageCount: stages.length,
|
|
106
137
|
slowestStage: slowestStage(stages),
|
|
@@ -108,9 +139,11 @@ export async function buildDoctorPerformanceReport(targetPath, options = {}) {
|
|
|
108
139
|
validationStatus: analysis.validation.status,
|
|
109
140
|
securityStatus: analysis.security.status,
|
|
110
141
|
trustScore: analysis.trust.score,
|
|
111
|
-
compatibilityFailures
|
|
142
|
+
compatibilityFailures,
|
|
143
|
+
thresholdFailures
|
|
112
144
|
},
|
|
113
|
-
stages
|
|
145
|
+
stages,
|
|
146
|
+
thresholds: thresholdResults
|
|
114
147
|
};
|
|
115
148
|
}
|
|
116
149
|
export function renderDoctorPerformanceReportJson(report) {
|
|
@@ -137,5 +170,12 @@ export function renderDoctorPerformanceReport(report, options = {}) {
|
|
|
137
170
|
const count = stage.itemCount === undefined ? "" : `, items: ${stage.itemCount}`;
|
|
138
171
|
lines.push(`${stage.name}: ${stage.durationMs}ms${status}${count}`);
|
|
139
172
|
}
|
|
173
|
+
if (report.thresholds.length > 0) {
|
|
174
|
+
lines.push("", "Thresholds", "----------");
|
|
175
|
+
for (const threshold of report.thresholds) {
|
|
176
|
+
lines.push(`${threshold.stage}: ${threshold.actualMs}ms <= ${threshold.limitMs}ms ` +
|
|
177
|
+
`(${threshold.status.toUpperCase()})`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
140
180
|
return lines.join("\n");
|
|
141
181
|
}
|
|
@@ -3,6 +3,7 @@ import { fileURLToPath } from "node:url";
|
|
|
3
3
|
import { buildPackageAnalysis } from "./package-analysis.js";
|
|
4
4
|
import { validatePlugin } from "./validate-plugin.js";
|
|
5
5
|
import { matrixExitCode } from "../compatibility/compatibility-matrix.js";
|
|
6
|
+
import { buildGenericMcpDoctor } from "../mcp/generic-mcp-doctor.js";
|
|
6
7
|
import { packageVersion } from "../version.js";
|
|
7
8
|
const bundledCorpusCases = [
|
|
8
9
|
{
|
|
@@ -38,6 +39,18 @@ const bundledCorpusCases = [
|
|
|
38
39
|
expected: {
|
|
39
40
|
validationStatus: "pass"
|
|
40
41
|
}
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: "bundled-generic-mcp",
|
|
45
|
+
label: "Bundled generic MCP package",
|
|
46
|
+
profile: "generic-mcp",
|
|
47
|
+
sourceType: "bundled-example",
|
|
48
|
+
relativePath: "examples/codex-doctor-generic-mcp",
|
|
49
|
+
runtimeEnabled: false,
|
|
50
|
+
mode: "generic-mcp",
|
|
51
|
+
expected: {
|
|
52
|
+
validationStatus: "pass"
|
|
53
|
+
}
|
|
41
54
|
}
|
|
42
55
|
];
|
|
43
56
|
function resolvePackageRoot() {
|
|
@@ -48,6 +61,36 @@ function includesExpectedFindings(actualFindingIds, expectedFindingIds) {
|
|
|
48
61
|
}
|
|
49
62
|
async function runCorpusCase(caseDefinition, options) {
|
|
50
63
|
const targetPath = path.resolve(resolvePackageRoot(), caseDefinition.relativePath);
|
|
64
|
+
const expectedFindingIds = [...(caseDefinition.expected.findingIds ?? [])].sort();
|
|
65
|
+
if (caseDefinition.mode === "generic-mcp") {
|
|
66
|
+
const report = await buildGenericMcpDoctor(targetPath, options.environment);
|
|
67
|
+
const findingIds = report.findings.map((finding) => finding.id).sort();
|
|
68
|
+
const compatibilityFailedClients = report.compatibility.results
|
|
69
|
+
.filter((result) => result.status === "fail")
|
|
70
|
+
.map((result) => result.client);
|
|
71
|
+
const expectationMatched = report.status === caseDefinition.expected.validationStatus &&
|
|
72
|
+
includesExpectedFindings(findingIds, expectedFindingIds);
|
|
73
|
+
return {
|
|
74
|
+
id: caseDefinition.id,
|
|
75
|
+
label: caseDefinition.label,
|
|
76
|
+
profile: caseDefinition.profile,
|
|
77
|
+
sourceType: caseDefinition.sourceType,
|
|
78
|
+
targetPath,
|
|
79
|
+
runtimeEnabled: caseDefinition.runtimeEnabled,
|
|
80
|
+
expected: {
|
|
81
|
+
validationStatus: caseDefinition.expected.validationStatus,
|
|
82
|
+
findingIds: expectedFindingIds
|
|
83
|
+
},
|
|
84
|
+
actual: {
|
|
85
|
+
validationStatus: report.status,
|
|
86
|
+
findingIds,
|
|
87
|
+
securityStatus: report.security.status,
|
|
88
|
+
trustStatus: "pass",
|
|
89
|
+
compatibilityFailedClients
|
|
90
|
+
},
|
|
91
|
+
expectationMatched
|
|
92
|
+
};
|
|
93
|
+
}
|
|
51
94
|
const analysis = await buildPackageAnalysis(targetPath, {
|
|
52
95
|
environment: options.environment,
|
|
53
96
|
runCheck: (pathToCheck) => validatePlugin(pathToCheck, {
|
|
@@ -55,7 +98,6 @@ async function runCorpusCase(caseDefinition, options) {
|
|
|
55
98
|
})
|
|
56
99
|
});
|
|
57
100
|
const findingIds = analysis.validation.findings.map((finding) => finding.id).sort();
|
|
58
|
-
const expectedFindingIds = [...(caseDefinition.expected.findingIds ?? [])].sort();
|
|
59
101
|
const compatibilityFailedClients = analysis.compatibility.results
|
|
60
102
|
.filter((result) => result.status === "fail")
|
|
61
103
|
.map((result) => result.client);
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export { buildTrustScore, renderTrustScore, renderTrustScoreJson, type BuildTrus
|
|
|
4
4
|
export { buildDoctorSnapshot, renderDoctorSnapshot, renderDoctorSnapshotJson, type DoctorSnapshot } from "./core/doctor-snapshot.js";
|
|
5
5
|
export { buildDoctorRecommendations, renderDoctorRecommendations, renderDoctorRecommendationsJson, type DoctorRecommendationAction, type DoctorRecommendationsReport } from "./core/doctor-recommendations.js";
|
|
6
6
|
export { buildDoctorExportBundle, renderDoctorExportBundle, renderDoctorExportBundleJson, type DoctorExportBundle } from "./core/doctor-export-bundle.js";
|
|
7
|
-
export { buildDoctorAttestation, renderDoctorAttestation, renderDoctorAttestationJson, type DoctorAttestation, type Digest, type PackageFingerprint } from "./core/attestation.js";
|
|
7
|
+
export { buildDoctorAttestation, renderDoctorAttestation, renderDoctorAttestationJson, renderDoctorAttestationVerification, renderDoctorAttestationVerificationJson, verifyDoctorAttestation, type DoctorAttestation, type DoctorAttestationVerificationCheck, type DoctorAttestationVerificationReport, type Digest, type PackageFingerprint } from "./core/attestation.js";
|
|
8
8
|
export { buildDoctorOutputContract, renderDoctorOutputContract, renderDoctorOutputContractJson, type DoctorOutputContract, type OutputContractRule, type OutputContractSchema } from "./core/output-contract.js";
|
|
9
9
|
export { buildDoctorValidationCorpusReport, renderDoctorValidationCorpusJson, renderDoctorValidationCorpusReport, type BuildDoctorValidationCorpusOptions, type DoctorValidationCorpusReport, type ValidationCorpusCaseDefinition, type ValidationCorpusCaseResult } from "./core/validation-corpus.js";
|
|
10
10
|
export { buildDoctorExportBundleFromAnalysis, buildDoctorRecommendationsFromAnalysis, buildPackageAnalysis, type PackageAnalysis, type PackageAnalysisOptions, type PackageAnalysisStage, type PackageAnalysisTiming } from "./core/package-analysis.js";
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ export { buildTrustScore, renderTrustScore, renderTrustScoreJson } from "./secur
|
|
|
4
4
|
export { buildDoctorSnapshot, renderDoctorSnapshot, renderDoctorSnapshotJson } from "./core/doctor-snapshot.js";
|
|
5
5
|
export { buildDoctorRecommendations, renderDoctorRecommendations, renderDoctorRecommendationsJson } from "./core/doctor-recommendations.js";
|
|
6
6
|
export { buildDoctorExportBundle, renderDoctorExportBundle, renderDoctorExportBundleJson } from "./core/doctor-export-bundle.js";
|
|
7
|
-
export { buildDoctorAttestation, renderDoctorAttestation, renderDoctorAttestationJson } from "./core/attestation.js";
|
|
7
|
+
export { buildDoctorAttestation, renderDoctorAttestation, renderDoctorAttestationJson, renderDoctorAttestationVerification, renderDoctorAttestationVerificationJson, verifyDoctorAttestation } from "./core/attestation.js";
|
|
8
8
|
export { buildDoctorOutputContract, renderDoctorOutputContract, renderDoctorOutputContractJson } from "./core/output-contract.js";
|
|
9
9
|
export { buildDoctorValidationCorpusReport, renderDoctorValidationCorpusJson, renderDoctorValidationCorpusReport } from "./core/validation-corpus.js";
|
|
10
10
|
export { buildDoctorExportBundleFromAnalysis, buildDoctorRecommendationsFromAnalysis, buildPackageAnalysis } from "./core/package-analysis.js";
|
|
@@ -125,6 +125,7 @@ export async function buildGenericMcpDoctor(targetPath, environment = {}) {
|
|
|
125
125
|
export function renderGenericMcpDoctorJson(report) {
|
|
126
126
|
return JSON.stringify({
|
|
127
127
|
schemaVersion: "1.0.0",
|
|
128
|
+
kind: "doctor.mcp.healthcheck",
|
|
128
129
|
generatedAt: new Date().toISOString(),
|
|
129
130
|
...report
|
|
130
131
|
}, null, 2);
|
package/dist/run-cli.js
CHANGED
|
@@ -16,7 +16,7 @@ import { applyDoctorConfig, loadDoctorConfig } from "./core/doctor-config.js";
|
|
|
16
16
|
import { buildDoctorSnapshot, renderDoctorSnapshot, renderDoctorSnapshotJson } from "./core/doctor-snapshot.js";
|
|
17
17
|
import { buildDoctorRecommendations, renderDoctorRecommendations, renderDoctorRecommendationsJson } from "./core/doctor-recommendations.js";
|
|
18
18
|
import { buildDoctorExportBundle, renderDoctorExportBundle, renderDoctorExportBundleJson } from "./core/doctor-export-bundle.js";
|
|
19
|
-
import { buildDoctorAttestation, renderDoctorAttestation, renderDoctorAttestationJson } from "./core/attestation.js";
|
|
19
|
+
import { buildDoctorAttestation, renderDoctorAttestation, renderDoctorAttestationJson, renderDoctorAttestationVerification, renderDoctorAttestationVerificationJson, verifyDoctorAttestation } from "./core/attestation.js";
|
|
20
20
|
import { buildDoctorOutputContract, renderDoctorOutputContract, renderDoctorOutputContractJson } from "./core/output-contract.js";
|
|
21
21
|
import { buildDoctorValidationCorpusReport, renderDoctorValidationCorpusJson, renderDoctorValidationCorpusReport } from "./core/validation-corpus.js";
|
|
22
22
|
import { buildDoctorPerformanceReport, renderDoctorPerformanceReport, renderDoctorPerformanceReportJson } from "./core/performance-report.js";
|
|
@@ -69,7 +69,71 @@ const defaultIo = {
|
|
|
69
69
|
}
|
|
70
70
|
};
|
|
71
71
|
function printUsage(io) {
|
|
72
|
-
io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--policy codex-publish|mcp-strict|security] [--compat] [--json|--markdown|--badge-json|--badge-markdown] [--output <path>] [--history <path>] [--runtime] [--verbose-runtime] [--explain] [--no-animations] [--ascii]\n codex-plugin-doctor audit --installed [filter] [--policy codex-publish|mcp-strict|security] [--security] [--compat] [--json] [--output <path>] [--cache] [--changed]\n codex-plugin-doctor mcp <path> [--json] [--output <path>]\n codex-plugin-doctor security <path> [--policy security] [--json|--scorecard]\n codex-plugin-doctor compat <path> [--all|--client <client>] [--json] [--scorecard] [--output <path>] [--install-preview|--apply --backup]\n codex-plugin-doctor fix <path> (--dry-run|--interactive --backup|--apply --backup)\n codex-plugin-doctor history <history.jsonl> [--json] [--fail-on-regression]\n codex-plugin-doctor doctor [npm <package>|contract|corpus|attest <path>|inspector <path>|diff --before <path> --after <path>|recommend <path>|trust <path>|perf <path
|
|
72
|
+
io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--policy codex-publish|mcp-strict|security] [--compat] [--json|--markdown|--badge-json|--badge-markdown] [--output <path>] [--history <path>] [--runtime] [--verbose-runtime] [--explain] [--no-animations] [--ascii]\n codex-plugin-doctor audit --installed [filter] [--policy codex-publish|mcp-strict|security] [--security] [--compat] [--json] [--output <path>] [--cache] [--changed]\n codex-plugin-doctor mcp <path> [--json] [--output <path>]\n codex-plugin-doctor security <path> [--policy security] [--json|--scorecard]\n codex-plugin-doctor compat <path> [--all|--client <client>] [--json] [--scorecard] [--output <path>] [--install-preview|--apply --backup]\n codex-plugin-doctor fix <path> (--dry-run|--interactive --backup|--apply --backup)\n codex-plugin-doctor history <history.jsonl> [--json] [--fail-on-regression]\n codex-plugin-doctor doctor [npm <package>|contract|corpus|attest <path> [--sign-key-env NAME]|attest verify <attestation.json> --target <path> --sign-key-env NAME|mcp <path>|inspector <path>|diff --before <path> --after <path>|recommend <path>|trust <path>|perf <path> [--max-total-ms <ms>] [--max-stage-ms stage=ms]|export --bundle <path>|snapshot|clients|--json|--update-check]\n codex-plugin-doctor init [path] [--template skill-only|mcp-stdio|mcp-http|full-runtime]\n codex-plugin-doctor init-ci [path]\n codex-plugin-doctor self-test\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version\n\nFirst run:\n codex-plugin-doctor doctor\n codex-plugin-doctor self-test\n codex-plugin-doctor init my-plugin\n codex-plugin-doctor check . --runtime --explain");
|
|
73
|
+
}
|
|
74
|
+
const performanceStageNames = new Set([
|
|
75
|
+
"validation",
|
|
76
|
+
"doctorConfig",
|
|
77
|
+
"security",
|
|
78
|
+
"compatibility",
|
|
79
|
+
"trust",
|
|
80
|
+
"recommendations",
|
|
81
|
+
"total"
|
|
82
|
+
]);
|
|
83
|
+
function parseNonNegativeNumber(value) {
|
|
84
|
+
if (value === undefined || value.startsWith("--")) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
const parsed = Number(value);
|
|
88
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
|
|
89
|
+
}
|
|
90
|
+
function buildGenericMcpDoctorCommandArgs(commandTarget, flags) {
|
|
91
|
+
if (!commandTarget || commandTarget.startsWith("--")) {
|
|
92
|
+
return "Missing target path. Usage: codex-plugin-doctor mcp <path> [--json] [--output <path>]";
|
|
93
|
+
}
|
|
94
|
+
const outputIndex = flags.indexOf("--output");
|
|
95
|
+
const outputPath = outputIndex === -1 ? null : flags[outputIndex + 1];
|
|
96
|
+
if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
|
|
97
|
+
return "Missing path after --output.";
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
targetPath: commandTarget,
|
|
101
|
+
jsonOutput: flags.includes("--json"),
|
|
102
|
+
outputPath
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function parsePerformanceThresholds(flags) {
|
|
106
|
+
const thresholds = {};
|
|
107
|
+
const totalIndex = flags.indexOf("--max-total-ms");
|
|
108
|
+
if (totalIndex !== -1) {
|
|
109
|
+
const totalMs = parseNonNegativeNumber(flags[totalIndex + 1]);
|
|
110
|
+
if (totalMs === null) {
|
|
111
|
+
return "Missing or invalid number after --max-total-ms.";
|
|
112
|
+
}
|
|
113
|
+
thresholds.totalMs = totalMs;
|
|
114
|
+
}
|
|
115
|
+
for (let index = 0; index < flags.length; index += 1) {
|
|
116
|
+
if (flags[index] !== "--max-stage-ms") {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const value = flags[index + 1];
|
|
120
|
+
if (!value || value.startsWith("--") || !value.includes("=")) {
|
|
121
|
+
return "Missing or invalid stage threshold after --max-stage-ms. Use stage=milliseconds.";
|
|
122
|
+
}
|
|
123
|
+
const [stageName, rawLimit] = value.split("=", 2);
|
|
124
|
+
if (!performanceStageNames.has(stageName)) {
|
|
125
|
+
return `Unknown performance stage: ${stageName}.`;
|
|
126
|
+
}
|
|
127
|
+
const limitMs = parseNonNegativeNumber(rawLimit);
|
|
128
|
+
if (limitMs === null) {
|
|
129
|
+
return "Missing or invalid number after --max-stage-ms.";
|
|
130
|
+
}
|
|
131
|
+
thresholds.stages = {
|
|
132
|
+
...thresholds.stages,
|
|
133
|
+
[stageName]: limitMs
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return { thresholds };
|
|
73
137
|
}
|
|
74
138
|
function renderInstalledPlugins(plugins) {
|
|
75
139
|
const lines = [
|
|
@@ -252,6 +316,29 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
252
316
|
: renderDoctorOutputContract(contract, { outputPath }));
|
|
253
317
|
return 0;
|
|
254
318
|
}
|
|
319
|
+
if (maybePath === "mcp") {
|
|
320
|
+
const targetPath = remainingArgs[0] && !remainingArgs[0].startsWith("--")
|
|
321
|
+
? remainingArgs[0]
|
|
322
|
+
: "";
|
|
323
|
+
const mcpFlags = targetPath ? remainingArgs.slice(1) : remainingArgs;
|
|
324
|
+
const parsedMcpArgs = buildGenericMcpDoctorCommandArgs(targetPath, mcpFlags);
|
|
325
|
+
if (typeof parsedMcpArgs === "string") {
|
|
326
|
+
io.writeStderr(parsedMcpArgs);
|
|
327
|
+
return 2;
|
|
328
|
+
}
|
|
329
|
+
const report = await buildGenericMcpDoctor(parsedMcpArgs.targetPath, {
|
|
330
|
+
env: terminalContext.env,
|
|
331
|
+
platform: terminalContext.platform
|
|
332
|
+
});
|
|
333
|
+
const renderedReport = parsedMcpArgs.jsonOutput
|
|
334
|
+
? renderGenericMcpDoctorJson(report)
|
|
335
|
+
: renderGenericMcpDoctor(report);
|
|
336
|
+
if (parsedMcpArgs.outputPath) {
|
|
337
|
+
await writeFile(parsedMcpArgs.outputPath, renderedReport, "utf8");
|
|
338
|
+
}
|
|
339
|
+
io.writeStdout(renderedReport);
|
|
340
|
+
return report.exitCode;
|
|
341
|
+
}
|
|
255
342
|
if (maybePath === "corpus") {
|
|
256
343
|
const jsonOutput = remainingArgs.includes("--json");
|
|
257
344
|
const outputIndex = remainingArgs.indexOf("--output");
|
|
@@ -276,6 +363,60 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
276
363
|
return report.summary.status === "pass" ? 0 : 1;
|
|
277
364
|
}
|
|
278
365
|
if (maybePath === "attest") {
|
|
366
|
+
if (remainingArgs[0] === "verify") {
|
|
367
|
+
const artifactPath = remainingArgs[1] && !remainingArgs[1].startsWith("--")
|
|
368
|
+
? remainingArgs[1]
|
|
369
|
+
: null;
|
|
370
|
+
const verifyFlags = artifactPath ? remainingArgs.slice(2) : remainingArgs.slice(1);
|
|
371
|
+
const jsonOutput = verifyFlags.includes("--json");
|
|
372
|
+
const outputIndex = verifyFlags.indexOf("--output");
|
|
373
|
+
const outputPath = outputIndex === -1 ? null : verifyFlags[outputIndex + 1];
|
|
374
|
+
const targetIndex = verifyFlags.indexOf("--target");
|
|
375
|
+
const targetPath = targetIndex === -1 ? null : verifyFlags[targetIndex + 1];
|
|
376
|
+
const signKeyIndex = verifyFlags.indexOf("--sign-key");
|
|
377
|
+
const signKeyEnvIndex = verifyFlags.indexOf("--sign-key-env");
|
|
378
|
+
const signKeyEnv = signKeyEnvIndex === -1 ? null : verifyFlags[signKeyEnvIndex + 1];
|
|
379
|
+
if (!artifactPath) {
|
|
380
|
+
io.writeStderr("Missing attestation artifact path. Usage: codex-plugin-doctor doctor attest verify <attestation.json> --target <path> --sign-key-env <name>");
|
|
381
|
+
return 2;
|
|
382
|
+
}
|
|
383
|
+
if (targetIndex === -1 || !targetPath || targetPath.startsWith("--")) {
|
|
384
|
+
io.writeStderr("Missing target path after --target.");
|
|
385
|
+
return 2;
|
|
386
|
+
}
|
|
387
|
+
if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
|
|
388
|
+
io.writeStderr("Missing path after --output.");
|
|
389
|
+
return 2;
|
|
390
|
+
}
|
|
391
|
+
if (signKeyEnvIndex !== -1 && (!signKeyEnv || signKeyEnv.startsWith("--"))) {
|
|
392
|
+
io.writeStderr("Missing environment variable name after --sign-key-env.");
|
|
393
|
+
return 2;
|
|
394
|
+
}
|
|
395
|
+
if (signKeyIndex !== -1) {
|
|
396
|
+
io.writeStderr("Use --sign-key-env for verification; inline verification keys are not supported.");
|
|
397
|
+
return 2;
|
|
398
|
+
}
|
|
399
|
+
if (signKeyEnvIndex === -1) {
|
|
400
|
+
io.writeStderr("Missing signing key. Use --sign-key-env <name> for verification.");
|
|
401
|
+
return 2;
|
|
402
|
+
}
|
|
403
|
+
const envSigningKey = signKeyEnv ? terminalContext.env[signKeyEnv] : undefined;
|
|
404
|
+
if (signKeyEnv && !envSigningKey) {
|
|
405
|
+
io.writeStderr(`Environment variable ${signKeyEnv} is not set.`);
|
|
406
|
+
return 2;
|
|
407
|
+
}
|
|
408
|
+
const report = await verifyDoctorAttestation(artifactPath, targetPath, {
|
|
409
|
+
signingKey: envSigningKey
|
|
410
|
+
});
|
|
411
|
+
const renderedReport = jsonOutput
|
|
412
|
+
? renderDoctorAttestationVerificationJson(report)
|
|
413
|
+
: renderDoctorAttestationVerification(report, { outputPath });
|
|
414
|
+
if (outputPath) {
|
|
415
|
+
await writeFile(outputPath, renderedReport, "utf8");
|
|
416
|
+
}
|
|
417
|
+
io.writeStdout(renderedReport);
|
|
418
|
+
return report.exitCode;
|
|
419
|
+
}
|
|
279
420
|
const targetPath = remainingArgs[0] && !remainingArgs[0].startsWith("--")
|
|
280
421
|
? remainingArgs[0]
|
|
281
422
|
: ".";
|
|
@@ -285,11 +426,36 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
285
426
|
const jsonOutput = attestFlags.includes("--json");
|
|
286
427
|
const outputIndex = attestFlags.indexOf("--output");
|
|
287
428
|
const outputPath = outputIndex === -1 ? null : attestFlags[outputIndex + 1];
|
|
429
|
+
const signKeyIndex = attestFlags.indexOf("--sign-key");
|
|
430
|
+
const signKeyEnvIndex = attestFlags.indexOf("--sign-key-env");
|
|
431
|
+
const signKey = signKeyIndex === -1 ? null : attestFlags[signKeyIndex + 1];
|
|
432
|
+
const signKeyEnv = signKeyEnvIndex === -1 ? null : attestFlags[signKeyEnvIndex + 1];
|
|
288
433
|
if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
|
|
289
434
|
io.writeStderr("Missing path after --output.");
|
|
290
435
|
return 2;
|
|
291
436
|
}
|
|
292
|
-
|
|
437
|
+
if (signKeyIndex !== -1 && (!signKey || signKey.startsWith("--"))) {
|
|
438
|
+
io.writeStderr("Missing key after --sign-key.");
|
|
439
|
+
return 2;
|
|
440
|
+
}
|
|
441
|
+
if (signKeyEnvIndex !== -1 && (!signKeyEnv || signKeyEnv.startsWith("--"))) {
|
|
442
|
+
io.writeStderr("Missing environment variable name after --sign-key-env.");
|
|
443
|
+
return 2;
|
|
444
|
+
}
|
|
445
|
+
if (signKeyIndex !== -1 && signKeyEnvIndex !== -1) {
|
|
446
|
+
io.writeStderr("Use either --sign-key or --sign-key-env, not both.");
|
|
447
|
+
return 2;
|
|
448
|
+
}
|
|
449
|
+
const envSigningKey = signKeyEnv ? terminalContext.env[signKeyEnv] : undefined;
|
|
450
|
+
if (signKeyEnv && !envSigningKey) {
|
|
451
|
+
io.writeStderr(`Environment variable ${signKeyEnv} is not set.`);
|
|
452
|
+
return 2;
|
|
453
|
+
}
|
|
454
|
+
const attestation = await buildDoctorAttestation(targetPath, {
|
|
455
|
+
signingKey: signKey ?? envSigningKey,
|
|
456
|
+
signingKeyHint: signKeyEnv ? `env:${signKeyEnv}` : signKey ? "inline" : undefined,
|
|
457
|
+
recomputeKeyEnv: signKeyEnv ?? undefined
|
|
458
|
+
});
|
|
293
459
|
const attestationJson = renderDoctorAttestationJson(attestation);
|
|
294
460
|
if (outputPath) {
|
|
295
461
|
await writeFile(outputPath, attestationJson, "utf8");
|
|
@@ -429,6 +595,11 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
429
595
|
io.writeStderr("Missing path after --output.");
|
|
430
596
|
return 2;
|
|
431
597
|
}
|
|
598
|
+
const parsedThresholds = parsePerformanceThresholds(perfFlags);
|
|
599
|
+
if (typeof parsedThresholds === "string") {
|
|
600
|
+
io.writeStderr(parsedThresholds);
|
|
601
|
+
return 2;
|
|
602
|
+
}
|
|
432
603
|
const report = await buildDoctorPerformanceReport(targetPath, {
|
|
433
604
|
environment: {
|
|
434
605
|
env: terminalContext.env,
|
|
@@ -436,7 +607,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
436
607
|
},
|
|
437
608
|
runCheck: options.runCheckImpl
|
|
438
609
|
? (pathToCheck) => options.runCheckImpl(pathToCheck)
|
|
439
|
-
: undefined
|
|
610
|
+
: undefined,
|
|
611
|
+
thresholds: parsedThresholds.thresholds
|
|
440
612
|
});
|
|
441
613
|
const renderedReport = jsonOutput
|
|
442
614
|
? renderDoctorPerformanceReportJson(report)
|
|
@@ -689,26 +861,20 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
689
861
|
return audit.status === "fail" ? 1 : 0;
|
|
690
862
|
}
|
|
691
863
|
if (command === "mcp") {
|
|
692
|
-
|
|
693
|
-
|
|
864
|
+
const parsedMcpArgs = buildGenericMcpDoctorCommandArgs(maybePath ?? "", remainingArgs);
|
|
865
|
+
if (typeof parsedMcpArgs === "string") {
|
|
866
|
+
io.writeStderr(parsedMcpArgs);
|
|
694
867
|
return 2;
|
|
695
868
|
}
|
|
696
|
-
const
|
|
697
|
-
const outputIndex = remainingArgs.indexOf("--output");
|
|
698
|
-
const outputPath = outputIndex === -1 ? null : remainingArgs[outputIndex + 1];
|
|
699
|
-
if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
|
|
700
|
-
io.writeStderr("Missing path after --output.");
|
|
701
|
-
return 2;
|
|
702
|
-
}
|
|
703
|
-
const report = await buildGenericMcpDoctor(maybePath, {
|
|
869
|
+
const report = await buildGenericMcpDoctor(parsedMcpArgs.targetPath, {
|
|
704
870
|
env: terminalContext.env,
|
|
705
871
|
platform: terminalContext.platform
|
|
706
872
|
});
|
|
707
|
-
const renderedReport = jsonOutput
|
|
873
|
+
const renderedReport = parsedMcpArgs.jsonOutput
|
|
708
874
|
? renderGenericMcpDoctorJson(report)
|
|
709
875
|
: renderGenericMcpDoctor(report);
|
|
710
|
-
if (outputPath) {
|
|
711
|
-
await writeFile(outputPath, renderedReport, "utf8");
|
|
876
|
+
if (parsedMcpArgs.outputPath) {
|
|
877
|
+
await writeFile(parsedMcpArgs.outputPath, renderedReport, "utf8");
|
|
712
878
|
}
|
|
713
879
|
io.writeStdout(renderedReport);
|
|
714
880
|
return report.exitCode;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
process.stdin.resume();
|
package/package.json
CHANGED