codex-plugin-doctor 0.11.0 → 0.13.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.
@@ -0,0 +1,166 @@
1
+ import { stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { buildCompatibilityMatrix, readMcpConfigPath } from "../compatibility/compatibility-matrix.js";
4
+ import { readJsonFile } from "../core/read-json-file.js";
5
+ import { auditMcpServerConfig, buildSecurityAuditFromFindings } from "../security/security-audit.js";
6
+ function buildFinding(severity, id, message, impact, suggestedFix) {
7
+ return {
8
+ id,
9
+ severity,
10
+ message,
11
+ impact,
12
+ suggestedFix
13
+ };
14
+ }
15
+ function isPlainObject(value) {
16
+ return typeof value === "object" && value !== null && !Array.isArray(value);
17
+ }
18
+ async function fileExists(targetPath) {
19
+ try {
20
+ const details = await stat(targetPath);
21
+ return details.isFile();
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
27
+ function isPathWithinRoot(rootPath, candidatePath) {
28
+ const relativePath = path.relative(rootPath, candidatePath);
29
+ return (relativePath === "" ||
30
+ (!relativePath.startsWith("..") && !path.isAbsolute(relativePath)));
31
+ }
32
+ function buildStaticMcpFindings(mcpConfigPath, parsedConfig) {
33
+ if (!mcpConfigPath) {
34
+ return {
35
+ serverCount: 0,
36
+ findings: [
37
+ buildFinding("fail", "mcp.config.missing", "No MCP config was found for this target.", "A generic MCP package needs a `.mcp.json` file or a Codex manifest `mcpServers` reference before clients can discover servers.", "Add `.mcp.json` with a non-empty top-level `mcpServers` object.")
38
+ ]
39
+ };
40
+ }
41
+ if (!isPlainObject(parsedConfig)) {
42
+ return {
43
+ serverCount: 0,
44
+ findings: [
45
+ buildFinding("fail", "mcp.config.invalid_shape", "The MCP config must be a JSON object.", "MCP clients expect object-shaped configuration so server entries can be resolved deterministically.", `Wrap the MCP config in a top-level object inside \`${mcpConfigPath}\`.`)
46
+ ]
47
+ };
48
+ }
49
+ const servers = parsedConfig.mcpServers;
50
+ if (!isPlainObject(servers) || Object.keys(servers).length === 0) {
51
+ return {
52
+ serverCount: 0,
53
+ findings: [
54
+ buildFinding("fail", "mcp.config.invalid_shape", "The MCP config must contain a non-empty `mcpServers` object.", "Without server entries, MCP clients cannot discover any package capabilities.", `Define MCP servers under \`mcpServers\` in \`${mcpConfigPath}\`.`)
55
+ ]
56
+ };
57
+ }
58
+ const findings = [];
59
+ for (const [serverName, serverConfig] of Object.entries(servers)) {
60
+ if (!isPlainObject(serverConfig)) {
61
+ findings.push(buildFinding("fail", "mcp.server.invalid", `The MCP server \`${serverName}\` must be configured as an object.`, "MCP clients cannot interpret a server entry unless it is represented as an object with server options.", `Change the \`${serverName}\` entry in \`${mcpConfigPath}\` to an object.`));
62
+ continue;
63
+ }
64
+ if (typeof serverConfig.command !== "string" && typeof serverConfig.url !== "string") {
65
+ findings.push(buildFinding("fail", "mcp.server.transport.missing", `The MCP server \`${serverName}\` must define either \`command\` or \`url\`.`, "MCP clients need a process command for stdio servers or a URL for remote servers.", `Add either \`command\` or \`url\` to the \`${serverName}\` entry in \`${mcpConfigPath}\`.`));
66
+ }
67
+ }
68
+ return {
69
+ serverCount: Object.keys(servers).length,
70
+ findings
71
+ };
72
+ }
73
+ function mergeReportStatus(staticFindings, security) {
74
+ if (staticFindings.some((finding) => finding.severity === "fail") || security.status === "fail") {
75
+ return "fail";
76
+ }
77
+ if (staticFindings.some((finding) => finding.severity === "warn") || security.status === "warn") {
78
+ return "warn";
79
+ }
80
+ return "pass";
81
+ }
82
+ export async function buildGenericMcpDoctor(targetPath, environment = {}) {
83
+ const rootPath = path.resolve(targetPath);
84
+ const compatibility = await buildCompatibilityMatrix(rootPath, environment);
85
+ const mcpConfigPath = await readMcpConfigPath(rootPath);
86
+ let parsedConfig = null;
87
+ let staticFindings = [];
88
+ let serverCount = 0;
89
+ if (!mcpConfigPath || !(await fileExists(mcpConfigPath))) {
90
+ staticFindings = buildStaticMcpFindings(null, null).findings;
91
+ }
92
+ else if (!isPathWithinRoot(rootPath, mcpConfigPath)) {
93
+ staticFindings = [
94
+ buildFinding("fail", "mcp.config.path_outside_root", "The MCP config path resolves outside the target root.", "A package that reads MCP configuration outside its root is harder to audit and can depend on unreviewed local files.", "Keep `.mcp.json` or the manifest `mcpServers` reference inside the package root.")
95
+ ];
96
+ }
97
+ else {
98
+ try {
99
+ parsedConfig = await readJsonFile(mcpConfigPath);
100
+ const staticResult = buildStaticMcpFindings(mcpConfigPath, parsedConfig);
101
+ staticFindings = staticResult.findings;
102
+ serverCount = staticResult.serverCount;
103
+ }
104
+ catch {
105
+ staticFindings = [
106
+ buildFinding("fail", "mcp.config.invalid_json", "The MCP config is not valid JSON.", "MCP clients cannot parse server configuration until the JSON syntax is valid.", `Fix the JSON syntax in \`${mcpConfigPath}\`.`)
107
+ ];
108
+ }
109
+ }
110
+ const security = buildSecurityAuditFromFindings(rootPath, mcpConfigPath && parsedConfig !== null
111
+ ? auditMcpServerConfig(rootPath, parsedConfig)
112
+ : []);
113
+ const status = mergeReportStatus(staticFindings, security);
114
+ return {
115
+ targetPath: rootPath,
116
+ status,
117
+ exitCode: status === "fail" ? 1 : 0,
118
+ mcpConfigPath,
119
+ serverCount,
120
+ findings: [...staticFindings, ...security.findings],
121
+ security,
122
+ compatibility
123
+ };
124
+ }
125
+ export function renderGenericMcpDoctorJson(report) {
126
+ return JSON.stringify({
127
+ schemaVersion: "1.0.0",
128
+ generatedAt: new Date().toISOString(),
129
+ ...report
130
+ }, null, 2);
131
+ }
132
+ export function renderGenericMcpDoctor(report) {
133
+ const lines = [
134
+ "Generic MCP Doctor",
135
+ "==================",
136
+ `Target: ${report.targetPath}`,
137
+ `Status: ${report.status.toUpperCase()}`,
138
+ `MCP config: ${report.mcpConfigPath ?? "not found"}`,
139
+ `Servers: ${report.serverCount}`,
140
+ `Security: ${report.security.status.toUpperCase()} (${report.security.score}/100)`,
141
+ `Compatibility: ${report.compatibility.results
142
+ .map((result) => `${result.client}=${result.status}`)
143
+ .join(", ")}`
144
+ ];
145
+ if (report.findings.length === 0) {
146
+ lines.push("", "No findings.");
147
+ return lines.join("\n");
148
+ }
149
+ const failures = report.findings.filter((finding) => finding.severity === "fail");
150
+ const warnings = report.findings.filter((finding) => finding.severity === "warn");
151
+ const appendFindings = (title, findings, marker) => {
152
+ if (findings.length === 0) {
153
+ return;
154
+ }
155
+ lines.push("", title, "--------");
156
+ for (const finding of findings) {
157
+ lines.push(`${marker} ${finding.id}`);
158
+ lines.push(` Message: ${finding.message}`);
159
+ lines.push(` Impact: ${finding.impact}`);
160
+ lines.push(` Suggested fix: ${finding.suggestedFix}`);
161
+ }
162
+ };
163
+ appendFindings("Failures", failures, "x");
164
+ appendFindings("Warnings", warnings, "!");
165
+ return lines.join("\n");
166
+ }
@@ -0,0 +1,9 @@
1
+ import type { DoctorConfig } from "../core/doctor-config.js";
2
+ import type { SecurityAudit } from "../security/security-audit.js";
3
+ export declare const policyPackNames: readonly ["codex-publish", "mcp-strict", "security"];
4
+ export type PolicyPackName = (typeof policyPackNames)[number];
5
+ export declare function parsePolicyPack(value: string | null): PolicyPackName | null;
6
+ export declare function policyEnablesRuntime(policy: PolicyPackName | null): boolean;
7
+ export declare function policyFailsOnWarnings(policy: PolicyPackName | null): boolean;
8
+ export declare function applyPolicyToDoctorConfig(config: DoctorConfig, policy: PolicyPackName | null): DoctorConfig;
9
+ export declare function applyPolicyToSecurityAudit(audit: SecurityAudit, policy: PolicyPackName | null): SecurityAudit;
@@ -0,0 +1,33 @@
1
+ export const policyPackNames = ["codex-publish", "mcp-strict", "security"];
2
+ export function parsePolicyPack(value) {
3
+ if (!value) {
4
+ return null;
5
+ }
6
+ return policyPackNames.includes(value)
7
+ ? value
8
+ : null;
9
+ }
10
+ export function policyEnablesRuntime(policy) {
11
+ return policy === "codex-publish" || policy === "mcp-strict";
12
+ }
13
+ export function policyFailsOnWarnings(policy) {
14
+ return policy !== null;
15
+ }
16
+ export function applyPolicyToDoctorConfig(config, policy) {
17
+ if (!policyFailsOnWarnings(policy)) {
18
+ return config;
19
+ }
20
+ return {
21
+ ...config,
22
+ failOnWarnings: true
23
+ };
24
+ }
25
+ export function applyPolicyToSecurityAudit(audit, policy) {
26
+ if (!policyFailsOnWarnings(policy) || audit.status !== "warn") {
27
+ return audit;
28
+ }
29
+ return {
30
+ ...audit,
31
+ status: "fail"
32
+ };
33
+ }
package/dist/run-cli.js CHANGED
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import { createInterface } from "node:readline/promises";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { discoverInstalledPlugins, filterInstalledPlugins } from "./core/discover-installed-plugins.js";
7
+ import { buildEcosystemAudit, renderEcosystemAudit, renderEcosystemAuditJson } from "./audit/ecosystem-audit.js";
7
8
  import { appendValidationHistoryEntry, readValidationHistory, summarizeValidationHistory } from "./core/validation-history.js";
8
9
  import { buildCompatibilityMatrix, matrixExitCode } from "./compatibility/compatibility-matrix.js";
9
10
  import { applyInstallPreview, renderApplyInstallResult } from "./compatibility/apply-install-preview.js";
@@ -13,11 +14,14 @@ import { buildClineInstallPreview, renderClineInstallPreview } from "./compatibi
13
14
  import { buildWindsurfInstallPreview, renderWindsurfInstallPreview } from "./compatibility/windsurf-install-preview.js";
14
15
  import { applyDoctorConfig, loadDoctorConfig } from "./core/doctor-config.js";
15
16
  import { buildDoctorSnapshot, renderDoctorSnapshot, renderDoctorSnapshotJson } from "./core/doctor-snapshot.js";
17
+ import { buildDoctorRecommendations, renderDoctorRecommendations, renderDoctorRecommendationsJson } from "./core/doctor-recommendations.js";
18
+ import { buildDoctorExportBundle, renderDoctorExportBundle, renderDoctorExportBundleJson } from "./core/doctor-export-bundle.js";
16
19
  import { applyFixPlan, buildFixPlan, renderApplyFixResult, renderFixPlanJsonReport, renderFixPlan } from "./core/fix-plan.js";
17
20
  import { renderClientDoctor, renderEnvironmentDoctor, renderEnvironmentDoctorJson } from "./core/environment-doctor.js";
18
21
  import { initCiWorkflow } from "./core/init-ci.js";
19
22
  import { initPluginPackage, initPluginTemplates, isInitPluginTemplate } from "./core/init-plugin.js";
20
23
  import { runCheck } from "./index.js";
24
+ import { buildGenericMcpDoctor, renderGenericMcpDoctor, renderGenericMcpDoctorJson } from "./mcp/generic-mcp-doctor.js";
21
25
  import { renderInstalledSummary } from "./reporting/render-installed-summary.js";
22
26
  import { renderBadgeJson, renderBadgeMarkdown } from "./reporting/render-badge-report.js";
23
27
  import { renderCompatibilityScorecard } from "./reporting/render-compatibility-scorecard.js";
@@ -28,8 +32,10 @@ import { buildMarkdownReport } from "./reporting/render-markdown-report.js";
28
32
  import { renderRuleExplanation } from "./reporting/render-rule-explanation.js";
29
33
  import { renderSarifReport } from "./reporting/render-sarif-report.js";
30
34
  import { renderTextReport } from "./reporting/render-text-report.js";
35
+ import { applyPolicyToDoctorConfig, applyPolicyToSecurityAudit, parsePolicyPack, policyEnablesRuntime, policyFailsOnWarnings, policyPackNames } from "./policy/policy-packs.js";
31
36
  import { findRuleDefinition } from "./rules/rule-catalog.js";
32
37
  import { buildSecurityAudit, renderSecurityAuditJson, renderSecurityScorecard } from "./security/security-audit.js";
38
+ import { buildTrustScore, renderTrustScore, renderTrustScoreJson } from "./security/trust-score.js";
33
39
  import { createLiveStatusRenderer } from "./terminal/live-status-renderer.js";
34
40
  import { determineOutputPolicy } from "./terminal/output-policy.js";
35
41
  import { getSpinner } from "./terminal/spinner-registry.js";
@@ -55,7 +61,7 @@ const defaultIo = {
55
61
  }
56
62
  };
57
63
  function printUsage(io) {
58
- io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--compat] [--json|--markdown|--badge-json|--badge-markdown] [--output <path>] [--history <path>] [--runtime] [--verbose-runtime] [--explain] [--no-animations] [--ascii]\n codex-plugin-doctor security <path> [--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 [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");
64
+ 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>]\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 [recommend <path>|trust <path>|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");
59
65
  }
60
66
  function renderInstalledPlugins(plugins) {
61
67
  const lines = [
@@ -188,6 +194,91 @@ export async function runCli(args, io = defaultIo, options = {}) {
188
194
  const doctorFlags = maybePath?.startsWith("--")
189
195
  ? [maybePath, ...remainingArgs]
190
196
  : remainingArgs;
197
+ if (maybePath === "recommend") {
198
+ const targetPath = remainingArgs[0] && !remainingArgs[0].startsWith("--")
199
+ ? remainingArgs[0]
200
+ : ".";
201
+ const recommendFlags = remainingArgs[0] && !remainingArgs[0].startsWith("--")
202
+ ? remainingArgs.slice(1)
203
+ : remainingArgs;
204
+ const jsonOutput = recommendFlags.includes("--json");
205
+ const outputIndex = recommendFlags.indexOf("--output");
206
+ const outputPath = outputIndex === -1 ? null : recommendFlags[outputIndex + 1];
207
+ if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
208
+ io.writeStderr("Missing path after --output.");
209
+ return 2;
210
+ }
211
+ const report = await buildDoctorRecommendations(targetPath, {
212
+ environment: {
213
+ env: terminalContext.env,
214
+ platform: terminalContext.platform
215
+ },
216
+ runCheck: options.runCheckImpl
217
+ ? (pathToCheck) => options.runCheckImpl(pathToCheck)
218
+ : undefined
219
+ });
220
+ const renderedReport = jsonOutput
221
+ ? renderDoctorRecommendationsJson(report)
222
+ : renderDoctorRecommendations(report);
223
+ if (outputPath) {
224
+ await writeFile(outputPath, renderedReport, "utf8");
225
+ }
226
+ io.writeStdout(renderedReport);
227
+ return report.exitCode;
228
+ }
229
+ if (maybePath === "trust") {
230
+ const targetPath = remainingArgs[0] && !remainingArgs[0].startsWith("--")
231
+ ? remainingArgs[0]
232
+ : ".";
233
+ const trustFlags = remainingArgs[0] && !remainingArgs[0].startsWith("--")
234
+ ? remainingArgs.slice(1)
235
+ : remainingArgs;
236
+ const jsonOutput = trustFlags.includes("--json");
237
+ const outputIndex = trustFlags.indexOf("--output");
238
+ const outputPath = outputIndex === -1 ? null : trustFlags[outputIndex + 1];
239
+ if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
240
+ io.writeStderr("Missing path after --output.");
241
+ return 2;
242
+ }
243
+ const report = await buildTrustScore(targetPath);
244
+ const renderedReport = jsonOutput
245
+ ? renderTrustScoreJson(report)
246
+ : renderTrustScore(report);
247
+ if (outputPath) {
248
+ await writeFile(outputPath, renderedReport, "utf8");
249
+ }
250
+ io.writeStdout(renderedReport);
251
+ return report.exitCode;
252
+ }
253
+ if (maybePath === "export") {
254
+ const bundleIndex = remainingArgs.indexOf("--bundle");
255
+ if (bundleIndex === -1) {
256
+ io.writeStderr("Usage: codex-plugin-doctor doctor export --bundle <path> [--json] [--output <path>]");
257
+ return 2;
258
+ }
259
+ const targetPath = remainingArgs[bundleIndex + 1] && !remainingArgs[bundleIndex + 1].startsWith("--")
260
+ ? remainingArgs[bundleIndex + 1]
261
+ : ".";
262
+ const jsonOutput = remainingArgs.includes("--json");
263
+ const outputIndex = remainingArgs.indexOf("--output");
264
+ const outputPath = outputIndex === -1 ? null : remainingArgs[outputIndex + 1];
265
+ if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
266
+ io.writeStderr("Missing path after --output.");
267
+ return 2;
268
+ }
269
+ const bundle = await buildDoctorExportBundle(targetPath, {
270
+ env: terminalContext.env,
271
+ platform: terminalContext.platform
272
+ });
273
+ const bundleJson = renderDoctorExportBundleJson(bundle);
274
+ if (outputPath) {
275
+ await writeFile(outputPath, bundleJson, "utf8");
276
+ }
277
+ io.writeStdout(jsonOutput
278
+ ? bundleJson
279
+ : renderDoctorExportBundle(bundle, { outputPath }));
280
+ return 0;
281
+ }
191
282
  if (maybePath === "snapshot") {
192
283
  const jsonOutput = doctorFlags.includes("--json");
193
284
  const outputIndex = doctorFlags.indexOf("--output");
@@ -379,16 +470,107 @@ export async function runCli(args, io = defaultIo, options = {}) {
379
470
  }
380
471
  const jsonOutput = remainingArgs.includes("--json");
381
472
  const scorecardOutput = remainingArgs.includes("--scorecard");
473
+ const policyIndex = remainingArgs.indexOf("--policy");
474
+ const policyName = policyIndex === -1 ? null : remainingArgs[policyIndex + 1];
475
+ const policy = parsePolicyPack(policyName);
382
476
  if (jsonOutput && scorecardOutput) {
383
477
  io.writeStderr("Use either --json or --scorecard, not both.");
384
478
  return 2;
385
479
  }
386
- const audit = await buildSecurityAudit(maybePath);
480
+ if (policyIndex !== -1 && (!policyName || policyName.startsWith("--"))) {
481
+ io.writeStderr("Missing policy after --policy.");
482
+ return 2;
483
+ }
484
+ if (policyIndex !== -1 && !policy) {
485
+ io.writeStderr(`Unknown policy: ${policyName}. Supported policies: ${policyPackNames.join(", ")}.`);
486
+ return 2;
487
+ }
488
+ const audit = applyPolicyToSecurityAudit(await buildSecurityAudit(maybePath), policy);
387
489
  io.writeStdout(jsonOutput
388
490
  ? renderSecurityAuditJson(audit)
389
491
  : renderSecurityScorecard(audit, { includeFindings: !scorecardOutput }));
390
492
  return audit.status === "fail" ? 1 : 0;
391
493
  }
494
+ if (command === "mcp") {
495
+ if (!maybePath || maybePath.startsWith("--")) {
496
+ io.writeStderr("Missing target path. Usage: codex-plugin-doctor mcp <path> [--json] [--output <path>]");
497
+ return 2;
498
+ }
499
+ const jsonOutput = remainingArgs.includes("--json");
500
+ const outputIndex = remainingArgs.indexOf("--output");
501
+ const outputPath = outputIndex === -1 ? null : remainingArgs[outputIndex + 1];
502
+ if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
503
+ io.writeStderr("Missing path after --output.");
504
+ return 2;
505
+ }
506
+ const report = await buildGenericMcpDoctor(maybePath, {
507
+ env: terminalContext.env,
508
+ platform: terminalContext.platform
509
+ });
510
+ const renderedReport = jsonOutput
511
+ ? renderGenericMcpDoctorJson(report)
512
+ : renderGenericMcpDoctor(report);
513
+ if (outputPath) {
514
+ await writeFile(outputPath, renderedReport, "utf8");
515
+ }
516
+ io.writeStdout(renderedReport);
517
+ return report.exitCode;
518
+ }
519
+ if (command === "audit") {
520
+ const auditFlags = maybePath ? [maybePath, ...remainingArgs] : remainingArgs;
521
+ const installed = auditFlags.includes("--installed");
522
+ if (!installed) {
523
+ io.writeStderr("Usage: codex-plugin-doctor audit --installed [filter] [--security] [--compat] [--json] [--output <path>]");
524
+ return 2;
525
+ }
526
+ const installedIndex = auditFlags.indexOf("--installed");
527
+ const installedFilter = auditFlags[installedIndex + 1] && !auditFlags[installedIndex + 1].startsWith("--")
528
+ ? auditFlags[installedIndex + 1]
529
+ : null;
530
+ const jsonOutput = auditFlags.includes("--json");
531
+ const includeSecurity = auditFlags.includes("--security");
532
+ const includeCompatibility = auditFlags.includes("--compat");
533
+ const outputIndex = auditFlags.indexOf("--output");
534
+ const outputPath = outputIndex === -1 ? null : auditFlags[outputIndex + 1];
535
+ const policyIndex = auditFlags.indexOf("--policy");
536
+ const policyName = policyIndex === -1 ? null : auditFlags[policyIndex + 1];
537
+ const policy = parsePolicyPack(policyName);
538
+ if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
539
+ io.writeStderr("Missing path after --output.");
540
+ return 2;
541
+ }
542
+ if (policyIndex !== -1 && (!policyName || policyName.startsWith("--"))) {
543
+ io.writeStderr("Missing policy after --policy.");
544
+ return 2;
545
+ }
546
+ if (policyIndex !== -1 && !policy) {
547
+ io.writeStderr(`Unknown policy: ${policyName}. Supported policies: ${policyPackNames.join(", ")}.`);
548
+ return 2;
549
+ }
550
+ const report = await buildEcosystemAudit({
551
+ env: terminalContext.env,
552
+ platform: terminalContext.platform,
553
+ filter: installedFilter,
554
+ includeSecurity,
555
+ includeCompatibility,
556
+ failOnWarnings: policyFailsOnWarnings(policy),
557
+ validatePlugin: options.runCheckImpl ?? runCheck
558
+ });
559
+ if (report.summary.totalPlugins === 0) {
560
+ io.writeStderr(installedFilter
561
+ ? `No installed Codex plugins matched '${installedFilter}'.`
562
+ : "No installed Codex plugins found.");
563
+ return 1;
564
+ }
565
+ const renderedReport = jsonOutput
566
+ ? renderEcosystemAuditJson(report)
567
+ : renderEcosystemAudit(report);
568
+ if (outputPath) {
569
+ await writeFile(outputPath, renderedReport, "utf8");
570
+ }
571
+ io.writeStdout(renderedReport);
572
+ return report.status === "fail" ? 1 : 0;
573
+ }
392
574
  if (command === "compat") {
393
575
  const targetPath = maybePath && !maybePath.startsWith("--") ? maybePath : ".";
394
576
  const compatFlags = maybePath && maybePath.startsWith("--")
@@ -540,6 +722,9 @@ export async function runCli(args, io = defaultIo, options = {}) {
540
722
  const profileIndex = normalizedFlags.indexOf("--profile");
541
723
  const profileName = profileIndex === -1 ? null : normalizedFlags[profileIndex + 1];
542
724
  const checkProfile = parseCheckProfile(profileName);
725
+ const policyIndex = normalizedFlags.indexOf("--policy");
726
+ const policyName = policyIndex === -1 ? null : normalizedFlags[policyIndex + 1];
727
+ const policy = parsePolicyPack(policyName);
543
728
  const historyIndex = normalizedFlags.indexOf("--history");
544
729
  const historyPath = historyIndex === -1 ? null : normalizedFlags[historyIndex + 1];
545
730
  if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
@@ -558,6 +743,14 @@ export async function runCli(args, io = defaultIo, options = {}) {
558
743
  io.writeStderr("Unknown profile. Supported profiles: ci, strict, publish.");
559
744
  return 2;
560
745
  }
746
+ if (policyIndex !== -1 && (!policyName || policyName.startsWith("--"))) {
747
+ io.writeStderr("Missing policy after --policy.");
748
+ return 2;
749
+ }
750
+ if (policyIndex !== -1 && !policy) {
751
+ io.writeStderr(`Unknown policy: ${policyName}. Supported policies: ${policyPackNames.join(", ")}.`);
752
+ return 2;
753
+ }
561
754
  if (historyIndex !== -1 && (!historyPath || historyPath.startsWith("--"))) {
562
755
  io.writeStderr("Missing path after --history.");
563
756
  return 2;
@@ -570,7 +763,9 @@ export async function runCli(args, io = defaultIo, options = {}) {
570
763
  io.writeStderr("History output requires a single package target.");
571
764
  return 2;
572
765
  }
573
- const effectiveRuntimeProbeEnabled = runtimeProbeEnabled || checkProfile === "publish";
766
+ const effectiveRuntimeProbeEnabled = runtimeProbeEnabled ||
767
+ checkProfile === "publish" ||
768
+ policyEnablesRuntime(policy);
574
769
  const outputPolicy = determineOutputPolicy({
575
770
  jsonOutput: jsonOutput || badgeJsonOutput,
576
771
  markdownOutput: markdownOutput || badgeMarkdownOutput,
@@ -606,7 +801,7 @@ export async function runCli(args, io = defaultIo, options = {}) {
606
801
  runtimeTranscript: effectiveRuntimeProbeEnabled && verboseRuntime
607
802
  ? (line) => io.writeStderr(line)
608
803
  : undefined
609
- }), applyCheckProfile(config, checkProfile)),
804
+ }), applyPolicyToDoctorConfig(applyCheckProfile(config, checkProfile), policy)),
610
805
  compatibilityMatrix
611
806
  });
612
807
  }
@@ -643,7 +838,7 @@ export async function runCli(args, io = defaultIo, options = {}) {
643
838
  runtimeTranscript: effectiveRuntimeProbeEnabled && verboseRuntime
644
839
  ? (line) => io.writeStderr(line)
645
840
  : undefined
646
- }), applyCheckProfile(await loadDoctorConfig(targetPath, configPath), checkProfile));
841
+ }), applyPolicyToDoctorConfig(applyCheckProfile(await loadDoctorConfig(targetPath, configPath), checkProfile), policy));
647
842
  if (renderer) {
648
843
  if (result.status === "fail") {
649
844
  renderer.stopFailure("Validation failed");
@@ -10,6 +10,8 @@ export interface SecurityAudit {
10
10
  };
11
11
  findings: Finding[];
12
12
  }
13
+ export declare function auditMcpServerConfig(rootPath: string, parsedConfig: unknown): Finding[];
14
+ export declare function buildSecurityAuditFromFindings(targetPath: string, findings: Finding[]): SecurityAudit;
13
15
  export declare function buildSecurityAudit(targetPath: string): Promise<SecurityAudit>;
14
16
  export declare function renderSecurityAuditJson(audit: SecurityAudit): string;
15
17
  export declare function renderSecurityScorecard(audit: SecurityAudit, options?: {
@@ -40,24 +40,7 @@ function containsPipeInstaller(args) {
40
40
  /\b(iwr|irm|invoke-webrequest|invoke-restmethod)\b[^|]*\|\s*(iex|invoke-expression)\b/.test(joinedArgs) ||
41
41
  /\binvoke-expression\b/.test(joinedArgs));
42
42
  }
43
- async function auditMcpCommandSurface(discoveredPackage) {
44
- const { manifest, rootPath } = discoveredPackage;
45
- if (!manifest.mcpServers) {
46
- return [];
47
- }
48
- const mcpConfigPath = path.resolve(rootPath, manifest.mcpServers);
49
- if (!isPathWithinRoot(rootPath, mcpConfigPath)) {
50
- return [];
51
- }
52
- let parsedConfig;
53
- try {
54
- parsedConfig = await readJsonFile(mcpConfigPath);
55
- }
56
- catch {
57
- return [
58
- buildFinding("fail", "plugin.security.audit_unavailable", "The MCP security audit could not parse the referenced MCP config.", "Unreadable MCP configuration prevents review of server commands, URLs, and working directories before install.", "Fix the `.mcp.json` syntax, then rerun `codex-plugin-doctor security <path>`.")
59
- ];
60
- }
43
+ export function auditMcpServerConfig(rootPath, parsedConfig) {
61
44
  if (!isPlainObject(parsedConfig) || !isPlainObject(parsedConfig.mcpServers)) {
62
45
  return [
63
46
  buildFinding("fail", "plugin.security.audit_unavailable", "The MCP security audit could not find a valid `mcpServers` object.", "Without server entries, the audit cannot evaluate command execution or remote transport risk.", "Define MCP servers under a top-level `mcpServers` object.")
@@ -93,6 +76,26 @@ async function auditMcpCommandSurface(discoveredPackage) {
93
76
  }
94
77
  return findings;
95
78
  }
79
+ async function auditMcpCommandSurface(discoveredPackage) {
80
+ const { manifest, rootPath } = discoveredPackage;
81
+ if (!manifest.mcpServers) {
82
+ return [];
83
+ }
84
+ const mcpConfigPath = path.resolve(rootPath, manifest.mcpServers);
85
+ if (!isPathWithinRoot(rootPath, mcpConfigPath)) {
86
+ return [];
87
+ }
88
+ let parsedConfig;
89
+ try {
90
+ parsedConfig = await readJsonFile(mcpConfigPath);
91
+ }
92
+ catch {
93
+ return [
94
+ buildFinding("fail", "plugin.security.audit_unavailable", "The MCP security audit could not parse the referenced MCP config.", "Unreadable MCP configuration prevents review of server commands, URLs, and working directories before install.", "Fix the `.mcp.json` syntax, then rerun `codex-plugin-doctor security <path>`.")
95
+ ];
96
+ }
97
+ return auditMcpServerConfig(rootPath, parsedConfig);
98
+ }
96
99
  function dedupeFindings(findings) {
97
100
  const seen = new Set();
98
101
  return findings.filter((finding) => {
@@ -116,6 +119,22 @@ function buildFindingCounts(findings) {
116
119
  function scoreSecurityAudit(findingCounts) {
117
120
  return Math.max(0, 100 - (findingCounts.fail * 35) - (findingCounts.warn * 10));
118
121
  }
122
+ export function buildSecurityAuditFromFindings(targetPath, findings) {
123
+ const dedupedFindings = dedupeFindings(findings);
124
+ const findingCounts = buildFindingCounts(dedupedFindings);
125
+ const status = findingCounts.fail > 0
126
+ ? "fail"
127
+ : findingCounts.warn > 0
128
+ ? "warn"
129
+ : "pass";
130
+ return {
131
+ targetPath: path.resolve(targetPath),
132
+ status,
133
+ score: scoreSecurityAudit(findingCounts),
134
+ findingCounts,
135
+ findings: dedupedFindings
136
+ };
137
+ }
119
138
  export async function buildSecurityAudit(targetPath) {
120
139
  const discoveredPackage = await discoverPackage(targetPath);
121
140
  if (!discoveredPackage) {
@@ -133,23 +152,11 @@ export async function buildSecurityAudit(targetPath) {
133
152
  }
134
153
  const validationResult = await validatePlugin(discoveredPackage.rootPath);
135
154
  const validationSecurityFindings = validationResult.findings.filter((finding) => finding.id.startsWith("plugin.security."));
136
- const findings = dedupeFindings([
155
+ const findings = [
137
156
  ...validationSecurityFindings,
138
157
  ...(await auditMcpCommandSurface(discoveredPackage))
139
- ]);
140
- const findingCounts = buildFindingCounts(findings);
141
- const status = findingCounts.fail > 0
142
- ? "fail"
143
- : findingCounts.warn > 0
144
- ? "warn"
145
- : "pass";
146
- return {
147
- targetPath: discoveredPackage.rootPath,
148
- status,
149
- score: scoreSecurityAudit(findingCounts),
150
- findingCounts,
151
- findings
152
- };
158
+ ];
159
+ return buildSecurityAuditFromFindings(discoveredPackage.rootPath, findings);
153
160
  }
154
161
  export function renderSecurityAuditJson(audit) {
155
162
  return JSON.stringify({
@@ -0,0 +1,23 @@
1
+ import type { Finding } from "../domain/types.js";
2
+ export interface TrustScoreReport {
3
+ schemaVersion: "1.0.0";
4
+ generatedAt: string;
5
+ targetPath: string;
6
+ status: "pass" | "warn" | "fail";
7
+ exitCode: 0 | 1;
8
+ score: number;
9
+ findingCounts: {
10
+ fail: number;
11
+ warn: number;
12
+ total: number;
13
+ };
14
+ packageJson: {
15
+ present: boolean;
16
+ scriptsChecked: number;
17
+ dependenciesChecked: number;
18
+ };
19
+ findings: Finding[];
20
+ }
21
+ export declare function buildTrustScore(targetPath: string): Promise<TrustScoreReport>;
22
+ export declare function renderTrustScoreJson(report: TrustScoreReport): string;
23
+ export declare function renderTrustScore(report: TrustScoreReport): string;