codex-plugin-doctor 1.56.0 → 1.58.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.
Files changed (42) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +3 -2
  3. package/dist/compatibility/claude-desktop-install-preview.js +4 -4
  4. package/dist/compatibility/cline-install-preview.js +4 -4
  5. package/dist/compatibility/compatibility-matrix.js +13 -17
  6. package/dist/compatibility/cursor-install-preview.js +4 -4
  7. package/dist/compatibility/windsurf-install-preview.js +4 -4
  8. package/dist/core/doctor-size.d.ts +5 -1
  9. package/dist/core/doctor-size.js +77 -3
  10. package/dist/core/init-plugin.js +2 -2
  11. package/dist/core/inspector-bridge.js +8 -13
  12. package/dist/core/mcp-config-normalizer.d.ts +18 -0
  13. package/dist/core/mcp-config-normalizer.js +50 -0
  14. package/dist/core/package-path.d.ts +1 -0
  15. package/dist/core/package-path.js +25 -0
  16. package/dist/core/plugin-components.d.ts +7 -0
  17. package/dist/core/plugin-components.js +169 -0
  18. package/dist/core/plugin-hooks.d.ts +2 -0
  19. package/dist/core/plugin-hooks.js +190 -0
  20. package/dist/core/runtime-plan.js +97 -16
  21. package/dist/core/runtime-probe.js +3 -8
  22. package/dist/core/validate-plugin.js +39 -17
  23. package/dist/domain/types.d.ts +34 -0
  24. package/dist/domain/types.js +13 -1
  25. package/dist/mcp/generic-mcp-doctor.js +45 -29
  26. package/dist/rules/rule-catalog.js +130 -4
  27. package/dist/run-cli.js +32 -2
  28. package/dist/security/security-audit.d.ts +1 -0
  29. package/dist/security/security-audit.js +104 -7
  30. package/examples/README.md +94 -94
  31. package/examples/codex-doctor-generic-mcp/.mcp.json +10 -10
  32. package/examples/codex-doctor-generic-mcp/server.js +1 -1
  33. package/examples/codex-doctor-risky/.codex-plugin/plugin.json +7 -7
  34. package/examples/codex-doctor-risky/.mcp.json +12 -12
  35. package/examples/codex-doctor-risky/mock-server.js +1 -1
  36. package/examples/codex-doctor-runtime/.codex-plugin/plugin.json +8 -8
  37. package/examples/codex-doctor-runtime/.mcp.json +9 -9
  38. package/examples/codex-doctor-runtime/mock-server.js +237 -237
  39. package/examples/codex-doctor-runtime/skills/context-check/SKILL.md +7 -7
  40. package/examples/codex-doctor-starter/.codex-plugin/plugin.json +7 -7
  41. package/examples/codex-doctor-starter/skills/repo-scan/SKILL.md +7 -7
  42. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  import { readFile, readdir, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { discoverPackage } from "../core/discover-package.js";
4
+ import { normalizeMcpConfig } from "../core/mcp-config-normalizer.js";
4
5
  import { readJsonFile } from "../core/read-json-file.js";
5
6
  import { inspectRemoteMcpUrl } from "../core/remote-url-policy.js";
6
7
  import { validatePlugin } from "../core/validate-plugin.js";
@@ -58,6 +59,101 @@ function containsPipeInstaller(args) {
58
59
  /\b(iwr|irm|invoke-webrequest|invoke-restmethod)\b[^|]*\|\s*(iex|invoke-expression)\b/.test(joinedArgs) ||
59
60
  /\binvoke-expression\b/.test(joinedArgs));
60
61
  }
62
+ function relativeSourcePath(rootPath, sourcePath) {
63
+ return relativePackagePath(rootPath, path.resolve(rootPath, sourcePath));
64
+ }
65
+ export function auditHookCommand(rootPath, sourcePath, event, field, command) {
66
+ const evidence = { sourcePath: relativeSourcePath(rootPath, sourcePath), event, field };
67
+ const findings = [];
68
+ if (/(?:^|\s)[/-]enc(?:odedcommand)?(?=\s|$)/i.test(command)) {
69
+ findings.push(buildFinding("fail", "plugin.security.encoded_command", "A plugin lifecycle hook uses an encoded shell command flag.", "Encoded command payloads hide the executed script from reviewers and increase supply-chain risk.", "Replace encoded shell payloads with a checked-in script or readable direct command.", evidence));
70
+ }
71
+ if (containsHookRemotePipeInstaller(command)) {
72
+ findings.push(buildFinding("fail", "plugin.security.remote_pipe_install", "A plugin lifecycle hook appears to pipe remote content into a shell.", "Download-and-execute patterns can run unreviewed remote code when a host invokes the hook.", "Pin dependencies through the package manager or use a reviewed local script instead of piping remote content to a shell.", evidence));
73
+ }
74
+ if (/^\s*(?:cmd(?:\.exe)?\s+\/c|(?:powershell|pwsh)(?:\.exe)?\s+-(?:command|c)\b)/i.test(command)) {
75
+ findings.push(buildFinding("warn", "plugin.security.command_shell_wrapper", "A plugin lifecycle hook starts through a shell wrapper.", "Shell wrappers expand quoting, pipes, aliases, and platform-specific behavior, which makes the execution path harder to audit.", "Prefer a concrete executable or checked-in script with explicit arguments.", evidence));
76
+ }
77
+ return findings;
78
+ }
79
+ function containsHookRemotePipeInstaller(command) {
80
+ const firstPipeIndex = command.indexOf("|");
81
+ if (firstPipeIndex === -1)
82
+ return false;
83
+ const leftHandSide = command.slice(0, firstPipeIndex);
84
+ const rightHandSide = command.slice(firstPipeIndex + 1).trim();
85
+ const interpreter = /^(?:\/(?:[^/\s|]+\/)*(?:sh|bash)\b|(?:sh|bash)\b|(?:powershell|pwsh)(?:\.exe)?\s+-(?:command|c)\s+-\s*(?:$|[;&|])|(?:iex|invoke-expression)\b)/i;
86
+ return isInvokedHookDownloader(leftHandSide) && interpreter.test(rightHandSide);
87
+ }
88
+ function isInvokedHookDownloader(command) {
89
+ const tokens = command.trim().split(/\s+/).filter(Boolean);
90
+ let index = 0;
91
+ while (index < tokens.length) {
92
+ const token = tokens[index].toLowerCase();
93
+ if (token === "env") {
94
+ index += 1;
95
+ while (index < tokens.length) {
96
+ const envToken = tokens[index];
97
+ if (envToken === "--") {
98
+ index += 1;
99
+ break;
100
+ }
101
+ if (envToken === "-i" || envToken === "--ignore-environment") {
102
+ index += 1;
103
+ continue;
104
+ }
105
+ if (envToken === "-u" || envToken === "--unset") {
106
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(tokens[index + 1] ?? "")) {
107
+ return false;
108
+ }
109
+ index += 2;
110
+ continue;
111
+ }
112
+ if (/^--unset=[A-Za-z_][A-Za-z0-9_]*$/.test(envToken)) {
113
+ index += 1;
114
+ continue;
115
+ }
116
+ if (envToken === "-C" || envToken === "--chdir") {
117
+ if (!tokens[index + 1] || tokens[index + 1].startsWith("-")) {
118
+ return false;
119
+ }
120
+ index += 2;
121
+ continue;
122
+ }
123
+ if (/^--chdir=\S+$/.test(envToken)) {
124
+ index += 1;
125
+ continue;
126
+ }
127
+ if (/^[A-Za-z_][A-Za-z0-9_]*=\S*$/.test(envToken)) {
128
+ index += 1;
129
+ continue;
130
+ }
131
+ break;
132
+ }
133
+ continue;
134
+ }
135
+ if (token === "command") {
136
+ index += 1;
137
+ if (tokens[index] === "-p" || tokens[index]?.toLowerCase() === "--default-search-path") {
138
+ index += 1;
139
+ }
140
+ if (tokens[index] === "--") {
141
+ index += 1;
142
+ }
143
+ continue;
144
+ }
145
+ break;
146
+ }
147
+ const downloader = normalizeCommandName(tokens[index] ?? "");
148
+ if (new Set(["curl", "wget", "iwr", "irm", "invoke-webrequest", "invoke-restmethod"]).has(downloader)) {
149
+ return true;
150
+ }
151
+ const shell = normalizeCommandName(tokens[index] ?? "");
152
+ return ((shell === "powershell" || shell === "pwsh") &&
153
+ /^-(?:command|c)$/i.test(tokens[index + 1] ?? "") &&
154
+ new Set(["curl", "wget", "iwr", "irm", "invoke-webrequest", "invoke-restmethod"])
155
+ .has(normalizeCommandName(tokens[index + 2] ?? "")));
156
+ }
61
157
  const pathLikeArgFlags = new Set([
62
158
  "--config",
63
159
  "--config-path",
@@ -200,16 +296,14 @@ export function auditMcpServerConfig(rootPath, parsedConfig, options = {}) {
200
296
  const configPath = options.configPath
201
297
  ? relativePackagePath(rootPath, options.configPath)
202
298
  : ".mcp.json";
203
- if (!isPlainObject(parsedConfig) || !isPlainObject(parsedConfig.mcpServers)) {
299
+ const normalizedConfig = normalizeMcpConfig(parsedConfig);
300
+ if (!normalizedConfig.ok) {
204
301
  return [
205
- 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.", { configPath })
302
+ buildFinding("fail", "plugin.security.audit_unavailable", "The MCP security audit could not find a valid MCP server map.", "Without server entries, the audit cannot evaluate command execution or remote transport risk.", "Use a direct server map, `mcp_servers`, or `mcpServers`.", { configPath })
206
303
  ];
207
304
  }
208
305
  const findings = [];
209
- for (const [serverName, serverConfig] of Object.entries(parsedConfig.mcpServers)) {
210
- if (!isPlainObject(serverConfig)) {
211
- continue;
212
- }
306
+ for (const [serverName, serverConfig] of Object.entries(normalizedConfig.servers)) {
213
307
  const command = serverConfig.command;
214
308
  const args = serverConfig.args;
215
309
  const cwd = serverConfig.cwd;
@@ -395,7 +489,10 @@ async function auditMcpCommandSurface(discoveredPackage) {
395
489
  function dedupeFindings(findings) {
396
490
  const seen = new Set();
397
491
  return findings.filter((finding) => {
398
- const key = `${finding.id}\n${finding.message}`;
492
+ const hookLocation = finding.evidence?.sourcePath && finding.evidence.event && finding.evidence.field
493
+ ? `${finding.evidence.sourcePath}\n${finding.evidence.event}\n${finding.evidence.field}`
494
+ : "";
495
+ const key = `${finding.id}\n${finding.message}\n${hookLocation}`;
399
496
  if (seen.has(key)) {
400
497
  return false;
401
498
  }
@@ -1,94 +1,94 @@
1
- # Examples
2
-
3
- This folder contains manual example packs for local testing. Unlike `tests/fixtures`, these examples are meant for humans to run directly against the CLI.
4
-
5
- ## Example Packs
6
-
7
- Use the smallest example that matches what you need to prove:
8
-
9
- - `codex-doctor-starter` for static manifest and skill validation.
10
- - `codex-doctor-runtime` for full MCP runtime probing and compatibility scoring.
11
- - `codex-doctor-risky` for security finding output.
12
- - `codex-doctor-generic-mcp` for MCP-only packages without a Codex plugin manifest.
13
-
14
- ### `codex-doctor-starter`
15
-
16
- Minimal valid Codex plugin package with one skill and no runtime MCP server.
17
-
18
- Expected result:
19
-
20
- - static validation passes
21
- - no runtime probing needed
22
-
23
- Command:
24
-
25
- ```bash
26
- codex-plugin-doctor check examples/codex-doctor-starter
27
- ```
28
-
29
- ### `codex-doctor-runtime`
30
-
31
- Valid Codex plugin package with:
32
-
33
- - skill metadata
34
- - `.mcp.json`
35
- - mock MCP stdio server
36
- - `tools/list`
37
- - `tools/call`
38
- - `resources/list`
39
- - `resources/read`
40
- - `resources/templates/list`
41
- - `prompts/list`
42
- - `prompts/get`
43
-
44
- Expected result:
45
-
46
- - static validation passes
47
- - runtime validation passes
48
- - runtime scorecard shows all supported runtime capabilities as `pass`
49
-
50
- Command:
51
-
52
- ```bash
53
- codex-plugin-doctor check examples/codex-doctor-runtime --json --runtime --verbose-runtime
54
- ```
55
-
56
- ### `codex-doctor-risky`
57
-
58
- Intentionally flawed package for showing failure output.
59
-
60
- Expected result:
61
-
62
- - security finding for hard-coded secret
63
-
64
- Command:
65
-
66
- ```bash
67
- codex-plugin-doctor check examples/codex-doctor-risky --ascii
68
- ```
69
-
70
- ### `codex-doctor-generic-mcp`
71
-
72
- Generic MCP package with a `.mcp.json` file and no Codex plugin manifest.
73
-
74
- Expected result:
75
-
76
- - `codex-plugin-doctor mcp` passes generic MCP static health checks
77
- - `codex-plugin-doctor check` still reports the missing Codex plugin manifest
78
-
79
- Command:
80
-
81
- ```bash
82
- codex-plugin-doctor mcp examples/codex-doctor-generic-mcp
83
- ```
84
-
85
- ## Suggested Local Flow
86
-
87
- ```bash
88
- npm install
89
- npm run build
90
- npm link
91
- codex-plugin-doctor check examples/codex-doctor-starter
92
- codex-plugin-doctor check examples/codex-doctor-runtime --json --runtime --verbose-runtime
93
- codex-plugin-doctor check examples/codex-doctor-risky --ascii
94
- ```
1
+ # Examples
2
+
3
+ This folder contains manual example packs for local testing. Unlike `tests/fixtures`, these examples are meant for humans to run directly against the CLI.
4
+
5
+ ## Example Packs
6
+
7
+ Use the smallest example that matches what you need to prove:
8
+
9
+ - `codex-doctor-starter` for static manifest and skill validation.
10
+ - `codex-doctor-runtime` for full MCP runtime probing and compatibility scoring.
11
+ - `codex-doctor-risky` for security finding output.
12
+ - `codex-doctor-generic-mcp` for MCP-only packages without a Codex plugin manifest.
13
+
14
+ ### `codex-doctor-starter`
15
+
16
+ Minimal valid Codex plugin package with one skill and no runtime MCP server.
17
+
18
+ Expected result:
19
+
20
+ - static validation passes
21
+ - no runtime probing needed
22
+
23
+ Command:
24
+
25
+ ```bash
26
+ codex-plugin-doctor check examples/codex-doctor-starter
27
+ ```
28
+
29
+ ### `codex-doctor-runtime`
30
+
31
+ Valid Codex plugin package with:
32
+
33
+ - skill metadata
34
+ - `.mcp.json`
35
+ - mock MCP stdio server
36
+ - `tools/list`
37
+ - `tools/call`
38
+ - `resources/list`
39
+ - `resources/read`
40
+ - `resources/templates/list`
41
+ - `prompts/list`
42
+ - `prompts/get`
43
+
44
+ Expected result:
45
+
46
+ - static validation passes
47
+ - runtime validation passes
48
+ - runtime scorecard shows all supported runtime capabilities as `pass`
49
+
50
+ Command:
51
+
52
+ ```bash
53
+ codex-plugin-doctor check examples/codex-doctor-runtime --json --runtime --verbose-runtime
54
+ ```
55
+
56
+ ### `codex-doctor-risky`
57
+
58
+ Intentionally flawed package for showing failure output.
59
+
60
+ Expected result:
61
+
62
+ - security finding for hard-coded secret
63
+
64
+ Command:
65
+
66
+ ```bash
67
+ codex-plugin-doctor check examples/codex-doctor-risky --ascii
68
+ ```
69
+
70
+ ### `codex-doctor-generic-mcp`
71
+
72
+ Generic MCP package with a `.mcp.json` file and no Codex plugin manifest.
73
+
74
+ Expected result:
75
+
76
+ - `codex-plugin-doctor mcp` passes generic MCP static health checks
77
+ - `codex-plugin-doctor check` still reports the missing Codex plugin manifest
78
+
79
+ Command:
80
+
81
+ ```bash
82
+ codex-plugin-doctor mcp examples/codex-doctor-generic-mcp
83
+ ```
84
+
85
+ ## Suggested Local Flow
86
+
87
+ ```bash
88
+ npm install
89
+ npm run build
90
+ npm link
91
+ codex-plugin-doctor check examples/codex-doctor-starter
92
+ codex-plugin-doctor check examples/codex-doctor-runtime --json --runtime --verbose-runtime
93
+ codex-plugin-doctor check examples/codex-doctor-risky --ascii
94
+ ```
@@ -1,10 +1,10 @@
1
- {
2
- "mcpServers": {
3
- "genericRuntime": {
4
- "command": "node",
5
- "args": [
6
- "server.js"
7
- ]
8
- }
9
- }
10
- }
1
+ {
2
+ "mcpServers": {
3
+ "genericRuntime": {
4
+ "command": "node",
5
+ "args": [
6
+ "server.js"
7
+ ]
8
+ }
9
+ }
10
+ }
@@ -1 +1 @@
1
- process.stdin.resume();
1
+ process.stdin.resume();
@@ -1,7 +1,7 @@
1
- {
2
- "name": "codex-doctor-risky",
3
- "version": "1.0.0",
4
- "description": "Intentionally risky sample package for showing Codex Doctor failure output.",
5
- "mcpServers": "./.mcp.json"
6
- }
7
-
1
+ {
2
+ "name": "codex-doctor-risky",
3
+ "version": "1.0.0",
4
+ "description": "Intentionally risky sample package for showing Codex Doctor failure output.",
5
+ "mcpServers": "./.mcp.json"
6
+ }
7
+
@@ -1,12 +1,12 @@
1
- {
2
- "mcpServers": {
3
- "dangerServer": {
4
- "command": "node",
5
- "args": ["./mock-server.js"],
6
- "env": {
7
- "OPENAI_API_KEY": "sk-live-example-secret-value"
8
- }
9
- }
10
- }
11
- }
12
-
1
+ {
2
+ "mcpServers": {
3
+ "dangerServer": {
4
+ "command": "node",
5
+ "args": ["./mock-server.js"],
6
+ "env": {
7
+ "OPENAI_API_KEY": "sk-live-example-secret-value"
8
+ }
9
+ }
10
+ }
11
+ }
12
+
@@ -1 +1 @@
1
- process.stdin.resume();
1
+ process.stdin.resume();
@@ -1,8 +1,8 @@
1
- {
2
- "name": "codex-doctor-runtime",
3
- "version": "1.0.0",
4
- "description": "Runtime-complete Codex Doctor sample plugin with MCP validation coverage.",
5
- "skills": "./skills/",
6
- "mcpServers": "./.mcp.json"
7
- }
8
-
1
+ {
2
+ "name": "codex-doctor-runtime",
3
+ "version": "1.0.0",
4
+ "description": "Runtime-complete Codex Doctor sample plugin with MCP validation coverage.",
5
+ "skills": "./skills/",
6
+ "mcpServers": "./.mcp.json"
7
+ }
8
+
@@ -1,9 +1,9 @@
1
- {
2
- "mcpServers": {
3
- "doctorRuntime": {
4
- "command": "node",
5
- "args": ["./mock-server.js"]
6
- }
7
- }
8
- }
9
-
1
+ {
2
+ "mcpServers": {
3
+ "doctorRuntime": {
4
+ "command": "node",
5
+ "args": ["./mock-server.js"]
6
+ }
7
+ }
8
+ }
9
+