codex-plugin-doctor 1.0.1 → 1.0.3

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 CHANGED
@@ -199,6 +199,8 @@ codex-plugin-doctor doctor npm <published-plugin-package> --json --output npm-pr
199
199
  codex-plugin-doctor doctor attest .
200
200
  codex-plugin-doctor doctor attest . --json --output attestation.json
201
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
202
204
  codex-plugin-doctor doctor inspector .
203
205
  codex-plugin-doctor doctor inspector . --server context7 --json --output inspector-command.json
204
206
  codex-plugin-doctor doctor diff --before ./old-plugin --after ./new-plugin
@@ -280,7 +282,7 @@ codex-plugin-doctor check . --json --runtime --verbose-runtime
280
282
 
281
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`.
282
284
 
283
- `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 deterministic local attestation with a package fingerprint, report digest, validation/security/compatibility/trust summary, and unsigned 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 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.
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 release-evidence <path> --sign-key-env NAME` creates one redacted release bundle with signed attestation, offline verification, corpus, performance, security, trust, package metadata, and git release gates. Strict release evidence requires a clean tagged worktree; use `--allow-dirty` or `--allow-untagged` only for local rehearsal. `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.
284
286
 
285
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.
286
288
 
@@ -346,9 +348,9 @@ jobs:
346
348
  runs-on: ubuntu-latest
347
349
  steps:
348
350
  - uses: actions/checkout@v5
349
- - uses: Esquetta/CodexPluginDoctor@v1.0.1
351
+ - uses: Esquetta/CodexPluginDoctor@v1.0.3
350
352
  with:
351
- version: "1.0.1"
353
+ version: "1.0.3"
352
354
  path: .
353
355
  runtime: "true"
354
356
  policy: codex-publish
@@ -66,9 +66,43 @@ export interface BuildDoctorAttestationOptions {
66
66
  signingKey?: string;
67
67
  signingKeyHint?: string;
68
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";
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;
96
+ artifactPath?: string;
69
97
  }
70
98
  export declare function buildDoctorAttestation(targetPath: string, options?: BuildDoctorAttestationOptions): Promise<DoctorAttestation>;
99
+ export declare function verifyDoctorAttestation(artifactPath: string, targetPath: string, options: VerifyDoctorAttestationOptions): Promise<DoctorAttestationVerificationReport>;
100
+ export declare function verifyDoctorAttestationObject(artifact: unknown, targetPath: string, options: VerifyDoctorAttestationOptions): Promise<DoctorAttestationVerificationReport>;
71
101
  export declare function renderDoctorAttestationJson(attestation: DoctorAttestation): string;
102
+ export declare function renderDoctorAttestationVerificationJson(report: DoctorAttestationVerificationReport): string;
103
+ export declare function renderDoctorAttestationVerification(report: DoctorAttestationVerificationReport, options?: {
104
+ outputPath?: string | null;
105
+ }): string;
72
106
  export declare function renderDoctorAttestation(attestation: DoctorAttestation, options?: {
73
107
  outputPath?: string | null;
74
108
  }): string;
@@ -1,4 +1,4 @@
1
- import { createHash, createHmac } 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: packageVersion,
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) {
@@ -188,18 +194,19 @@ export async function buildDoctorAttestation(targetPath, options = {}) {
188
194
  readPackageSubject(analysis.targetPath),
189
195
  readManifestSubject(analysis.targetPath)
190
196
  ]);
191
- const reportDigest = sha256(stableStringify(buildReportDigestPayload(analysis, packageFingerprint)));
197
+ const attestationVersion = options.versionOverride ?? packageVersion;
198
+ const reportDigest = sha256(stableStringify(buildReportDigestPayload(analysis, packageFingerprint, attestationVersion)));
192
199
  const subject = buildSubject(analysis, packageJson, manifest);
193
200
  const summary = buildSummary(analysis);
194
- const signingPayload = {
201
+ const signingPayload = buildSigningPayload({
195
202
  schemaVersion: "1.0.0",
196
203
  kind: "doctor.attestation.signature.v1",
197
- version: packageVersion,
204
+ version: attestationVersion,
198
205
  subject,
199
206
  packageFingerprint,
200
207
  reportDigest,
201
208
  summary
202
- };
209
+ });
203
210
  const signature = options.signingKey
204
211
  ? {
205
212
  status: "signed",
@@ -221,7 +228,7 @@ export async function buildDoctorAttestation(targetPath, options = {}) {
221
228
  schemaVersion: "1.0.0",
222
229
  kind: "doctor.attestation",
223
230
  generatedAt: analysis.generatedAt,
224
- version: packageVersion,
231
+ version: attestationVersion,
225
232
  targetPath: analysis.targetPath,
226
233
  subject,
227
234
  packageFingerprint,
@@ -243,9 +250,169 @@ export async function buildDoctorAttestation(targetPath, options = {}) {
243
250
  signature
244
251
  };
245
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
+ return verifyDoctorAttestationObject(artifact, targetPath, {
302
+ ...options,
303
+ artifactPath: resolvedArtifactPath
304
+ });
305
+ }
306
+ export async function verifyDoctorAttestationObject(artifact, targetPath, options) {
307
+ const artifactPath = options.artifactPath ?? "inline:doctor.attestation";
308
+ if (!isDoctorAttestation(artifact)) {
309
+ return {
310
+ schemaVersion: "1.0.0",
311
+ kind: "doctor.attestation.verification",
312
+ generatedAt: new Date().toISOString(),
313
+ artifactPath,
314
+ targetPath: path.resolve(targetPath),
315
+ status: "fail",
316
+ exitCode: 1,
317
+ summary: {
318
+ packageFingerprint: "fail",
319
+ reportDigest: "fail",
320
+ signature: "fail"
321
+ },
322
+ unsignedFields: [
323
+ "generatedAt",
324
+ "targetPath",
325
+ "verification",
326
+ "signature.keyHint"
327
+ ],
328
+ checks: [
329
+ {
330
+ id: "attestation.artifact.invalid",
331
+ status: "fail",
332
+ message: "The attestation artifact is not a valid doctor attestation."
333
+ }
334
+ ]
335
+ };
336
+ }
337
+ const expected = await buildDoctorAttestation(targetPath, {
338
+ signingKey: options.signingKey,
339
+ signingKeyHint: "verification",
340
+ versionOverride: artifact.version
341
+ });
342
+ const checks = [
343
+ createDigestVerificationCheck("attestation.package_fingerprint", "Package fingerprint matches the target package contents.", expected.packageFingerprint.digest, artifact.packageFingerprint.digest),
344
+ createDigestVerificationCheck("attestation.report_digest", "Report digest matches the target validation evidence.", expected.reportDigest.digest, artifact.reportDigest.digest)
345
+ ];
346
+ if (artifact.signature.status !== "signed") {
347
+ checks.push({
348
+ id: "attestation.signature.unsigned",
349
+ status: "fail",
350
+ message: "The attestation artifact is unsigned and cannot be verified with a signing key."
351
+ });
352
+ }
353
+ else {
354
+ const expectedSignature = signPayload(buildSigningPayload(artifact), options.signingKey);
355
+ 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));
356
+ }
357
+ const failedChecks = checks.filter((check) => check.status === "fail");
358
+ const packageFingerprint = checks.find((check) => check.id === "attestation.package_fingerprint")?.status ?? "fail";
359
+ const reportDigest = checks.find((check) => check.id === "attestation.report_digest")?.status ?? "fail";
360
+ const signature = checks
361
+ .filter((check) => check.id.startsWith("attestation.signature."))
362
+ .every((check) => check.status === "pass")
363
+ ? "pass"
364
+ : "fail";
365
+ return {
366
+ schemaVersion: "1.0.0",
367
+ kind: "doctor.attestation.verification",
368
+ generatedAt: new Date().toISOString(),
369
+ artifactPath,
370
+ targetPath: expected.targetPath,
371
+ status: failedChecks.length === 0 ? "pass" : "fail",
372
+ exitCode: failedChecks.length === 0 ? 0 : 1,
373
+ summary: {
374
+ packageFingerprint,
375
+ reportDigest,
376
+ signature
377
+ },
378
+ unsignedFields: [
379
+ "generatedAt",
380
+ "targetPath",
381
+ "verification",
382
+ "signature.keyHint"
383
+ ],
384
+ checks
385
+ };
386
+ }
246
387
  export function renderDoctorAttestationJson(attestation) {
247
388
  return JSON.stringify(attestation, null, 2);
248
389
  }
390
+ export function renderDoctorAttestationVerificationJson(report) {
391
+ return JSON.stringify(report, null, 2);
392
+ }
393
+ export function renderDoctorAttestationVerification(report, options = {}) {
394
+ const lines = [
395
+ "Doctor Attestation Verification",
396
+ "===============================",
397
+ `Artifact: ${report.artifactPath}`,
398
+ `Target: ${report.targetPath}`,
399
+ `Status: ${report.status.toUpperCase()}`,
400
+ `Package fingerprint: ${report.summary.packageFingerprint.toUpperCase()}`,
401
+ `Report digest: ${report.summary.reportDigest.toUpperCase()}`,
402
+ `Signature: ${report.summary.signature.toUpperCase()}`
403
+ ];
404
+ if (options.outputPath) {
405
+ lines.push(`Output: ${options.outputPath}`);
406
+ }
407
+ lines.push("", "Checks", "------");
408
+ for (const check of report.checks) {
409
+ lines.push(`${check.status === "pass" ? "PASS" : "FAIL"} ${check.id}`);
410
+ lines.push(` ${check.message}`);
411
+ }
412
+ lines.push("", "Unsigned metadata", "-----------------");
413
+ lines.push(report.unsignedFields.join(", "));
414
+ return lines.join("\n");
415
+ }
249
416
  export function renderDoctorAttestation(attestation, options = {}) {
250
417
  const lines = [
251
418
  "Doctor Attestation",
@@ -15,6 +15,8 @@ export interface DoctorExportBundle {
15
15
  recommendations: DoctorRecommendationsReport;
16
16
  trust: TrustScoreReport;
17
17
  }
18
+ export declare function redactString(value: string): string;
19
+ export declare function redactValue(value: unknown): unknown;
18
20
  export declare function buildDoctorExportBundle(targetPath: string, environment?: CompatibilityEnvironment): Promise<DoctorExportBundle>;
19
21
  export declare function renderDoctorExportBundleJson(bundle: DoctorExportBundle): string;
20
22
  export declare function renderDoctorExportBundle(bundle: DoctorExportBundle, options?: {
@@ -1,12 +1,12 @@
1
1
  import { buildDoctorExportBundleFromAnalysis, buildDoctorRecommendationsFromAnalysis, buildPackageAnalysis } from "./package-analysis.js";
2
- function redactString(value) {
2
+ export function redactString(value) {
3
3
  return value
4
4
  .replace(/sk-[A-Za-z0-9_-]{12,}/g, "[REDACTED_SECRET]")
5
5
  .replace(/npm_[A-Za-z0-9_-]{12,}/g, "[REDACTED_SECRET]")
6
6
  .replace(/gh[pousr]_[A-Za-z0-9_]{12,}/g, "[REDACTED_SECRET]")
7
7
  .replace(/SHOULD_NOT_LEAK/g, "[REDACTED_SECRET]");
8
8
  }
9
- function redactValue(value) {
9
+ export function redactValue(value) {
10
10
  if (typeof value === "string") {
11
11
  return redactString(value);
12
12
  }
@@ -83,6 +83,18 @@ const publicSchemaDefinitions = [
83
83
  outputKind: "doctor.attestation",
84
84
  required: ["schemaVersion", "kind", "generatedAt", "targetPath", "subject", "packageFingerprint", "reportDigest", "summary", "verification", "signature"]
85
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
+ },
92
+ {
93
+ id: "doctor.release.evidence.json",
94
+ command: "codex-plugin-doctor doctor release-evidence <path> --json",
95
+ outputKind: "doctor.release.evidence",
96
+ required: ["schemaVersion", "kind", "generatedAt", "version", "targetPath", "status", "exitCode", "releaseReady", "summary", "package", "git", "releaseGates", "attestation", "attestationVerification", "corpus", "performance", "security", "trust"]
97
+ },
86
98
  {
87
99
  id: "doctor.npm.json",
88
100
  command: "codex-plugin-doctor doctor npm <package> --json",
@@ -0,0 +1,66 @@
1
+ import { type DoctorAttestation, type DoctorAttestationVerificationReport } from "./attestation.js";
2
+ import { type DoctorPerformanceReport, type DoctorPerformanceThresholdOptions } from "./performance-report.js";
3
+ import { type DoctorValidationCorpusReport } from "./validation-corpus.js";
4
+ import { type SecurityAudit } from "../security/security-audit.js";
5
+ import { type TrustScoreReport } from "../security/trust-score.js";
6
+ import type { CompatibilityEnvironment } from "../compatibility/compatibility-matrix.js";
7
+ import type { CheckResult } from "../domain/types.js";
8
+ type EvidenceStatus = "pass" | "warn" | "fail";
9
+ export interface DoctorReleaseEvidencePackageMetadata {
10
+ name: string | null;
11
+ version: string | null;
12
+ private: boolean | null;
13
+ }
14
+ export interface DoctorReleaseEvidenceGitMetadata {
15
+ commit: string | null;
16
+ tag: string | null;
17
+ dirty: boolean | null;
18
+ }
19
+ export interface DoctorReleaseEvidenceReport {
20
+ schemaVersion: "1.0.0";
21
+ kind: "doctor.release.evidence";
22
+ generatedAt: string;
23
+ version: string;
24
+ targetPath: string;
25
+ status: "pass" | "fail";
26
+ exitCode: 0 | 1;
27
+ releaseReady: boolean;
28
+ summary: {
29
+ attestation: EvidenceStatus;
30
+ attestationVerification: EvidenceStatus;
31
+ corpus: EvidenceStatus;
32
+ performance: EvidenceStatus;
33
+ releaseGates: EvidenceStatus;
34
+ security: EvidenceStatus;
35
+ trust: EvidenceStatus;
36
+ };
37
+ releaseGates: {
38
+ status: EvidenceStatus;
39
+ checks: Array<{
40
+ id: string;
41
+ status: EvidenceStatus;
42
+ message: string;
43
+ }>;
44
+ };
45
+ package: DoctorReleaseEvidencePackageMetadata;
46
+ git: DoctorReleaseEvidenceGitMetadata;
47
+ attestation: DoctorAttestation;
48
+ attestationVerification: DoctorAttestationVerificationReport;
49
+ corpus: DoctorValidationCorpusReport;
50
+ performance: DoctorPerformanceReport;
51
+ security: Pick<SecurityAudit, "status" | "score" | "findingCounts">;
52
+ trust: Pick<TrustScoreReport, "status" | "score" | "findingCounts">;
53
+ }
54
+ export interface BuildDoctorReleaseEvidenceOptions {
55
+ signingKey: string;
56
+ signingKeyEnv: string;
57
+ allowDirty?: boolean;
58
+ allowUntagged?: boolean;
59
+ environment?: CompatibilityEnvironment;
60
+ runCheck?: (targetPath: string) => Promise<CheckResult>;
61
+ performanceThresholds?: DoctorPerformanceThresholdOptions;
62
+ }
63
+ export declare function buildDoctorReleaseEvidenceReport(targetPath: string, options: BuildDoctorReleaseEvidenceOptions): Promise<DoctorReleaseEvidenceReport>;
64
+ export declare function renderDoctorReleaseEvidenceJson(report: DoctorReleaseEvidenceReport): string;
65
+ export declare function renderDoctorReleaseEvidence(report: DoctorReleaseEvidenceReport): string;
66
+ export {};
@@ -0,0 +1,196 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import path from "node:path";
4
+ import { buildDoctorAttestation, verifyDoctorAttestationObject } from "./attestation.js";
5
+ import { buildDoctorPerformanceReport } from "./performance-report.js";
6
+ import { buildDoctorValidationCorpusReport } from "./validation-corpus.js";
7
+ import { redactValue } from "./doctor-export-bundle.js";
8
+ import { readJsonFile } from "./read-json-file.js";
9
+ import { buildSecurityAudit } from "../security/security-audit.js";
10
+ import { buildTrustScore } from "../security/trust-score.js";
11
+ import { packageVersion } from "../version.js";
12
+ const execFileAsync = promisify(execFile);
13
+ function toEvidenceStatus(status) {
14
+ return status;
15
+ }
16
+ async function readPackageMetadata(rootPath) {
17
+ try {
18
+ const packageJson = await readJsonFile(path.join(rootPath, "package.json"));
19
+ return {
20
+ name: typeof packageJson.name === "string" ? packageJson.name : null,
21
+ version: typeof packageJson.version === "string" ? packageJson.version : null,
22
+ private: typeof packageJson.private === "boolean" ? packageJson.private : null
23
+ };
24
+ }
25
+ catch {
26
+ return {
27
+ name: null,
28
+ version: null,
29
+ private: null
30
+ };
31
+ }
32
+ }
33
+ async function readGitValue(args, cwd) {
34
+ try {
35
+ const { stdout } = await execFileAsync("git", args, { cwd });
36
+ const value = stdout.trim();
37
+ return value.length > 0 ? value : null;
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ }
43
+ async function readGitDirty(cwd) {
44
+ try {
45
+ const { stdout } = await execFileAsync("git", ["status", "--short"], { cwd });
46
+ return stdout.trim().length > 0;
47
+ }
48
+ catch {
49
+ return null;
50
+ }
51
+ }
52
+ async function readGitMetadata(rootPath) {
53
+ const [commit, tag, dirtyOutput] = await Promise.all([
54
+ readGitValue(["rev-parse", "HEAD"], rootPath),
55
+ readGitValue(["describe", "--tags", "--exact-match"], rootPath),
56
+ readGitDirty(rootPath)
57
+ ]);
58
+ return {
59
+ commit,
60
+ tag,
61
+ dirty: dirtyOutput
62
+ };
63
+ }
64
+ function releaseReady(report) {
65
+ return report.attestation.summary.status !== "fail" &&
66
+ report.attestationVerification.status === "pass" &&
67
+ report.corpus.summary.status === "pass" &&
68
+ report.performance.status === "pass" &&
69
+ report.releaseGates.status === "pass" &&
70
+ report.security.status !== "fail" &&
71
+ report.trust.status !== "fail";
72
+ }
73
+ function buildReleaseGateReport(git, options) {
74
+ const checks = [
75
+ {
76
+ id: "git.commit.present",
77
+ status: git.commit ? "pass" : "fail",
78
+ message: git.commit
79
+ ? "Git commit resolved for this release evidence bundle."
80
+ : "Git commit could not be resolved."
81
+ },
82
+ {
83
+ id: "git.tag.exact",
84
+ status: git.tag || options.allowUntagged ? "pass" : "fail",
85
+ message: git.tag
86
+ ? `Current commit is tagged as ${git.tag}.`
87
+ : options.allowUntagged
88
+ ? "Current commit is not on an exact git tag, but --allow-untagged was explicitly set."
89
+ : "Current commit is not on an exact git tag."
90
+ },
91
+ {
92
+ id: "git.worktree.clean",
93
+ status: git.dirty === false || options.allowDirty ? "pass" : "fail",
94
+ message: git.dirty === false
95
+ ? "Git worktree is clean."
96
+ : options.allowDirty
97
+ ? "Git worktree is dirty, but --allow-dirty was explicitly set."
98
+ : "Git worktree has uncommitted changes."
99
+ }
100
+ ];
101
+ return {
102
+ status: checks.every((check) => check.status === "pass") ? "pass" : "fail",
103
+ checks
104
+ };
105
+ }
106
+ export async function buildDoctorReleaseEvidenceReport(targetPath, options) {
107
+ const rootPath = path.resolve(targetPath);
108
+ const security = await buildSecurityAudit(rootPath);
109
+ const [attestation, corpus, performance, trust, packageMetadata, git] = await Promise.all([
110
+ buildDoctorAttestation(rootPath, {
111
+ signingKey: options.signingKey,
112
+ signingKeyHint: `env:${options.signingKeyEnv}`,
113
+ recomputeKeyEnv: options.signingKeyEnv
114
+ }),
115
+ buildDoctorValidationCorpusReport({ environment: options.environment }),
116
+ buildDoctorPerformanceReport(rootPath, {
117
+ environment: options.environment,
118
+ runCheck: options.runCheck,
119
+ thresholds: options.performanceThresholds
120
+ }),
121
+ buildTrustScore(rootPath, { securityAudit: security }),
122
+ readPackageMetadata(rootPath),
123
+ readGitMetadata(rootPath)
124
+ ]);
125
+ const normalizedPackageMetadata = {
126
+ name: packageMetadata.name ?? attestation.subject.name,
127
+ version: packageMetadata.version ?? attestation.subject.version,
128
+ private: packageMetadata.private
129
+ };
130
+ const attestationVerification = await verifyDoctorAttestationObject(attestation, rootPath, {
131
+ signingKey: options.signingKey,
132
+ artifactPath: "inline:doctor.release-evidence.attestation"
133
+ });
134
+ const releaseGates = buildReleaseGateReport(git, options);
135
+ const partialReport = {
136
+ schemaVersion: "1.0.0",
137
+ kind: "doctor.release.evidence",
138
+ generatedAt: new Date().toISOString(),
139
+ version: packageVersion,
140
+ targetPath: rootPath,
141
+ summary: {
142
+ attestation: toEvidenceStatus(attestation.summary.status),
143
+ attestationVerification: attestationVerification.status,
144
+ corpus: corpus.summary.status,
145
+ performance: performance.status,
146
+ releaseGates: releaseGates.status,
147
+ security: toEvidenceStatus(security.status),
148
+ trust: toEvidenceStatus(trust.status)
149
+ },
150
+ package: normalizedPackageMetadata,
151
+ git,
152
+ releaseGates,
153
+ attestation,
154
+ attestationVerification,
155
+ corpus,
156
+ performance,
157
+ security: {
158
+ status: security.status,
159
+ score: security.score,
160
+ findingCounts: security.findingCounts
161
+ },
162
+ trust: {
163
+ status: trust.status,
164
+ score: trust.score,
165
+ findingCounts: trust.findingCounts
166
+ }
167
+ };
168
+ const ready = releaseReady(partialReport);
169
+ return {
170
+ ...partialReport,
171
+ status: ready ? "pass" : "fail",
172
+ exitCode: ready ? 0 : 1,
173
+ releaseReady: ready
174
+ };
175
+ }
176
+ export function renderDoctorReleaseEvidenceJson(report) {
177
+ return JSON.stringify(redactValue(report), null, 2);
178
+ }
179
+ export function renderDoctorReleaseEvidence(report) {
180
+ return [
181
+ "Doctor Release Evidence",
182
+ "=======================",
183
+ `Target: ${report.targetPath}`,
184
+ `Status: ${report.status.toUpperCase()}`,
185
+ `Release ready: ${report.releaseReady ? "yes" : "no"}`,
186
+ `Attestation: ${report.summary.attestation.toUpperCase()}`,
187
+ `Attestation verification: ${report.summary.attestationVerification.toUpperCase()}`,
188
+ `Corpus: ${report.summary.corpus.toUpperCase()}`,
189
+ `Performance: ${report.summary.performance.toUpperCase()}`,
190
+ `Release gates: ${report.summary.releaseGates.toUpperCase()}`,
191
+ `Security: ${report.summary.security.toUpperCase()} (${report.security.score}/100)`,
192
+ `Trust: ${report.summary.trust.toUpperCase()} (${report.trust.score}/100)`,
193
+ `Git commit: ${report.git.commit ?? "unknown"}`,
194
+ `Git tag: ${report.git.tag ?? "not tagged"}`
195
+ ].join("\n");
196
+ }
package/dist/index.d.ts CHANGED
@@ -4,11 +4,12 @@ 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";
11
11
  export { buildDoctorPerformanceReport, renderDoctorPerformanceReport, renderDoctorPerformanceReportJson, type BuildDoctorPerformanceReportOptions, type DoctorPerformanceReport, type DoctorPerformanceStage, type DoctorPerformanceStageName } from "./core/performance-report.js";
12
+ export { buildDoctorReleaseEvidenceReport, renderDoctorReleaseEvidence, renderDoctorReleaseEvidenceJson, type BuildDoctorReleaseEvidenceOptions, type DoctorReleaseEvidenceGitMetadata, type DoctorReleaseEvidencePackageMetadata, type DoctorReleaseEvidenceReport } from "./core/release-evidence.js";
12
13
  export { buildDoctorNpmPackageReport, renderDoctorNpmPackageReport, renderDoctorNpmPackageReportJson, type BuildDoctorNpmPackageReportOptions, type DoctorNpmPackageReport } from "./core/npm-package-doctor.js";
13
14
  export { buildDoctorRiskDiffReport, renderDoctorRiskDiffReport, renderDoctorRiskDiffReportJson, type BuildDoctorRiskDiffReportOptions, type DoctorRiskDiffReport, type RiskDiffFinding, type RiskFindingCategory } from "./core/risk-diff.js";
14
15
  export { buildDoctorInspectorReport, renderDoctorInspectorReport, renderDoctorInspectorReportJson, type BuildDoctorInspectorReportOptions, type DoctorInspectorReport } from "./core/inspector-bridge.js";
package/dist/index.js CHANGED
@@ -4,11 +4,12 @@ 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";
11
11
  export { buildDoctorPerformanceReport, renderDoctorPerformanceReport, renderDoctorPerformanceReportJson } from "./core/performance-report.js";
12
+ export { buildDoctorReleaseEvidenceReport, renderDoctorReleaseEvidence, renderDoctorReleaseEvidenceJson } from "./core/release-evidence.js";
12
13
  export { buildDoctorNpmPackageReport, renderDoctorNpmPackageReport, renderDoctorNpmPackageReportJson } from "./core/npm-package-doctor.js";
13
14
  export { buildDoctorRiskDiffReport, renderDoctorRiskDiffReport, renderDoctorRiskDiffReportJson } from "./core/risk-diff.js";
14
15
  export { buildDoctorInspectorReport, renderDoctorInspectorReport, renderDoctorInspectorReportJson } from "./core/inspector-bridge.js";
package/dist/run-cli.js CHANGED
@@ -16,10 +16,11 @@ 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";
23
+ import { buildDoctorReleaseEvidenceReport, renderDoctorReleaseEvidence, renderDoctorReleaseEvidenceJson } from "./core/release-evidence.js";
23
24
  import { buildDoctorNpmPackageReport, renderDoctorNpmPackageReport, renderDoctorNpmPackageReportJson } from "./core/npm-package-doctor.js";
24
25
  import { buildDoctorRiskDiffReport, renderDoctorRiskDiffReport, renderDoctorRiskDiffReportJson } from "./core/risk-diff.js";
25
26
  import { buildDoctorInspectorReport, renderDoctorInspectorReport, renderDoctorInspectorReportJson } from "./core/inspector-bridge.js";
@@ -69,7 +70,7 @@ const defaultIo = {
69
70
  }
70
71
  };
71
72
  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> [--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
+ 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|release-evidence <path> --sign-key-env NAME [--allow-dirty] [--allow-untagged]|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
  }
74
75
  const performanceStageNames = new Set([
75
76
  "validation",
@@ -339,6 +340,69 @@ export async function runCli(args, io = defaultIo, options = {}) {
339
340
  io.writeStdout(renderedReport);
340
341
  return report.exitCode;
341
342
  }
343
+ if (maybePath === "release-evidence") {
344
+ const targetPath = remainingArgs[0] && !remainingArgs[0].startsWith("--")
345
+ ? remainingArgs[0]
346
+ : ".";
347
+ const evidenceFlags = remainingArgs[0] && !remainingArgs[0].startsWith("--")
348
+ ? remainingArgs.slice(1)
349
+ : remainingArgs;
350
+ const jsonOutput = evidenceFlags.includes("--json");
351
+ const outputIndex = evidenceFlags.indexOf("--output");
352
+ const outputPath = outputIndex === -1 ? null : evidenceFlags[outputIndex + 1];
353
+ const signKeyIndex = evidenceFlags.indexOf("--sign-key");
354
+ const signKeyEnvIndex = evidenceFlags.indexOf("--sign-key-env");
355
+ const signKeyEnv = signKeyEnvIndex === -1 ? null : evidenceFlags[signKeyEnvIndex + 1];
356
+ const allowDirty = evidenceFlags.includes("--allow-dirty");
357
+ const allowUntagged = evidenceFlags.includes("--allow-untagged");
358
+ if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
359
+ io.writeStderr("Missing path after --output.");
360
+ return 2;
361
+ }
362
+ if (signKeyIndex !== -1) {
363
+ io.writeStderr("Use --sign-key-env for release evidence; inline signing keys are not supported.");
364
+ return 2;
365
+ }
366
+ if (signKeyEnvIndex === -1) {
367
+ io.writeStderr("Missing signing key. Use --sign-key-env <name>.");
368
+ return 2;
369
+ }
370
+ if (!signKeyEnv || signKeyEnv.startsWith("--")) {
371
+ io.writeStderr("Missing environment variable name after --sign-key-env.");
372
+ return 2;
373
+ }
374
+ const signingKey = terminalContext.env[signKeyEnv];
375
+ if (!signingKey) {
376
+ io.writeStderr(`Environment variable ${signKeyEnv} is not set.`);
377
+ return 2;
378
+ }
379
+ const parsedThresholds = parsePerformanceThresholds(evidenceFlags);
380
+ if (typeof parsedThresholds === "string") {
381
+ io.writeStderr(parsedThresholds);
382
+ return 2;
383
+ }
384
+ const report = await buildDoctorReleaseEvidenceReport(targetPath, {
385
+ signingKey,
386
+ signingKeyEnv: signKeyEnv,
387
+ allowDirty,
388
+ allowUntagged,
389
+ environment: {
390
+ env: terminalContext.env,
391
+ platform: terminalContext.platform
392
+ },
393
+ runCheck: options.runCheckImpl
394
+ ? (pathToCheck) => options.runCheckImpl(pathToCheck)
395
+ : undefined,
396
+ performanceThresholds: parsedThresholds.thresholds
397
+ });
398
+ const reportJson = renderDoctorReleaseEvidenceJson(report);
399
+ const renderedReport = jsonOutput ? reportJson : renderDoctorReleaseEvidence(report);
400
+ if (outputPath) {
401
+ await writeFile(outputPath, reportJson, "utf8");
402
+ }
403
+ io.writeStdout(renderedReport);
404
+ return report.exitCode;
405
+ }
342
406
  if (maybePath === "corpus") {
343
407
  const jsonOutput = remainingArgs.includes("--json");
344
408
  const outputIndex = remainingArgs.indexOf("--output");
@@ -363,6 +427,60 @@ export async function runCli(args, io = defaultIo, options = {}) {
363
427
  return report.summary.status === "pass" ? 0 : 1;
364
428
  }
365
429
  if (maybePath === "attest") {
430
+ if (remainingArgs[0] === "verify") {
431
+ const artifactPath = remainingArgs[1] && !remainingArgs[1].startsWith("--")
432
+ ? remainingArgs[1]
433
+ : null;
434
+ const verifyFlags = artifactPath ? remainingArgs.slice(2) : remainingArgs.slice(1);
435
+ const jsonOutput = verifyFlags.includes("--json");
436
+ const outputIndex = verifyFlags.indexOf("--output");
437
+ const outputPath = outputIndex === -1 ? null : verifyFlags[outputIndex + 1];
438
+ const targetIndex = verifyFlags.indexOf("--target");
439
+ const targetPath = targetIndex === -1 ? null : verifyFlags[targetIndex + 1];
440
+ const signKeyIndex = verifyFlags.indexOf("--sign-key");
441
+ const signKeyEnvIndex = verifyFlags.indexOf("--sign-key-env");
442
+ const signKeyEnv = signKeyEnvIndex === -1 ? null : verifyFlags[signKeyEnvIndex + 1];
443
+ if (!artifactPath) {
444
+ io.writeStderr("Missing attestation artifact path. Usage: codex-plugin-doctor doctor attest verify <attestation.json> --target <path> --sign-key-env <name>");
445
+ return 2;
446
+ }
447
+ if (targetIndex === -1 || !targetPath || targetPath.startsWith("--")) {
448
+ io.writeStderr("Missing target path after --target.");
449
+ return 2;
450
+ }
451
+ if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
452
+ io.writeStderr("Missing path after --output.");
453
+ return 2;
454
+ }
455
+ if (signKeyEnvIndex !== -1 && (!signKeyEnv || signKeyEnv.startsWith("--"))) {
456
+ io.writeStderr("Missing environment variable name after --sign-key-env.");
457
+ return 2;
458
+ }
459
+ if (signKeyIndex !== -1) {
460
+ io.writeStderr("Use --sign-key-env for verification; inline verification keys are not supported.");
461
+ return 2;
462
+ }
463
+ if (signKeyEnvIndex === -1) {
464
+ io.writeStderr("Missing signing key. Use --sign-key-env <name> for verification.");
465
+ return 2;
466
+ }
467
+ const envSigningKey = signKeyEnv ? terminalContext.env[signKeyEnv] : undefined;
468
+ if (signKeyEnv && !envSigningKey) {
469
+ io.writeStderr(`Environment variable ${signKeyEnv} is not set.`);
470
+ return 2;
471
+ }
472
+ const report = await verifyDoctorAttestation(artifactPath, targetPath, {
473
+ signingKey: envSigningKey
474
+ });
475
+ const renderedReport = jsonOutput
476
+ ? renderDoctorAttestationVerificationJson(report)
477
+ : renderDoctorAttestationVerification(report, { outputPath });
478
+ if (outputPath) {
479
+ await writeFile(outputPath, renderedReport, "utf8");
480
+ }
481
+ io.writeStdout(renderedReport);
482
+ return report.exitCode;
483
+ }
366
484
  const targetPath = remainingArgs[0] && !remainingArgs[0].startsWith("--")
367
485
  ? remainingArgs[0]
368
486
  : ".";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-plugin-doctor",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "CLI-first validator for Codex plugins, skills, and MCP package surfaces with runtime MCP protocol validation.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",