codex-plugin-doctor 0.13.0 → 0.15.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,102 @@
1
+ import { buildPackageAnalysis } from "./package-analysis.js";
2
+ function findingKey(finding) {
3
+ return `${finding.category}\n${finding.id}\n${finding.message}`;
4
+ }
5
+ function collectComparableFindings(analysis) {
6
+ const findings = [
7
+ ...analysis.validation.findings.map((finding) => ({
8
+ ...finding,
9
+ category: "validation"
10
+ })),
11
+ ...analysis.security.findings.map((finding) => ({
12
+ ...finding,
13
+ category: "security"
14
+ }))
15
+ ];
16
+ const seen = new Set();
17
+ return findings.filter((finding) => {
18
+ const key = findingKey(finding);
19
+ if (seen.has(key)) {
20
+ return false;
21
+ }
22
+ seen.add(key);
23
+ return true;
24
+ });
25
+ }
26
+ function compareFindings(beforeFindings, afterFindings) {
27
+ const beforeKeys = new Set(beforeFindings.map(findingKey));
28
+ const afterKeys = new Set(afterFindings.map(findingKey));
29
+ return {
30
+ newFindings: afterFindings.filter((finding) => !beforeKeys.has(findingKey(finding))),
31
+ resolvedFindings: beforeFindings.filter((finding) => !afterKeys.has(findingKey(finding)))
32
+ };
33
+ }
34
+ export async function buildDoctorRiskDiffReport(beforePath, afterPath, options = {}) {
35
+ const [beforeAnalysis, afterAnalysis] = await Promise.all([
36
+ buildPackageAnalysis(beforePath, { environment: options.environment }),
37
+ buildPackageAnalysis(afterPath, { environment: options.environment })
38
+ ]);
39
+ const { newFindings, resolvedFindings } = compareFindings(collectComparableFindings(beforeAnalysis), collectComparableFindings(afterAnalysis));
40
+ const trustScoreDelta = afterAnalysis.trust.score - beforeAnalysis.trust.score;
41
+ const hasNewFailure = newFindings.some((finding) => finding.severity === "fail");
42
+ const hasNewWarning = newFindings.some((finding) => finding.severity === "warn");
43
+ const riskIncreased = hasNewFailure || trustScoreDelta < 0;
44
+ const status = riskIncreased
45
+ ? "fail"
46
+ : hasNewWarning
47
+ ? "warn"
48
+ : "pass";
49
+ return {
50
+ schemaVersion: "1.0.0",
51
+ generatedAt: new Date().toISOString(),
52
+ kind: "doctor.risk.diff",
53
+ beforePath: beforeAnalysis.targetPath,
54
+ afterPath: afterAnalysis.targetPath,
55
+ status,
56
+ exitCode: status === "fail" ? 1 : 0,
57
+ summary: {
58
+ riskIncreased,
59
+ newFindings: newFindings.length,
60
+ resolvedFindings: resolvedFindings.length,
61
+ trustScoreBefore: beforeAnalysis.trust.score,
62
+ trustScoreAfter: afterAnalysis.trust.score,
63
+ trustScoreDelta
64
+ },
65
+ newFindings,
66
+ resolvedFindings
67
+ };
68
+ }
69
+ export function renderDoctorRiskDiffReportJson(report) {
70
+ return JSON.stringify(report, null, 2);
71
+ }
72
+ export function renderDoctorRiskDiffReport(report, options = {}) {
73
+ const lines = [
74
+ "Doctor Risk Diff",
75
+ "================",
76
+ `Status: ${report.status.toUpperCase()}`,
77
+ `Before: ${report.beforePath}`,
78
+ `After: ${report.afterPath}`,
79
+ `Risk increased: ${report.summary.riskIncreased ? "yes" : "no"}`,
80
+ `Trust score delta: ${report.summary.trustScoreDelta}`,
81
+ `New findings: ${report.summary.newFindings}`,
82
+ `Resolved findings: ${report.summary.resolvedFindings}`
83
+ ];
84
+ if (options.outputPath) {
85
+ lines.push(`Output: ${options.outputPath}`);
86
+ }
87
+ if (report.newFindings.length > 0) {
88
+ lines.push("", "New Findings", "------------");
89
+ for (const finding of report.newFindings) {
90
+ lines.push(`[${finding.severity.toUpperCase()}] ${finding.category}: ${finding.id}`);
91
+ lines.push(` Message: ${finding.message}`);
92
+ }
93
+ }
94
+ if (report.resolvedFindings.length > 0) {
95
+ lines.push("", "Resolved Findings", "-----------------");
96
+ for (const finding of report.resolvedFindings) {
97
+ lines.push(`[${finding.severity.toUpperCase()}] ${finding.category}: ${finding.id}`);
98
+ lines.push(` Message: ${finding.message}`);
99
+ }
100
+ }
101
+ return lines.join("\n");
102
+ }
package/dist/index.d.ts CHANGED
@@ -1,9 +1,13 @@
1
1
  import type { CheckOptions, CheckResult } from "./domain/types.js";
2
2
  export { buildSecurityAudit, renderSecurityAuditJson, renderSecurityScorecard, type SecurityAudit } from "./security/security-audit.js";
3
- export { buildTrustScore, renderTrustScore, renderTrustScoreJson, type TrustScoreReport } from "./security/trust-score.js";
3
+ export { buildTrustScore, renderTrustScore, renderTrustScoreJson, type BuildTrustScoreOptions, type TrustScoreReport } from "./security/trust-score.js";
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 { buildDoctorExportBundleFromAnalysis, buildDoctorRecommendationsFromAnalysis, buildPackageAnalysis, type PackageAnalysis, type PackageAnalysisOptions, type PackageAnalysisStage, type PackageAnalysisTiming } from "./core/package-analysis.js";
8
+ export { buildDoctorPerformanceReport, renderDoctorPerformanceReport, renderDoctorPerformanceReportJson, type BuildDoctorPerformanceReportOptions, type DoctorPerformanceReport, type DoctorPerformanceStage, type DoctorPerformanceStageName } from "./core/performance-report.js";
9
+ export { buildDoctorNpmPackageReport, renderDoctorNpmPackageReport, renderDoctorNpmPackageReportJson, type BuildDoctorNpmPackageReportOptions, type DoctorNpmPackageReport } from "./core/npm-package-doctor.js";
10
+ export { buildDoctorRiskDiffReport, renderDoctorRiskDiffReport, renderDoctorRiskDiffReportJson, type BuildDoctorRiskDiffReportOptions, type DoctorRiskDiffReport, type RiskDiffFinding, type RiskFindingCategory } from "./core/risk-diff.js";
7
11
  export { buildEcosystemAudit, renderEcosystemAudit, renderEcosystemAuditJson, type EcosystemAuditReport } from "./audit/ecosystem-audit.js";
8
12
  export { applyPolicyToDoctorConfig, applyPolicyToSecurityAudit, parsePolicyPack, policyEnablesRuntime, policyFailsOnWarnings, policyPackNames, type PolicyPackName } from "./policy/policy-packs.js";
9
13
  export { buildGenericMcpDoctor, renderGenericMcpDoctor, renderGenericMcpDoctorJson, type GenericMcpDoctorReport } from "./mcp/generic-mcp-doctor.js";
package/dist/index.js CHANGED
@@ -4,6 +4,10 @@ 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 { buildDoctorExportBundleFromAnalysis, buildDoctorRecommendationsFromAnalysis, buildPackageAnalysis } from "./core/package-analysis.js";
8
+ export { buildDoctorPerformanceReport, renderDoctorPerformanceReport, renderDoctorPerformanceReportJson } from "./core/performance-report.js";
9
+ export { buildDoctorNpmPackageReport, renderDoctorNpmPackageReport, renderDoctorNpmPackageReportJson } from "./core/npm-package-doctor.js";
10
+ export { buildDoctorRiskDiffReport, renderDoctorRiskDiffReport, renderDoctorRiskDiffReportJson } from "./core/risk-diff.js";
7
11
  export { buildEcosystemAudit, renderEcosystemAudit, renderEcosystemAuditJson } from "./audit/ecosystem-audit.js";
8
12
  export { applyPolicyToDoctorConfig, applyPolicyToSecurityAudit, parsePolicyPack, policyEnablesRuntime, policyFailsOnWarnings, policyPackNames } from "./policy/policy-packs.js";
9
13
  export { buildGenericMcpDoctor, renderGenericMcpDoctor, renderGenericMcpDoctorJson } from "./mcp/generic-mcp-doctor.js";
@@ -215,6 +215,15 @@ export const ruleCatalog = [
215
215
  fix: "Use HTTPS for remote MCP servers; reserve HTTP for explicit localhost development endpoints.",
216
216
  example: '{ "url": "https://example.com/mcp" }'
217
217
  },
218
+ {
219
+ id: "plugin.security.prompt_injection_text",
220
+ category: "security",
221
+ defaultSeverity: "fail",
222
+ summary: "Packaged text contains prompt-injection or secret-exfiltration instructions.",
223
+ why: "Poisoned tool, prompt, resource, or skill text can instruct an agent to ignore higher-priority instructions or leak secrets when loaded into context.",
224
+ fix: "Remove hidden override or exfiltration instructions and keep descriptions scoped to legitimate behavior.",
225
+ example: "Keep SKILL.md, prompt, resource, and tool descriptions direct and user-facing."
226
+ },
218
227
  {
219
228
  id: "plugin.runtime.exited_early",
220
229
  category: "runtime",
package/dist/run-cli.js CHANGED
@@ -16,6 +16,9 @@ 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 { buildDoctorPerformanceReport, renderDoctorPerformanceReport, renderDoctorPerformanceReportJson } from "./core/performance-report.js";
20
+ import { buildDoctorNpmPackageReport, renderDoctorNpmPackageReport, renderDoctorNpmPackageReportJson } from "./core/npm-package-doctor.js";
21
+ import { buildDoctorRiskDiffReport, renderDoctorRiskDiffReport, renderDoctorRiskDiffReportJson } from "./core/risk-diff.js";
19
22
  import { applyFixPlan, buildFixPlan, renderApplyFixResult, renderFixPlanJsonReport, renderFixPlan } from "./core/fix-plan.js";
20
23
  import { renderClientDoctor, renderEnvironmentDoctor, renderEnvironmentDoctorJson } from "./core/environment-doctor.js";
21
24
  import { initCiWorkflow } from "./core/init-ci.js";
@@ -61,7 +64,7 @@ const defaultIo = {
61
64
  }
62
65
  };
63
66
  function printUsage(io) {
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");
67
+ 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>|diff --before <path> --after <path>|recommend <path>|trust <path>|perf <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");
65
68
  }
66
69
  function renderInstalledPlugins(plugins) {
67
70
  const lines = [
@@ -226,6 +229,68 @@ export async function runCli(args, io = defaultIo, options = {}) {
226
229
  io.writeStdout(renderedReport);
227
230
  return report.exitCode;
228
231
  }
232
+ if (maybePath === "npm") {
233
+ const packageSpec = remainingArgs[0] && !remainingArgs[0].startsWith("--")
234
+ ? remainingArgs[0]
235
+ : null;
236
+ if (!packageSpec) {
237
+ io.writeStderr("Missing package spec. Usage: codex-plugin-doctor doctor npm <package> [--json] [--output <path>]");
238
+ return 2;
239
+ }
240
+ const npmFlags = remainingArgs.slice(1);
241
+ const jsonOutput = npmFlags.includes("--json");
242
+ const outputIndex = npmFlags.indexOf("--output");
243
+ const outputPath = outputIndex === -1 ? null : npmFlags[outputIndex + 1];
244
+ if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
245
+ io.writeStderr("Missing path after --output.");
246
+ return 2;
247
+ }
248
+ const report = await buildDoctorNpmPackageReport(packageSpec, {
249
+ environment: {
250
+ env: terminalContext.env,
251
+ platform: terminalContext.platform
252
+ }
253
+ });
254
+ const renderedReport = jsonOutput
255
+ ? renderDoctorNpmPackageReportJson(report)
256
+ : renderDoctorNpmPackageReport(report, { outputPath });
257
+ if (outputPath) {
258
+ await writeFile(outputPath, renderedReport, "utf8");
259
+ }
260
+ io.writeStdout(renderedReport);
261
+ return report.summary.exitCode;
262
+ }
263
+ if (maybePath === "diff") {
264
+ const beforeIndex = remainingArgs.indexOf("--before");
265
+ const afterIndex = remainingArgs.indexOf("--after");
266
+ const beforePath = beforeIndex === -1 ? null : remainingArgs[beforeIndex + 1];
267
+ const afterPath = afterIndex === -1 ? null : remainingArgs[afterIndex + 1];
268
+ if (!beforePath || beforePath.startsWith("--") || !afterPath || afterPath.startsWith("--")) {
269
+ io.writeStderr("Usage: codex-plugin-doctor doctor diff --before <path> --after <path> [--json] [--output <path>]");
270
+ return 2;
271
+ }
272
+ const jsonOutput = remainingArgs.includes("--json");
273
+ const outputIndex = remainingArgs.indexOf("--output");
274
+ const outputPath = outputIndex === -1 ? null : remainingArgs[outputIndex + 1];
275
+ if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
276
+ io.writeStderr("Missing path after --output.");
277
+ return 2;
278
+ }
279
+ const report = await buildDoctorRiskDiffReport(beforePath, afterPath, {
280
+ environment: {
281
+ env: terminalContext.env,
282
+ platform: terminalContext.platform
283
+ }
284
+ });
285
+ const renderedReport = jsonOutput
286
+ ? renderDoctorRiskDiffReportJson(report)
287
+ : renderDoctorRiskDiffReport(report, { outputPath });
288
+ if (outputPath) {
289
+ await writeFile(outputPath, renderedReport, "utf8");
290
+ }
291
+ io.writeStdout(renderedReport);
292
+ return report.exitCode;
293
+ }
229
294
  if (maybePath === "trust") {
230
295
  const targetPath = remainingArgs[0] && !remainingArgs[0].startsWith("--")
231
296
  ? remainingArgs[0]
@@ -250,6 +315,38 @@ export async function runCli(args, io = defaultIo, options = {}) {
250
315
  io.writeStdout(renderedReport);
251
316
  return report.exitCode;
252
317
  }
318
+ if (maybePath === "perf") {
319
+ const targetPath = remainingArgs[0] && !remainingArgs[0].startsWith("--")
320
+ ? remainingArgs[0]
321
+ : ".";
322
+ const perfFlags = remainingArgs[0] && !remainingArgs[0].startsWith("--")
323
+ ? remainingArgs.slice(1)
324
+ : remainingArgs;
325
+ const jsonOutput = perfFlags.includes("--json");
326
+ const outputIndex = perfFlags.indexOf("--output");
327
+ const outputPath = outputIndex === -1 ? null : perfFlags[outputIndex + 1];
328
+ if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
329
+ io.writeStderr("Missing path after --output.");
330
+ return 2;
331
+ }
332
+ const report = await buildDoctorPerformanceReport(targetPath, {
333
+ environment: {
334
+ env: terminalContext.env,
335
+ platform: terminalContext.platform
336
+ },
337
+ runCheck: options.runCheckImpl
338
+ ? (pathToCheck) => options.runCheckImpl(pathToCheck)
339
+ : undefined
340
+ });
341
+ const renderedReport = jsonOutput
342
+ ? renderDoctorPerformanceReportJson(report)
343
+ : renderDoctorPerformanceReport(report, { outputPath });
344
+ if (outputPath) {
345
+ await writeFile(outputPath, renderedReport, "utf8");
346
+ }
347
+ io.writeStdout(renderedReport);
348
+ return report.exitCode;
349
+ }
253
350
  if (maybePath === "export") {
254
351
  const bundleIndex = remainingArgs.indexOf("--bundle");
255
352
  if (bundleIndex === -1) {
@@ -520,7 +617,7 @@ export async function runCli(args, io = defaultIo, options = {}) {
520
617
  const auditFlags = maybePath ? [maybePath, ...remainingArgs] : remainingArgs;
521
618
  const installed = auditFlags.includes("--installed");
522
619
  if (!installed) {
523
- io.writeStderr("Usage: codex-plugin-doctor audit --installed [filter] [--security] [--compat] [--json] [--output <path>]");
620
+ io.writeStderr("Usage: codex-plugin-doctor audit --installed [filter] [--security] [--compat] [--json] [--output <path>] [--cache] [--changed]");
524
621
  return 2;
525
622
  }
526
623
  const installedIndex = auditFlags.indexOf("--installed");
@@ -535,6 +632,10 @@ export async function runCli(args, io = defaultIo, options = {}) {
535
632
  const policyIndex = auditFlags.indexOf("--policy");
536
633
  const policyName = policyIndex === -1 ? null : auditFlags[policyIndex + 1];
537
634
  const policy = parsePolicyPack(policyName);
635
+ const cacheEnabled = auditFlags.includes("--cache") || auditFlags.includes("--changed");
636
+ const changedOnly = auditFlags.includes("--changed");
637
+ const cacheFileIndex = auditFlags.indexOf("--cache-file");
638
+ const cachePath = cacheFileIndex === -1 ? null : auditFlags[cacheFileIndex + 1];
538
639
  if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
539
640
  io.writeStderr("Missing path after --output.");
540
641
  return 2;
@@ -543,6 +644,10 @@ export async function runCli(args, io = defaultIo, options = {}) {
543
644
  io.writeStderr("Missing policy after --policy.");
544
645
  return 2;
545
646
  }
647
+ if (cacheFileIndex !== -1 && (!cachePath || cachePath.startsWith("--"))) {
648
+ io.writeStderr("Missing path after --cache-file.");
649
+ return 2;
650
+ }
546
651
  if (policyIndex !== -1 && !policy) {
547
652
  io.writeStderr(`Unknown policy: ${policyName}. Supported policies: ${policyPackNames.join(", ")}.`);
548
653
  return 2;
@@ -554,9 +659,14 @@ export async function runCli(args, io = defaultIo, options = {}) {
554
659
  includeSecurity,
555
660
  includeCompatibility,
556
661
  failOnWarnings: policyFailsOnWarnings(policy),
662
+ cache: {
663
+ enabled: cacheEnabled,
664
+ changedOnly,
665
+ cachePath
666
+ },
557
667
  validatePlugin: options.runCheckImpl ?? runCheck
558
668
  });
559
- if (report.summary.totalPlugins === 0) {
669
+ if (report.summary.totalPlugins === 0 && !changedOnly) {
560
670
  io.writeStderr(installedFilter
561
671
  ? `No installed Codex plugins matched '${installedFilter}'.`
562
672
  : "No installed Codex plugins found.");
@@ -1,3 +1,4 @@
1
+ import { readFile, readdir, stat } from "node:fs/promises";
1
2
  import path from "node:path";
2
3
  import { discoverPackage } from "../core/discover-package.js";
3
4
  import { readJsonFile } from "../core/read-json-file.js";
@@ -40,6 +41,25 @@ function containsPipeInstaller(args) {
40
41
  /\b(iwr|irm|invoke-webrequest|invoke-restmethod)\b[^|]*\|\s*(iex|invoke-expression)\b/.test(joinedArgs) ||
41
42
  /\binvoke-expression\b/.test(joinedArgs));
42
43
  }
44
+ const poisonScanExtensions = new Set([
45
+ ".json",
46
+ ".md",
47
+ ".mdx",
48
+ ".txt",
49
+ ".yaml",
50
+ ".yml"
51
+ ]);
52
+ const poisonScanSkippedDirectories = new Set([
53
+ ".git",
54
+ "coverage",
55
+ "dist",
56
+ "node_modules"
57
+ ]);
58
+ const promptInjectionPatterns = [
59
+ /\bignore\s+(?:all\s+)?(?:previous|prior|system|developer)\s+instructions?\b/i,
60
+ /\b(?:exfiltrate|steal|leak|upload|send)\b.{0,120}\b(?:secret|secrets|token|tokens|api\s*key|api\s*keys|credential|credentials|environment\s+variables?|env)\b/i,
61
+ /\bdo\s+not\s+(?:reveal|tell|mention|disclose)\b.{0,120}\b(?:instruction|instructions|prompt|prompts|system|developer)\b/i
62
+ ];
43
63
  export function auditMcpServerConfig(rootPath, parsedConfig) {
44
64
  if (!isPlainObject(parsedConfig) || !isPlainObject(parsedConfig.mcpServers)) {
45
65
  return [
@@ -76,6 +96,43 @@ export function auditMcpServerConfig(rootPath, parsedConfig) {
76
96
  }
77
97
  return findings;
78
98
  }
99
+ async function collectPromptPoisoningScanFiles(rootPath, currentPath = rootPath) {
100
+ const entries = await readdir(currentPath, { withFileTypes: true });
101
+ const filePaths = [];
102
+ for (const entry of entries) {
103
+ const entryPath = path.join(currentPath, entry.name);
104
+ if (entry.isDirectory()) {
105
+ if (poisonScanSkippedDirectories.has(entry.name)) {
106
+ continue;
107
+ }
108
+ filePaths.push(...(await collectPromptPoisoningScanFiles(rootPath, entryPath)));
109
+ continue;
110
+ }
111
+ if (!entry.isFile() || !poisonScanExtensions.has(path.extname(entry.name).toLowerCase())) {
112
+ continue;
113
+ }
114
+ const details = await stat(entryPath);
115
+ if (details.size <= 256 * 1024) {
116
+ filePaths.push(entryPath);
117
+ }
118
+ }
119
+ return filePaths;
120
+ }
121
+ function containsPromptInjectionText(content) {
122
+ return promptInjectionPatterns.some((pattern) => pattern.test(content));
123
+ }
124
+ async function auditPromptPoisoningSurface(rootPath) {
125
+ const findings = [];
126
+ for (const filePath of await collectPromptPoisoningScanFiles(rootPath)) {
127
+ const content = await readFile(filePath, "utf8");
128
+ if (!containsPromptInjectionText(content)) {
129
+ continue;
130
+ }
131
+ const relativeFilePath = path.relative(rootPath, filePath).replace(/\\/g, "/");
132
+ findings.push(buildFinding("fail", "plugin.security.prompt_injection_text", `The packaged text file \`${relativeFilePath}\` contains prompt-injection or secret-exfiltration style instructions.`, "Malicious or poisoned tool, prompt, resource, or skill text can instruct an agent to ignore higher-priority instructions or leak secrets when loaded into context.", "Remove hidden override or exfiltration instructions, then keep tool/prompt/resource descriptions scoped to the legitimate user-facing behavior."));
133
+ }
134
+ return findings;
135
+ }
79
136
  async function auditMcpCommandSurface(discoveredPackage) {
80
137
  const { manifest, rootPath } = discoveredPackage;
81
138
  if (!manifest.mcpServers) {
@@ -154,7 +211,8 @@ export async function buildSecurityAudit(targetPath) {
154
211
  const validationSecurityFindings = validationResult.findings.filter((finding) => finding.id.startsWith("plugin.security."));
155
212
  const findings = [
156
213
  ...validationSecurityFindings,
157
- ...(await auditMcpCommandSurface(discoveredPackage))
214
+ ...(await auditMcpCommandSurface(discoveredPackage)),
215
+ ...(await auditPromptPoisoningSurface(discoveredPackage.rootPath))
158
216
  ];
159
217
  return buildSecurityAuditFromFindings(discoveredPackage.rootPath, findings);
160
218
  }
@@ -1,4 +1,5 @@
1
1
  import type { Finding } from "../domain/types.js";
2
+ import { type SecurityAudit } from "./security-audit.js";
2
3
  export interface TrustScoreReport {
3
4
  schemaVersion: "1.0.0";
4
5
  generatedAt: string;
@@ -18,6 +19,9 @@ export interface TrustScoreReport {
18
19
  };
19
20
  findings: Finding[];
20
21
  }
21
- export declare function buildTrustScore(targetPath: string): Promise<TrustScoreReport>;
22
+ export interface BuildTrustScoreOptions {
23
+ securityAudit?: SecurityAudit | null;
24
+ }
25
+ export declare function buildTrustScore(targetPath: string, options?: BuildTrustScoreOptions): Promise<TrustScoreReport>;
22
26
  export declare function renderTrustScoreJson(report: TrustScoreReport): string;
23
27
  export declare function renderTrustScore(report: TrustScoreReport): string;
@@ -116,7 +116,7 @@ function scoreFindings(findings) {
116
116
  const warnCount = findings.filter((finding) => finding.severity === "warn").length;
117
117
  return Math.max(0, 100 - (failCount * 35) - (warnCount * 10));
118
118
  }
119
- export async function buildTrustScore(targetPath) {
119
+ export async function buildTrustScore(targetPath, options = {}) {
120
120
  const rootPath = path.resolve(targetPath);
121
121
  const packageJson = await readPackageJson(rootPath);
122
122
  const scriptAudit = packageJson
@@ -125,10 +125,11 @@ export async function buildTrustScore(targetPath) {
125
125
  const dependencyAudit = packageJson
126
126
  ? auditDependencies(packageJson)
127
127
  : { findings: [], dependenciesChecked: 0 };
128
- const discoveredPackage = await discoverPackage(rootPath);
129
- const securityAudit = discoveredPackage
130
- ? await buildSecurityAudit(rootPath)
131
- : null;
128
+ const securityAudit = options.securityAudit !== undefined
129
+ ? options.securityAudit
130
+ : await discoverPackage(rootPath)
131
+ ? await buildSecurityAudit(rootPath)
132
+ : null;
132
133
  const findings = dedupeFindings([
133
134
  ...scriptAudit.findings,
134
135
  ...dependencyAudit.findings,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-plugin-doctor",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
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",