codex-plugin-doctor 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -9
- package/dist/core/output-contract.js +7 -1
- package/dist/core/release-evidence.d.ts +5 -0
- package/dist/core/release-evidence.js +13 -2
- package/dist/core/runtime-plan.d.ts +51 -0
- package/dist/core/runtime-plan.js +232 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/run-cli.js +81 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -64,7 +64,7 @@ Security scorecard with `security`:
|
|
|
64
64
|
- MCP server `cwd` paths that escape the package root
|
|
65
65
|
- plain HTTP remote transport warnings
|
|
66
66
|
|
|
67
|
-
Runtime MCP validation with `--runtime`:
|
|
67
|
+
Runtime MCP validation with `--runtime`:
|
|
68
68
|
|
|
69
69
|
- `initialize`
|
|
70
70
|
- `notifications/initialized`
|
|
@@ -76,8 +76,9 @@ Runtime MCP validation with `--runtime`:
|
|
|
76
76
|
- `prompts/list`
|
|
77
77
|
- `prompts/get`
|
|
78
78
|
- paginated list responses
|
|
79
|
-
- runtime capability scorecard
|
|
80
|
-
- redacted verbose transcript with `--verbose-runtime`
|
|
79
|
+
- runtime capability scorecard
|
|
80
|
+
- redacted verbose transcript with `--verbose-runtime`
|
|
81
|
+
- optional runtime approval gating with a precomputed `doctor runtime-plan` digest
|
|
81
82
|
|
|
82
83
|
Output formats:
|
|
83
84
|
|
|
@@ -212,6 +213,8 @@ codex-plugin-doctor doctor trust . --json --output trust-score.json
|
|
|
212
213
|
codex-plugin-doctor doctor perf .
|
|
213
214
|
codex-plugin-doctor doctor perf . --json --output perf.json
|
|
214
215
|
codex-plugin-doctor doctor perf . --max-total-ms 2500 --max-stage-ms validation=500
|
|
216
|
+
codex-plugin-doctor doctor runtime-plan .
|
|
217
|
+
codex-plugin-doctor doctor runtime-plan . --json --output runtime-plan.json
|
|
215
218
|
codex-plugin-doctor doctor mcp .
|
|
216
219
|
codex-plugin-doctor doctor mcp . --json --output mcp-healthcheck.json
|
|
217
220
|
codex-plugin-doctor doctor export --bundle .
|
|
@@ -267,9 +270,10 @@ codex-plugin-doctor check . --badge-json --output doctor-badge.json
|
|
|
267
270
|
codex-plugin-doctor check . --badge-markdown
|
|
268
271
|
codex-plugin-doctor check . --sarif --output results.sarif
|
|
269
272
|
codex-plugin-doctor check . --ascii
|
|
270
|
-
codex-plugin-doctor check . --no-animations
|
|
271
|
-
codex-plugin-doctor check . --runtime
|
|
272
|
-
codex-plugin-doctor check . --
|
|
273
|
+
codex-plugin-doctor check . --no-animations
|
|
274
|
+
codex-plugin-doctor check . --runtime
|
|
275
|
+
codex-plugin-doctor check . --runtime --require-runtime-approval --runtime-approval-digest sha256:<approved-plan-digest>
|
|
276
|
+
codex-plugin-doctor check . --config .codex-doctor.json
|
|
273
277
|
codex-plugin-doctor check . --history validation-history.jsonl
|
|
274
278
|
codex-plugin-doctor history validation-history.jsonl
|
|
275
279
|
codex-plugin-doctor history validation-history.jsonl --json
|
|
@@ -282,7 +286,7 @@ codex-plugin-doctor check . --json --runtime --verbose-runtime
|
|
|
282
286
|
|
|
283
287
|
`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`.
|
|
284
288
|
|
|
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,
|
|
289
|
+
`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 runtime-plan <path>` creates a non-executing runtime plan that lists MCP server commands, safe probe methods, risk reasons, and a stable approval digest before any local server is started. `check --runtime --require-runtime-approval --runtime-approval-digest <digest>` refuses to run runtime probes unless the current plan digest matches the approved digest. `doctor release-evidence <path> --sign-key-env NAME` creates one redacted release bundle with signed attestation, offline verification, corpus, performance, security, trust, package metadata, git release gates, and runtime approval status. Strict release evidence requires a clean tagged worktree; use `--allow-dirty` or `--allow-untagged` only for local rehearsal. `doctor release-evidence verify <evidence.json> --target <path> --sign-key-env NAME` verifies a shared release evidence artifact offline against an explicit package path; the artifact target path is treated as display metadata, not trusted input. `doctor release-evidence asset <path> --tag <tag> --output <evidence.json> --sign-key-env NAME` writes a signed release evidence file and prints the `gh release upload` command; add `--upload` to run the upload through GitHub CLI with `--clobber`. `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.
|
|
286
290
|
|
|
287
291
|
`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.
|
|
288
292
|
|
|
@@ -348,9 +352,9 @@ jobs:
|
|
|
348
352
|
runs-on: ubuntu-latest
|
|
349
353
|
steps:
|
|
350
354
|
- uses: actions/checkout@v5
|
|
351
|
-
- uses: Esquetta/CodexPluginDoctor@v1.
|
|
355
|
+
- uses: Esquetta/CodexPluginDoctor@v1.2.0
|
|
352
356
|
with:
|
|
353
|
-
version: "1.
|
|
357
|
+
version: "1.2.0"
|
|
354
358
|
path: .
|
|
355
359
|
runtime: "true"
|
|
356
360
|
policy: codex-publish
|
|
@@ -71,6 +71,12 @@ const publicSchemaDefinitions = [
|
|
|
71
71
|
outputKind: "doctor.perf",
|
|
72
72
|
required: ["schemaVersion", "kind", "generatedAt", "targetPath", "status", "exitCode", "summary", "stages", "thresholds"]
|
|
73
73
|
},
|
|
74
|
+
{
|
|
75
|
+
id: "doctor.runtime.plan.json",
|
|
76
|
+
command: "codex-plugin-doctor doctor runtime-plan <path> --json",
|
|
77
|
+
outputKind: "doctor.runtime.plan",
|
|
78
|
+
required: ["schemaVersion", "kind", "generatedAt", "version", "targetPath", "status", "exitCode", "runtimeExecution", "digest", "summary", "servers", "findings"]
|
|
79
|
+
},
|
|
74
80
|
{
|
|
75
81
|
id: "doctor.export.bundle.json",
|
|
76
82
|
command: "codex-plugin-doctor doctor export --bundle <path> --json",
|
|
@@ -93,7 +99,7 @@ const publicSchemaDefinitions = [
|
|
|
93
99
|
id: "doctor.release.evidence.json",
|
|
94
100
|
command: "codex-plugin-doctor doctor release-evidence <path> --json",
|
|
95
101
|
outputKind: "doctor.release.evidence",
|
|
96
|
-
required: ["schemaVersion", "kind", "generatedAt", "version", "targetPath", "status", "exitCode", "releaseReady", "summary", "package", "git", "releaseGates", "attestation", "attestationVerification", "corpus", "performance", "security", "trust", "evidenceSignature"]
|
|
102
|
+
required: ["schemaVersion", "kind", "generatedAt", "version", "targetPath", "status", "exitCode", "releaseReady", "summary", "package", "git", "releaseGates", "runtimeApproval", "attestation", "attestationVerification", "corpus", "performance", "security", "trust", "evidenceSignature"]
|
|
97
103
|
},
|
|
98
104
|
{
|
|
99
105
|
id: "doctor.release.evidence.verification.json",
|
|
@@ -3,6 +3,7 @@ import { type DoctorPerformanceReport, type DoctorPerformanceThresholdOptions }
|
|
|
3
3
|
import { type DoctorValidationCorpusReport } from "./validation-corpus.js";
|
|
4
4
|
import { type SecurityAudit } from "../security/security-audit.js";
|
|
5
5
|
import { type TrustScoreReport } from "../security/trust-score.js";
|
|
6
|
+
import { type RuntimeApprovalReport } from "./runtime-plan.js";
|
|
6
7
|
import type { CompatibilityEnvironment } from "../compatibility/compatibility-matrix.js";
|
|
7
8
|
import type { CheckResult } from "../domain/types.js";
|
|
8
9
|
type EvidenceStatus = "pass" | "warn" | "fail";
|
|
@@ -38,6 +39,7 @@ export interface DoctorReleaseEvidenceReport {
|
|
|
38
39
|
corpus: EvidenceStatus;
|
|
39
40
|
performance: EvidenceStatus;
|
|
40
41
|
releaseGates: EvidenceStatus;
|
|
42
|
+
runtimeApproval: EvidenceStatus;
|
|
41
43
|
security: EvidenceStatus;
|
|
42
44
|
trust: EvidenceStatus;
|
|
43
45
|
};
|
|
@@ -49,6 +51,7 @@ export interface DoctorReleaseEvidenceReport {
|
|
|
49
51
|
message: string;
|
|
50
52
|
}>;
|
|
51
53
|
};
|
|
54
|
+
runtimeApproval: RuntimeApprovalReport;
|
|
52
55
|
package: DoctorReleaseEvidencePackageMetadata;
|
|
53
56
|
git: DoctorReleaseEvidenceGitMetadata;
|
|
54
57
|
attestation: DoctorAttestation;
|
|
@@ -104,6 +107,8 @@ export interface BuildDoctorReleaseEvidenceOptions {
|
|
|
104
107
|
signingKeyEnv: string;
|
|
105
108
|
allowDirty?: boolean;
|
|
106
109
|
allowUntagged?: boolean;
|
|
110
|
+
requireRuntimeApproval?: boolean;
|
|
111
|
+
runtimeApprovalDigest?: string | null;
|
|
107
112
|
environment?: CompatibilityEnvironment;
|
|
108
113
|
runCheck?: (targetPath: string) => Promise<CheckResult>;
|
|
109
114
|
performanceThresholds?: DoctorPerformanceThresholdOptions;
|
|
@@ -9,6 +9,7 @@ import { redactValue } from "./doctor-export-bundle.js";
|
|
|
9
9
|
import { readJsonFile } from "./read-json-file.js";
|
|
10
10
|
import { buildSecurityAudit } from "../security/security-audit.js";
|
|
11
11
|
import { buildTrustScore } from "../security/trust-score.js";
|
|
12
|
+
import { buildDoctorRuntimePlan, evaluateRuntimeApproval, runtimeApprovalPassed } from "./runtime-plan.js";
|
|
12
13
|
import { packageVersion } from "../version.js";
|
|
13
14
|
const execFileAsync = promisify(execFile);
|
|
14
15
|
function isPlainObject(value) {
|
|
@@ -102,6 +103,7 @@ function releaseReady(report) {
|
|
|
102
103
|
report.corpus.summary.status === "pass" &&
|
|
103
104
|
report.performance.status === "pass" &&
|
|
104
105
|
report.releaseGates.status === "pass" &&
|
|
106
|
+
runtimeApprovalPassed(report.runtimeApproval) &&
|
|
105
107
|
report.security.status !== "fail" &&
|
|
106
108
|
report.trust.status !== "fail";
|
|
107
109
|
}
|
|
@@ -149,6 +151,7 @@ function buildReleaseEvidenceSigningPayload(report) {
|
|
|
149
151
|
package: report.package,
|
|
150
152
|
git: report.git,
|
|
151
153
|
releaseGates: report.releaseGates,
|
|
154
|
+
runtimeApproval: report.runtimeApproval,
|
|
152
155
|
attestation: {
|
|
153
156
|
version: report.attestation.version,
|
|
154
157
|
subject: report.attestation.subject,
|
|
@@ -190,7 +193,7 @@ function signReleaseEvidence(report, signingKey, keyHint) {
|
|
|
190
193
|
export async function buildDoctorReleaseEvidenceReport(targetPath, options) {
|
|
191
194
|
const rootPath = path.resolve(targetPath);
|
|
192
195
|
const security = await buildSecurityAudit(rootPath);
|
|
193
|
-
const [attestation, corpus, performance, trust, packageMetadata, git] = await Promise.all([
|
|
196
|
+
const [attestation, corpus, performance, trust, packageMetadata, git, runtimePlan] = await Promise.all([
|
|
194
197
|
buildDoctorAttestation(rootPath, {
|
|
195
198
|
signingKey: options.signingKey,
|
|
196
199
|
signingKeyHint: `env:${options.signingKeyEnv}`,
|
|
@@ -204,7 +207,8 @@ export async function buildDoctorReleaseEvidenceReport(targetPath, options) {
|
|
|
204
207
|
}),
|
|
205
208
|
buildTrustScore(rootPath, { securityAudit: security }),
|
|
206
209
|
readPackageMetadata(rootPath),
|
|
207
|
-
readGitMetadata(rootPath)
|
|
210
|
+
readGitMetadata(rootPath),
|
|
211
|
+
buildDoctorRuntimePlan(rootPath)
|
|
208
212
|
]);
|
|
209
213
|
const normalizedPackageMetadata = {
|
|
210
214
|
name: packageMetadata.name ?? attestation.subject.name,
|
|
@@ -216,6 +220,10 @@ export async function buildDoctorReleaseEvidenceReport(targetPath, options) {
|
|
|
216
220
|
artifactPath: "inline:doctor.release-evidence.attestation"
|
|
217
221
|
});
|
|
218
222
|
const releaseGates = buildReleaseGateReport(git, options);
|
|
223
|
+
const runtimeApproval = evaluateRuntimeApproval(runtimePlan, {
|
|
224
|
+
required: options.requireRuntimeApproval ?? false,
|
|
225
|
+
approvedDigest: options.runtimeApprovalDigest
|
|
226
|
+
});
|
|
219
227
|
const partialReport = {
|
|
220
228
|
schemaVersion: "1.0.0",
|
|
221
229
|
kind: "doctor.release.evidence",
|
|
@@ -228,12 +236,14 @@ export async function buildDoctorReleaseEvidenceReport(targetPath, options) {
|
|
|
228
236
|
corpus: corpus.summary.status,
|
|
229
237
|
performance: performance.status,
|
|
230
238
|
releaseGates: releaseGates.status,
|
|
239
|
+
runtimeApproval: runtimeApprovalPassed(runtimeApproval) ? "pass" : "fail",
|
|
231
240
|
security: toEvidenceStatus(security.status),
|
|
232
241
|
trust: toEvidenceStatus(trust.status)
|
|
233
242
|
},
|
|
234
243
|
package: normalizedPackageMetadata,
|
|
235
244
|
git,
|
|
236
245
|
releaseGates,
|
|
246
|
+
runtimeApproval,
|
|
237
247
|
attestation,
|
|
238
248
|
attestationVerification,
|
|
239
249
|
corpus,
|
|
@@ -447,6 +457,7 @@ export function renderDoctorReleaseEvidence(report) {
|
|
|
447
457
|
`Corpus: ${report.summary.corpus.toUpperCase()}`,
|
|
448
458
|
`Performance: ${report.summary.performance.toUpperCase()}`,
|
|
449
459
|
`Release gates: ${report.summary.releaseGates.toUpperCase()}`,
|
|
460
|
+
`Runtime approval: ${report.summary.runtimeApproval.toUpperCase()} (${report.runtimeApproval.status})`,
|
|
450
461
|
`Security: ${report.summary.security.toUpperCase()} (${report.security.score}/100)`,
|
|
451
462
|
`Trust: ${report.summary.trust.toUpperCase()} (${report.trust.score}/100)`,
|
|
452
463
|
`Git commit: ${report.git.commit ?? "unknown"}`,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type SecurityAudit } from "../security/security-audit.js";
|
|
2
|
+
import type { Finding } from "../domain/types.js";
|
|
3
|
+
type RuntimePlanStatus = "pass" | "warn" | "fail";
|
|
4
|
+
type RuntimePlanRiskLevel = "low" | "medium" | "high";
|
|
5
|
+
type RuntimePlanTransport = "stdio" | "http";
|
|
6
|
+
export interface RuntimePlanServer {
|
|
7
|
+
name: string;
|
|
8
|
+
transport: RuntimePlanTransport;
|
|
9
|
+
command: string | null;
|
|
10
|
+
args: string[];
|
|
11
|
+
cwd: string | null;
|
|
12
|
+
url: string | null;
|
|
13
|
+
probeMethods: string[];
|
|
14
|
+
riskLevel: RuntimePlanRiskLevel;
|
|
15
|
+
riskReasons: string[];
|
|
16
|
+
}
|
|
17
|
+
export interface DoctorRuntimePlan {
|
|
18
|
+
schemaVersion: "1.0.0";
|
|
19
|
+
kind: "doctor.runtime.plan";
|
|
20
|
+
generatedAt: string;
|
|
21
|
+
version: string;
|
|
22
|
+
targetPath: string;
|
|
23
|
+
status: RuntimePlanStatus;
|
|
24
|
+
exitCode: 0 | 1;
|
|
25
|
+
runtimeExecution: "not_started";
|
|
26
|
+
digest: string;
|
|
27
|
+
summary: {
|
|
28
|
+
serverCount: number;
|
|
29
|
+
executableServerCount: number;
|
|
30
|
+
highRiskServerCount: number;
|
|
31
|
+
findings: SecurityAudit["findingCounts"];
|
|
32
|
+
};
|
|
33
|
+
servers: RuntimePlanServer[];
|
|
34
|
+
findings: Finding[];
|
|
35
|
+
}
|
|
36
|
+
export interface RuntimeApprovalReport {
|
|
37
|
+
required: boolean;
|
|
38
|
+
status: "approved" | "missing" | "mismatch" | "not_required";
|
|
39
|
+
planDigest: string;
|
|
40
|
+
approvedDigest: string | null;
|
|
41
|
+
message: string;
|
|
42
|
+
}
|
|
43
|
+
export declare function buildDoctorRuntimePlan(targetPath: string, generatedAt?: string): Promise<DoctorRuntimePlan>;
|
|
44
|
+
export declare function evaluateRuntimeApproval(plan: DoctorRuntimePlan, options: {
|
|
45
|
+
required: boolean;
|
|
46
|
+
approvedDigest?: string | null;
|
|
47
|
+
}): RuntimeApprovalReport;
|
|
48
|
+
export declare function runtimeApprovalPassed(approval: RuntimeApprovalReport): boolean;
|
|
49
|
+
export declare function renderDoctorRuntimePlanJson(plan: DoctorRuntimePlan): string;
|
|
50
|
+
export declare function renderDoctorRuntimePlan(plan: DoctorRuntimePlan): string;
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { packageVersion } from "../version.js";
|
|
4
|
+
import { discoverPackage } from "./discover-package.js";
|
|
5
|
+
import { readJsonFile } from "./read-json-file.js";
|
|
6
|
+
import { buildSecurityAudit } from "../security/security-audit.js";
|
|
7
|
+
function isPlainObject(value) {
|
|
8
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
function stableStringify(value) {
|
|
11
|
+
if (Array.isArray(value)) {
|
|
12
|
+
return `[${value.map((item) => stableStringify(item)).join(",")}]`;
|
|
13
|
+
}
|
|
14
|
+
if (isPlainObject(value)) {
|
|
15
|
+
return `{${Object.keys(value)
|
|
16
|
+
.sort()
|
|
17
|
+
.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`)
|
|
18
|
+
.join(",")}}`;
|
|
19
|
+
}
|
|
20
|
+
return JSON.stringify(value);
|
|
21
|
+
}
|
|
22
|
+
function sha256(value) {
|
|
23
|
+
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
24
|
+
}
|
|
25
|
+
function normalizeCwd(rootPath, cwd) {
|
|
26
|
+
if (typeof cwd !== "string") {
|
|
27
|
+
return ".";
|
|
28
|
+
}
|
|
29
|
+
const resolvedCwd = path.resolve(rootPath, cwd);
|
|
30
|
+
const relativeCwd = path.relative(rootPath, resolvedCwd).replace(/\\/g, "/");
|
|
31
|
+
return relativeCwd.length === 0 ? "." : relativeCwd;
|
|
32
|
+
}
|
|
33
|
+
function buildRisk(serverName, serverConfig, findings) {
|
|
34
|
+
const matchingFindings = findings.filter((finding) => finding.message.includes(`\`${serverName}\``));
|
|
35
|
+
const riskReasons = matchingFindings.map((finding) => finding.id);
|
|
36
|
+
const hasFail = matchingFindings.some((finding) => finding.severity === "fail");
|
|
37
|
+
const hasWarn = matchingFindings.some((finding) => finding.severity === "warn");
|
|
38
|
+
if (typeof serverConfig.command === "string") {
|
|
39
|
+
riskReasons.push("runtime.executes_local_command");
|
|
40
|
+
}
|
|
41
|
+
if (typeof serverConfig.url === "string") {
|
|
42
|
+
riskReasons.push("runtime.connects_remote_server");
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
riskLevel: hasFail ? "high" : hasWarn ? "medium" : "low",
|
|
46
|
+
riskReasons: [...new Set(riskReasons)]
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function planDigestPayload(plan) {
|
|
50
|
+
return {
|
|
51
|
+
schemaVersion: plan.schemaVersion,
|
|
52
|
+
kind: "doctor.runtime.plan.digest.v1",
|
|
53
|
+
version: plan.version,
|
|
54
|
+
status: plan.status,
|
|
55
|
+
summary: plan.summary,
|
|
56
|
+
servers: plan.servers,
|
|
57
|
+
findings: plan.findings.map((finding) => ({
|
|
58
|
+
id: finding.id,
|
|
59
|
+
severity: finding.severity,
|
|
60
|
+
message: finding.message
|
|
61
|
+
}))
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function buildRuntimePlanDigest(plan) {
|
|
65
|
+
return sha256(stableStringify(planDigestPayload(plan)));
|
|
66
|
+
}
|
|
67
|
+
export async function buildDoctorRuntimePlan(targetPath, generatedAt = new Date().toISOString()) {
|
|
68
|
+
const rootPath = path.resolve(targetPath);
|
|
69
|
+
const discoveredPackage = await discoverPackage(rootPath);
|
|
70
|
+
const security = await buildSecurityAudit(rootPath);
|
|
71
|
+
if (!discoveredPackage?.manifest.mcpServers) {
|
|
72
|
+
const partialPlan = {
|
|
73
|
+
schemaVersion: "1.0.0",
|
|
74
|
+
kind: "doctor.runtime.plan",
|
|
75
|
+
version: packageVersion,
|
|
76
|
+
targetPath: rootPath,
|
|
77
|
+
status: security.status,
|
|
78
|
+
exitCode: (security.status === "fail" ? 1 : 0),
|
|
79
|
+
runtimeExecution: "not_started",
|
|
80
|
+
summary: {
|
|
81
|
+
serverCount: 0,
|
|
82
|
+
executableServerCount: 0,
|
|
83
|
+
highRiskServerCount: 0,
|
|
84
|
+
findings: security.findingCounts
|
|
85
|
+
},
|
|
86
|
+
servers: [],
|
|
87
|
+
findings: security.findings
|
|
88
|
+
};
|
|
89
|
+
return {
|
|
90
|
+
...partialPlan,
|
|
91
|
+
generatedAt,
|
|
92
|
+
digest: buildRuntimePlanDigest(partialPlan)
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
let parsedConfig;
|
|
96
|
+
try {
|
|
97
|
+
parsedConfig = await readJsonFile(path.resolve(discoveredPackage.rootPath, discoveredPackage.manifest.mcpServers));
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
parsedConfig = {};
|
|
101
|
+
}
|
|
102
|
+
const serverEntries = isPlainObject(parsedConfig) && isPlainObject(parsedConfig.mcpServers)
|
|
103
|
+
? Object.entries(parsedConfig.mcpServers)
|
|
104
|
+
: [];
|
|
105
|
+
const servers = serverEntries
|
|
106
|
+
.filter((entry) => isPlainObject(entry[1]))
|
|
107
|
+
.map(([serverName, serverConfig]) => {
|
|
108
|
+
const command = typeof serverConfig.command === "string" ? serverConfig.command : null;
|
|
109
|
+
const url = typeof serverConfig.url === "string" ? serverConfig.url : null;
|
|
110
|
+
const { riskLevel, riskReasons } = buildRisk(serverName, serverConfig, security.findings);
|
|
111
|
+
return {
|
|
112
|
+
name: serverName,
|
|
113
|
+
transport: command ? "stdio" : "http",
|
|
114
|
+
command,
|
|
115
|
+
args: Array.isArray(serverConfig.args)
|
|
116
|
+
? serverConfig.args.filter((arg) => typeof arg === "string")
|
|
117
|
+
: [],
|
|
118
|
+
cwd: command ? normalizeCwd(discoveredPackage.rootPath, serverConfig.cwd) : null,
|
|
119
|
+
url,
|
|
120
|
+
probeMethods: command
|
|
121
|
+
? [
|
|
122
|
+
"initialize",
|
|
123
|
+
"tools/list",
|
|
124
|
+
"tools/call:safe-only",
|
|
125
|
+
"resources/list",
|
|
126
|
+
"resources/read:first-resource-only",
|
|
127
|
+
"resources/templates/list",
|
|
128
|
+
"prompts/list",
|
|
129
|
+
"prompts/get:first-prompt-only"
|
|
130
|
+
]
|
|
131
|
+
: [],
|
|
132
|
+
riskLevel,
|
|
133
|
+
riskReasons
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
const highRiskServerCount = servers.filter((server) => server.riskLevel === "high").length;
|
|
137
|
+
const partialPlan = {
|
|
138
|
+
schemaVersion: "1.0.0",
|
|
139
|
+
kind: "doctor.runtime.plan",
|
|
140
|
+
version: packageVersion,
|
|
141
|
+
targetPath: discoveredPackage.rootPath,
|
|
142
|
+
status: highRiskServerCount > 0
|
|
143
|
+
? "fail"
|
|
144
|
+
: security.status === "warn"
|
|
145
|
+
? "warn"
|
|
146
|
+
: "pass",
|
|
147
|
+
exitCode: (highRiskServerCount > 0 ? 1 : 0),
|
|
148
|
+
runtimeExecution: "not_started",
|
|
149
|
+
summary: {
|
|
150
|
+
serverCount: servers.length,
|
|
151
|
+
executableServerCount: servers.filter((server) => server.command).length,
|
|
152
|
+
highRiskServerCount,
|
|
153
|
+
findings: security.findingCounts
|
|
154
|
+
},
|
|
155
|
+
servers,
|
|
156
|
+
findings: security.findings
|
|
157
|
+
};
|
|
158
|
+
return {
|
|
159
|
+
...partialPlan,
|
|
160
|
+
generatedAt,
|
|
161
|
+
digest: buildRuntimePlanDigest(partialPlan)
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
export function evaluateRuntimeApproval(plan, options) {
|
|
165
|
+
if (!options.required) {
|
|
166
|
+
return {
|
|
167
|
+
required: false,
|
|
168
|
+
status: "not_required",
|
|
169
|
+
planDigest: plan.digest,
|
|
170
|
+
approvedDigest: options.approvedDigest ?? null,
|
|
171
|
+
message: "Runtime approval was not required for this run."
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
if (!options.approvedDigest) {
|
|
175
|
+
return {
|
|
176
|
+
required: true,
|
|
177
|
+
status: "missing",
|
|
178
|
+
planDigest: plan.digest,
|
|
179
|
+
approvedDigest: null,
|
|
180
|
+
message: "Runtime approval was required, but no approved plan digest was provided."
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
if (options.approvedDigest !== plan.digest) {
|
|
184
|
+
return {
|
|
185
|
+
required: true,
|
|
186
|
+
status: "mismatch",
|
|
187
|
+
planDigest: plan.digest,
|
|
188
|
+
approvedDigest: options.approvedDigest,
|
|
189
|
+
message: "Runtime approval digest does not match the current runtime plan."
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
required: true,
|
|
194
|
+
status: "approved",
|
|
195
|
+
planDigest: plan.digest,
|
|
196
|
+
approvedDigest: options.approvedDigest,
|
|
197
|
+
message: "Runtime approval digest matches the current runtime plan."
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
export function runtimeApprovalPassed(approval) {
|
|
201
|
+
return approval.status === "approved" || approval.status === "not_required";
|
|
202
|
+
}
|
|
203
|
+
export function renderDoctorRuntimePlanJson(plan) {
|
|
204
|
+
return JSON.stringify(plan, null, 2);
|
|
205
|
+
}
|
|
206
|
+
export function renderDoctorRuntimePlan(plan) {
|
|
207
|
+
const lines = [
|
|
208
|
+
"Doctor Runtime Plan",
|
|
209
|
+
"===================",
|
|
210
|
+
`Target: ${plan.targetPath}`,
|
|
211
|
+
`Status: ${plan.status.toUpperCase()}`,
|
|
212
|
+
`Runtime execution: ${plan.runtimeExecution}`,
|
|
213
|
+
`Digest: ${plan.digest}`,
|
|
214
|
+
`Servers: ${plan.summary.serverCount}`,
|
|
215
|
+
`Executable servers: ${plan.summary.executableServerCount}`,
|
|
216
|
+
`High-risk servers: ${plan.summary.highRiskServerCount}`
|
|
217
|
+
];
|
|
218
|
+
if (plan.servers.length === 0) {
|
|
219
|
+
lines.push("", "No MCP runtime servers found.");
|
|
220
|
+
return lines.join("\n");
|
|
221
|
+
}
|
|
222
|
+
lines.push("", "Servers", "-------");
|
|
223
|
+
for (const server of plan.servers) {
|
|
224
|
+
lines.push(`${server.riskLevel.toUpperCase()} ${server.name}`);
|
|
225
|
+
lines.push(` Transport: ${server.transport}`);
|
|
226
|
+
lines.push(` Command: ${server.command ?? server.url ?? "not executable by runtime probe"}`);
|
|
227
|
+
lines.push(` Cwd: ${server.cwd ?? "n/a"}`);
|
|
228
|
+
lines.push(` Probes: ${server.probeMethods.length > 0 ? server.probeMethods.join(", ") : "none"}`);
|
|
229
|
+
lines.push(` Risk reasons: ${server.riskReasons.length > 0 ? server.riskReasons.join(", ") : "none"}`);
|
|
230
|
+
}
|
|
231
|
+
return lines.join("\n");
|
|
232
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export { buildDoctorOutputContract, renderDoctorOutputContract, renderDoctorOutp
|
|
|
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 { buildDoctorRuntimePlan, evaluateRuntimeApproval, renderDoctorRuntimePlan, renderDoctorRuntimePlanJson, runtimeApprovalPassed, type DoctorRuntimePlan, type RuntimeApprovalReport, type RuntimePlanServer } from "./core/runtime-plan.js";
|
|
12
13
|
export { buildDoctorReleaseEvidenceAssetReport, buildDoctorReleaseEvidenceReport, renderDoctorReleaseEvidenceAsset, renderDoctorReleaseEvidenceAssetJson, renderDoctorReleaseEvidence, renderDoctorReleaseEvidenceJson, renderDoctorReleaseEvidenceVerification, renderDoctorReleaseEvidenceVerificationJson, verifyDoctorReleaseEvidence, type BuildDoctorReleaseEvidenceOptions, type DoctorReleaseEvidenceAssetReport, type DoctorReleaseEvidenceGitMetadata, type DoctorReleaseEvidencePackageMetadata, type DoctorReleaseEvidenceReport, type DoctorReleaseEvidenceVerificationReport } from "./core/release-evidence.js";
|
|
13
14
|
export { buildDoctorNpmPackageReport, renderDoctorNpmPackageReport, renderDoctorNpmPackageReportJson, type BuildDoctorNpmPackageReportOptions, type DoctorNpmPackageReport } from "./core/npm-package-doctor.js";
|
|
14
15
|
export { buildDoctorRiskDiffReport, renderDoctorRiskDiffReport, renderDoctorRiskDiffReportJson, type BuildDoctorRiskDiffReportOptions, type DoctorRiskDiffReport, type RiskDiffFinding, type RiskFindingCategory } from "./core/risk-diff.js";
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ export { buildDoctorOutputContract, renderDoctorOutputContract, renderDoctorOutp
|
|
|
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 { buildDoctorRuntimePlan, evaluateRuntimeApproval, renderDoctorRuntimePlan, renderDoctorRuntimePlanJson, runtimeApprovalPassed } from "./core/runtime-plan.js";
|
|
12
13
|
export { buildDoctorReleaseEvidenceAssetReport, buildDoctorReleaseEvidenceReport, renderDoctorReleaseEvidenceAsset, renderDoctorReleaseEvidenceAssetJson, renderDoctorReleaseEvidence, renderDoctorReleaseEvidenceJson, renderDoctorReleaseEvidenceVerification, renderDoctorReleaseEvidenceVerificationJson, verifyDoctorReleaseEvidence } from "./core/release-evidence.js";
|
|
13
14
|
export { buildDoctorNpmPackageReport, renderDoctorNpmPackageReport, renderDoctorNpmPackageReportJson } from "./core/npm-package-doctor.js";
|
|
14
15
|
export { buildDoctorRiskDiffReport, renderDoctorRiskDiffReport, renderDoctorRiskDiffReportJson } from "./core/risk-diff.js";
|
package/dist/run-cli.js
CHANGED
|
@@ -20,6 +20,7 @@ import { buildDoctorAttestation, renderDoctorAttestation, renderDoctorAttestatio
|
|
|
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 { buildDoctorRuntimePlan, evaluateRuntimeApproval, renderDoctorRuntimePlan, renderDoctorRuntimePlanJson, runtimeApprovalPassed } from "./core/runtime-plan.js";
|
|
23
24
|
import { buildDoctorReleaseEvidenceAssetReport, buildDoctorReleaseEvidenceReport, renderDoctorReleaseEvidenceAsset, renderDoctorReleaseEvidenceAssetJson, renderDoctorReleaseEvidence, renderDoctorReleaseEvidenceJson, renderDoctorReleaseEvidenceVerification, renderDoctorReleaseEvidenceVerificationJson, verifyDoctorReleaseEvidence } from "./core/release-evidence.js";
|
|
24
25
|
import { buildDoctorNpmPackageReport, renderDoctorNpmPackageReport, renderDoctorNpmPackageReportJson } from "./core/npm-package-doctor.js";
|
|
25
26
|
import { buildDoctorRiskDiffReport, renderDoctorRiskDiffReport, renderDoctorRiskDiffReportJson } from "./core/risk-diff.js";
|
|
@@ -70,7 +71,7 @@ const defaultIo = {
|
|
|
70
71
|
}
|
|
71
72
|
};
|
|
72
73
|
function printUsage(io) {
|
|
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]|release-evidence verify <evidence.json> --target <path> --sign-key-env NAME|release-evidence asset <path> --tag <tag> --output <evidence.json> --sign-key-env NAME [--upload]|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");
|
|
74
|
+
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] [--require-runtime-approval --runtime-approval-digest <digest>] [--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|runtime-plan <path>|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] [--require-runtime-approval --runtime-approval-digest <digest>]|release-evidence verify <evidence.json> --target <path> --sign-key-env NAME|release-evidence asset <path> --tag <tag> --output <evidence.json> --sign-key-env NAME [--upload]|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");
|
|
74
75
|
}
|
|
75
76
|
const performanceStageNames = new Set([
|
|
76
77
|
"validation",
|
|
@@ -351,6 +352,32 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
351
352
|
io.writeStdout(renderedReport);
|
|
352
353
|
return report.exitCode;
|
|
353
354
|
}
|
|
355
|
+
if (maybePath === "runtime-plan") {
|
|
356
|
+
const targetPath = remainingArgs[0] && !remainingArgs[0].startsWith("--")
|
|
357
|
+
? remainingArgs[0]
|
|
358
|
+
: null;
|
|
359
|
+
const runtimePlanFlags = targetPath ? remainingArgs.slice(1) : remainingArgs;
|
|
360
|
+
const jsonOutput = runtimePlanFlags.includes("--json");
|
|
361
|
+
const outputIndex = runtimePlanFlags.indexOf("--output");
|
|
362
|
+
const outputPath = outputIndex === -1 ? null : runtimePlanFlags[outputIndex + 1];
|
|
363
|
+
if (!targetPath) {
|
|
364
|
+
io.writeStderr("Missing target path for runtime plan.");
|
|
365
|
+
return 2;
|
|
366
|
+
}
|
|
367
|
+
if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
|
|
368
|
+
io.writeStderr("Missing path after --output.");
|
|
369
|
+
return 2;
|
|
370
|
+
}
|
|
371
|
+
const plan = await buildDoctorRuntimePlan(targetPath);
|
|
372
|
+
const renderedPlan = jsonOutput
|
|
373
|
+
? renderDoctorRuntimePlanJson(plan)
|
|
374
|
+
: renderDoctorRuntimePlan(plan);
|
|
375
|
+
if (outputPath) {
|
|
376
|
+
await writeFile(outputPath, renderDoctorRuntimePlanJson(plan), "utf8");
|
|
377
|
+
}
|
|
378
|
+
io.writeStdout(renderedPlan);
|
|
379
|
+
return plan.exitCode;
|
|
380
|
+
}
|
|
354
381
|
if (maybePath === "release-evidence") {
|
|
355
382
|
if (remainingArgs[0] === "asset") {
|
|
356
383
|
const targetPath = remainingArgs[1] && !remainingArgs[1].startsWith("--")
|
|
@@ -368,6 +395,11 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
368
395
|
const signKeyEnv = signKeyEnvIndex === -1 ? null : assetFlags[signKeyEnvIndex + 1];
|
|
369
396
|
const allowDirty = assetFlags.includes("--allow-dirty");
|
|
370
397
|
const allowUntagged = assetFlags.includes("--allow-untagged");
|
|
398
|
+
const requireRuntimeApproval = assetFlags.includes("--require-runtime-approval");
|
|
399
|
+
const runtimeApprovalDigestIndex = assetFlags.indexOf("--runtime-approval-digest");
|
|
400
|
+
const runtimeApprovalDigest = runtimeApprovalDigestIndex === -1
|
|
401
|
+
? null
|
|
402
|
+
: assetFlags[runtimeApprovalDigestIndex + 1];
|
|
371
403
|
if (!targetPath) {
|
|
372
404
|
io.writeStderr("Missing target path for release evidence asset.");
|
|
373
405
|
return 2;
|
|
@@ -400,6 +432,11 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
400
432
|
io.writeStderr("Missing environment variable name after --sign-key-env.");
|
|
401
433
|
return 2;
|
|
402
434
|
}
|
|
435
|
+
if (runtimeApprovalDigestIndex !== -1 &&
|
|
436
|
+
(!runtimeApprovalDigest || runtimeApprovalDigest.startsWith("--"))) {
|
|
437
|
+
io.writeStderr("Missing digest after --runtime-approval-digest.");
|
|
438
|
+
return 2;
|
|
439
|
+
}
|
|
403
440
|
const signingKey = terminalContext.env[signKeyEnv];
|
|
404
441
|
if (!signingKey) {
|
|
405
442
|
io.writeStderr(`Environment variable ${signKeyEnv} is not set.`);
|
|
@@ -416,6 +453,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
416
453
|
signingKeyEnv: signKeyEnv,
|
|
417
454
|
allowDirty,
|
|
418
455
|
allowUntagged,
|
|
456
|
+
requireRuntimeApproval,
|
|
457
|
+
runtimeApprovalDigest,
|
|
419
458
|
environment: {
|
|
420
459
|
env: terminalContext.env,
|
|
421
460
|
platform: terminalContext.platform
|
|
@@ -516,6 +555,11 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
516
555
|
const signKeyEnv = signKeyEnvIndex === -1 ? null : evidenceFlags[signKeyEnvIndex + 1];
|
|
517
556
|
const allowDirty = evidenceFlags.includes("--allow-dirty");
|
|
518
557
|
const allowUntagged = evidenceFlags.includes("--allow-untagged");
|
|
558
|
+
const requireRuntimeApproval = evidenceFlags.includes("--require-runtime-approval");
|
|
559
|
+
const runtimeApprovalDigestIndex = evidenceFlags.indexOf("--runtime-approval-digest");
|
|
560
|
+
const runtimeApprovalDigest = runtimeApprovalDigestIndex === -1
|
|
561
|
+
? null
|
|
562
|
+
: evidenceFlags[runtimeApprovalDigestIndex + 1];
|
|
519
563
|
if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
|
|
520
564
|
io.writeStderr("Missing path after --output.");
|
|
521
565
|
return 2;
|
|
@@ -532,6 +576,11 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
532
576
|
io.writeStderr("Missing environment variable name after --sign-key-env.");
|
|
533
577
|
return 2;
|
|
534
578
|
}
|
|
579
|
+
if (runtimeApprovalDigestIndex !== -1 &&
|
|
580
|
+
(!runtimeApprovalDigest || runtimeApprovalDigest.startsWith("--"))) {
|
|
581
|
+
io.writeStderr("Missing digest after --runtime-approval-digest.");
|
|
582
|
+
return 2;
|
|
583
|
+
}
|
|
535
584
|
const signingKey = terminalContext.env[signKeyEnv];
|
|
536
585
|
if (!signingKey) {
|
|
537
586
|
io.writeStderr(`Environment variable ${signKeyEnv} is not set.`);
|
|
@@ -547,6 +596,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
547
596
|
signingKeyEnv: signKeyEnv,
|
|
548
597
|
allowDirty,
|
|
549
598
|
allowUntagged,
|
|
599
|
+
requireRuntimeApproval,
|
|
600
|
+
runtimeApprovalDigest,
|
|
550
601
|
environment: {
|
|
551
602
|
env: terminalContext.env,
|
|
552
603
|
platform: terminalContext.platform
|
|
@@ -1328,6 +1379,11 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
1328
1379
|
const policy = parsePolicyPack(policyName);
|
|
1329
1380
|
const historyIndex = normalizedFlags.indexOf("--history");
|
|
1330
1381
|
const historyPath = historyIndex === -1 ? null : normalizedFlags[historyIndex + 1];
|
|
1382
|
+
const requireRuntimeApproval = normalizedFlags.includes("--require-runtime-approval");
|
|
1383
|
+
const runtimeApprovalDigestIndex = normalizedFlags.indexOf("--runtime-approval-digest");
|
|
1384
|
+
const runtimeApprovalDigest = runtimeApprovalDigestIndex === -1
|
|
1385
|
+
? null
|
|
1386
|
+
: normalizedFlags[runtimeApprovalDigestIndex + 1];
|
|
1331
1387
|
if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
|
|
1332
1388
|
io.writeStderr("Missing path after --output.");
|
|
1333
1389
|
return 2;
|
|
@@ -1356,6 +1412,11 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
1356
1412
|
io.writeStderr("Missing path after --history.");
|
|
1357
1413
|
return 2;
|
|
1358
1414
|
}
|
|
1415
|
+
if (runtimeApprovalDigestIndex !== -1 &&
|
|
1416
|
+
(!runtimeApprovalDigest || runtimeApprovalDigest.startsWith("--"))) {
|
|
1417
|
+
io.writeStderr("Missing digest after --runtime-approval-digest.");
|
|
1418
|
+
return 2;
|
|
1419
|
+
}
|
|
1359
1420
|
if (checkInstalled && (badgeJsonOutput || badgeMarkdownOutput)) {
|
|
1360
1421
|
io.writeStderr("Badge output requires a single package target.");
|
|
1361
1422
|
return 2;
|
|
@@ -1367,6 +1428,14 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
1367
1428
|
const effectiveRuntimeProbeEnabled = runtimeProbeEnabled ||
|
|
1368
1429
|
checkProfile === "publish" ||
|
|
1369
1430
|
policyEnablesRuntime(policy);
|
|
1431
|
+
if (requireRuntimeApproval && !effectiveRuntimeProbeEnabled) {
|
|
1432
|
+
io.writeStderr("Runtime approval requires runtime probing. Add --runtime, --profile publish, or a runtime-enabled policy.");
|
|
1433
|
+
return 2;
|
|
1434
|
+
}
|
|
1435
|
+
if (checkInstalled && requireRuntimeApproval) {
|
|
1436
|
+
io.writeStderr("Runtime approval gating requires a single package target, not --installed.");
|
|
1437
|
+
return 2;
|
|
1438
|
+
}
|
|
1370
1439
|
const outputPolicy = determineOutputPolicy({
|
|
1371
1440
|
jsonOutput: jsonOutput || badgeJsonOutput,
|
|
1372
1441
|
markdownOutput: markdownOutput || badgeMarkdownOutput,
|
|
@@ -1378,6 +1447,17 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
1378
1447
|
env: terminalContext.env
|
|
1379
1448
|
});
|
|
1380
1449
|
const runCheckImpl = options.runCheckImpl ?? runCheck;
|
|
1450
|
+
if (!checkInstalled && effectiveRuntimeProbeEnabled && requireRuntimeApproval) {
|
|
1451
|
+
const runtimePlan = await buildDoctorRuntimePlan(targetPath);
|
|
1452
|
+
const approval = evaluateRuntimeApproval(runtimePlan, {
|
|
1453
|
+
required: true,
|
|
1454
|
+
approvedDigest: runtimeApprovalDigest
|
|
1455
|
+
});
|
|
1456
|
+
if (!runtimeApprovalPassed(approval)) {
|
|
1457
|
+
io.writeStderr(`${approval.message}\nCurrent runtime plan digest: ${runtimePlan.digest}`);
|
|
1458
|
+
return 1;
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1381
1461
|
if (checkInstalled) {
|
|
1382
1462
|
const installedPlugins = filterInstalledPlugins(await discoverInstalledPlugins({ env: terminalContext.env }), installedFilter);
|
|
1383
1463
|
if (installedPlugins.length === 0) {
|
package/package.json
CHANGED