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.
- package/LICENSE +21 -21
- package/README.md +3 -2
- package/dist/compatibility/claude-desktop-install-preview.js +4 -4
- package/dist/compatibility/cline-install-preview.js +4 -4
- package/dist/compatibility/compatibility-matrix.js +13 -17
- package/dist/compatibility/cursor-install-preview.js +4 -4
- package/dist/compatibility/windsurf-install-preview.js +4 -4
- package/dist/core/doctor-size.d.ts +5 -1
- package/dist/core/doctor-size.js +77 -3
- package/dist/core/init-plugin.js +2 -2
- package/dist/core/inspector-bridge.js +8 -13
- package/dist/core/mcp-config-normalizer.d.ts +18 -0
- package/dist/core/mcp-config-normalizer.js +50 -0
- package/dist/core/package-path.d.ts +1 -0
- package/dist/core/package-path.js +25 -0
- package/dist/core/plugin-components.d.ts +7 -0
- package/dist/core/plugin-components.js +169 -0
- package/dist/core/plugin-hooks.d.ts +2 -0
- package/dist/core/plugin-hooks.js +190 -0
- package/dist/core/runtime-plan.js +97 -16
- package/dist/core/runtime-probe.js +3 -8
- package/dist/core/validate-plugin.js +39 -17
- package/dist/domain/types.d.ts +34 -0
- package/dist/domain/types.js +13 -1
- package/dist/mcp/generic-mcp-doctor.js +45 -29
- package/dist/rules/rule-catalog.js +130 -4
- package/dist/run-cli.js +32 -2
- package/dist/security/security-audit.d.ts +1 -0
- package/dist/security/security-audit.js +104 -7
- package/examples/README.md +94 -94
- package/examples/codex-doctor-generic-mcp/.mcp.json +10 -10
- package/examples/codex-doctor-generic-mcp/server.js +1 -1
- package/examples/codex-doctor-risky/.codex-plugin/plugin.json +7 -7
- package/examples/codex-doctor-risky/.mcp.json +12 -12
- package/examples/codex-doctor-risky/mock-server.js +1 -1
- package/examples/codex-doctor-runtime/.codex-plugin/plugin.json +8 -8
- package/examples/codex-doctor-runtime/.mcp.json +9 -9
- package/examples/codex-doctor-runtime/mock-server.js +237 -237
- package/examples/codex-doctor-runtime/skills/context-check/SKILL.md +7 -7
- package/examples/codex-doctor-starter/.codex-plugin/plugin.json +7 -7
- package/examples/codex-doctor-starter/skills/repo-scan/SKILL.md +7 -7
- package/package.json +1 -1
|
@@ -2,6 +2,9 @@ import { readFile, readdir, stat } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { withFindingFingerprints } from "../reporting/finding-fingerprint.js";
|
|
4
4
|
import { discoverPackage } from "./discover-package.js";
|
|
5
|
+
import { normalizeMcpConfig } from "./mcp-config-normalizer.js";
|
|
6
|
+
import { validatePluginComponents } from "./plugin-components.js";
|
|
7
|
+
import { validatePluginHooks } from "./plugin-hooks.js";
|
|
5
8
|
import { inspectRemoteMcpUrl } from "./remote-url-policy.js";
|
|
6
9
|
import { probeRuntime, remoteReliabilityGatePassed } from "./runtime-probe.js";
|
|
7
10
|
function buildFailure(id, message, impact, suggestedFix, evidence) {
|
|
@@ -24,6 +27,9 @@ function buildWarning(id, message, impact, suggestedFix, evidence) {
|
|
|
24
27
|
...(evidence ? { evidence } : {})
|
|
25
28
|
};
|
|
26
29
|
}
|
|
30
|
+
function hasInvalidComponentPath(findings, field) {
|
|
31
|
+
return findings.some((finding) => finding.id === "plugin.manifest.invalid_path" && finding.evidence?.field === field);
|
|
32
|
+
}
|
|
27
33
|
function remoteUrlIssueFindingId(issue) {
|
|
28
34
|
return issue === "insecure_non_loopback"
|
|
29
35
|
? "plugin.security.insecure_http_url"
|
|
@@ -223,9 +229,11 @@ function validateRequiredManifestFields(discoveredPackage) {
|
|
|
223
229
|
}
|
|
224
230
|
async function validateSkillsDirectory(discoveredPackage) {
|
|
225
231
|
const { manifest, rootPath } = discoveredPackage;
|
|
226
|
-
if (
|
|
232
|
+
if (manifest.skills === undefined) {
|
|
227
233
|
return [];
|
|
228
234
|
}
|
|
235
|
+
if (typeof manifest.skills !== "string")
|
|
236
|
+
return [];
|
|
229
237
|
const skillsPath = path.resolve(rootPath, manifest.skills);
|
|
230
238
|
if (!isPathWithinRoot(rootPath, skillsPath)) {
|
|
231
239
|
return [
|
|
@@ -233,7 +241,7 @@ async function validateSkillsDirectory(discoveredPackage) {
|
|
|
233
241
|
manifestPath: relativePackagePath(rootPath, discoveredPackage.manifestPath),
|
|
234
242
|
field: "skills",
|
|
235
243
|
configuredPath: manifest.skills,
|
|
236
|
-
resolvedPath: skillsPath
|
|
244
|
+
resolvedPath: relativePackagePath(rootPath, skillsPath)
|
|
237
245
|
})
|
|
238
246
|
];
|
|
239
247
|
}
|
|
@@ -246,13 +254,13 @@ async function validateSkillsDirectory(discoveredPackage) {
|
|
|
246
254
|
manifestPath: relativePackagePath(rootPath, discoveredPackage.manifestPath),
|
|
247
255
|
field: "skills",
|
|
248
256
|
configuredPath: manifest.skills,
|
|
249
|
-
resolvedPath: skillsPath
|
|
257
|
+
resolvedPath: relativePackagePath(rootPath, skillsPath)
|
|
250
258
|
})
|
|
251
259
|
];
|
|
252
260
|
}
|
|
253
261
|
async function validateSkillDefinitions(discoveredPackage) {
|
|
254
262
|
const { manifest, rootPath } = discoveredPackage;
|
|
255
|
-
if (
|
|
263
|
+
if (typeof manifest.skills !== "string") {
|
|
256
264
|
return [];
|
|
257
265
|
}
|
|
258
266
|
const skillsPath = path.resolve(rootPath, manifest.skills);
|
|
@@ -319,7 +327,7 @@ async function validateSkillDefinitions(discoveredPackage) {
|
|
|
319
327
|
}
|
|
320
328
|
async function validateMcpConfig(discoveredPackage) {
|
|
321
329
|
const { manifest, rootPath } = discoveredPackage;
|
|
322
|
-
if (
|
|
330
|
+
if (typeof manifest.mcpServers !== "string") {
|
|
323
331
|
return [];
|
|
324
332
|
}
|
|
325
333
|
const mcpConfigPath = path.resolve(rootPath, manifest.mcpServers);
|
|
@@ -353,20 +361,28 @@ async function validateMcpConfig(discoveredPackage) {
|
|
|
353
361
|
buildFailure("plugin.mcp.invalid_json", "The referenced `.mcp.json` file is not valid JSON.", "Codex will not be able to parse bundled MCP server configuration.", `Fix the JSON syntax in \`${mcpConfigPath}\`.`, { configPath: relativePackagePath(rootPath, mcpConfigPath), field: "json" })
|
|
354
362
|
];
|
|
355
363
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
364
|
+
const normalizedConfig = normalizeMcpConfig(parsedConfig);
|
|
365
|
+
if (!normalizedConfig.ok) {
|
|
366
|
+
if (normalizedConfig.reason === "ambiguous_shape") {
|
|
367
|
+
return [
|
|
368
|
+
buildFailure("plugin.mcp.ambiguous_shape", "The referenced `.mcp.json` file uses an ambiguous MCP config layout.", "Codex cannot safely choose between multiple wrapper layouts when they appear in one configuration file.", "Use exactly one supported layout: a direct server map, `mcp_servers`, or `mcpServers`.", { configPath: relativePackagePath(rootPath, mcpConfigPath), field: "root" })
|
|
369
|
+
];
|
|
370
|
+
}
|
|
371
|
+
if (normalizedConfig.field === "server" && normalizedConfig.invalidServerNames) {
|
|
372
|
+
return normalizedConfig.invalidServerNames.map((serverName) => buildFailure("plugin.mcp.server.invalid", `The MCP server \`${serverName}\` must be configured as an object.`, "Codex cannot interpret a server entry unless it is represented as an object with server options.", `Change the \`${serverName}\` entry in \`${mcpConfigPath}\` to an object.`, {
|
|
373
|
+
configPath: relativePackagePath(rootPath, mcpConfigPath),
|
|
374
|
+
serverName,
|
|
375
|
+
field: "server"
|
|
376
|
+
}));
|
|
377
|
+
}
|
|
363
378
|
return [
|
|
364
|
-
buildFailure("plugin.mcp.invalid_shape", "The referenced `.mcp.json` file must contain a
|
|
379
|
+
buildFailure("plugin.mcp.invalid_shape", "The referenced `.mcp.json` file must contain a valid MCP server map.", "Without valid server entries, Codex cannot discover the bundled MCP server definitions.", `Use a direct server map, \`mcp_servers\`, or \`mcpServers\` in \`${mcpConfigPath}\`.`, {
|
|
365
380
|
configPath: relativePackagePath(rootPath, mcpConfigPath),
|
|
366
|
-
field:
|
|
381
|
+
field: normalizedConfig.field
|
|
367
382
|
})
|
|
368
383
|
];
|
|
369
384
|
}
|
|
385
|
+
const servers = normalizedConfig.servers;
|
|
370
386
|
const findings = [];
|
|
371
387
|
for (const [serverName, serverConfig] of Object.entries(servers)) {
|
|
372
388
|
if (!isPlainObject(serverConfig)) {
|
|
@@ -433,11 +449,17 @@ export async function validatePlugin(targetPath, options = {}) {
|
|
|
433
449
|
findings: withFindingFingerprints([await buildMissingManifestFailure(rootPath)], rootPath)
|
|
434
450
|
};
|
|
435
451
|
}
|
|
452
|
+
const componentFindings = await validatePluginComponents(discoveredPackage);
|
|
453
|
+
const hookFindings = await validatePluginHooks(discoveredPackage);
|
|
454
|
+
const hasInvalidSkillsPath = hasInvalidComponentPath(componentFindings, "skills");
|
|
455
|
+
const hasInvalidMcpPath = hasInvalidComponentPath(componentFindings, "mcpServers");
|
|
436
456
|
const staticFindings = [
|
|
437
457
|
...validateRequiredManifestFields(discoveredPackage),
|
|
438
|
-
...
|
|
439
|
-
...
|
|
440
|
-
...(await
|
|
458
|
+
...componentFindings,
|
|
459
|
+
...hookFindings,
|
|
460
|
+
...(hasInvalidSkillsPath ? [] : await validateSkillsDirectory(discoveredPackage)),
|
|
461
|
+
...(hasInvalidSkillsPath ? [] : await validateSkillDefinitions(discoveredPackage)),
|
|
462
|
+
...(hasInvalidMcpPath ? [] : await validateMcpConfig(discoveredPackage))
|
|
441
463
|
];
|
|
442
464
|
const staticFailed = staticFindings.some((finding) => finding.severity === "fail");
|
|
443
465
|
const runtimeResult = options.runtime && !staticFailed
|
package/dist/domain/types.d.ts
CHANGED
|
@@ -55,7 +55,41 @@ export interface PluginManifest {
|
|
|
55
55
|
description?: string;
|
|
56
56
|
skills?: string;
|
|
57
57
|
mcpServers?: string;
|
|
58
|
+
author?: unknown;
|
|
59
|
+
homepage?: unknown;
|
|
60
|
+
repository?: unknown;
|
|
61
|
+
license?: unknown;
|
|
62
|
+
keywords?: unknown;
|
|
63
|
+
apps?: unknown;
|
|
64
|
+
hooks?: PluginHooks;
|
|
65
|
+
interface?: unknown;
|
|
66
|
+
}
|
|
67
|
+
export declare const pluginHookEvents: readonly ["PreToolUse", "PermissionRequest", "PostToolUse", "PreCompact", "PostCompact", "UserPromptSubmit", "SubagentStop", "Stop", "SessionStart", "SubagentStart", "SessionEnd"];
|
|
68
|
+
export type PluginHookEvent = (typeof pluginHookEvents)[number];
|
|
69
|
+
export interface PluginHookCommandHandler {
|
|
70
|
+
type: "command";
|
|
71
|
+
command: string;
|
|
72
|
+
commandWindows?: string;
|
|
73
|
+
timeout?: number;
|
|
74
|
+
statusMessage?: string;
|
|
75
|
+
additionalContextLimit?: number;
|
|
76
|
+
async?: boolean;
|
|
77
|
+
}
|
|
78
|
+
export interface PluginHookUnsupportedHandler {
|
|
79
|
+
type: "prompt" | "agent";
|
|
80
|
+
async?: boolean;
|
|
81
|
+
[key: string]: unknown;
|
|
82
|
+
}
|
|
83
|
+
export type PluginHookHandler = PluginHookCommandHandler | PluginHookUnsupportedHandler;
|
|
84
|
+
export interface PluginHookMatcherGroup {
|
|
85
|
+
matcher?: string;
|
|
86
|
+
hooks: PluginHookHandler[];
|
|
87
|
+
}
|
|
88
|
+
export interface PluginHookConfig {
|
|
89
|
+
description?: string;
|
|
90
|
+
hooks: Partial<Record<PluginHookEvent, PluginHookMatcherGroup[]>>;
|
|
58
91
|
}
|
|
92
|
+
export type PluginHooks = string | string[] | PluginHookConfig | PluginHookConfig[];
|
|
59
93
|
export interface DiscoveredPackage {
|
|
60
94
|
rootPath: string;
|
|
61
95
|
manifestPath: string;
|
package/dist/domain/types.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { realpath, stat } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { buildCompatibilityMatrix, readMcpConfigPath } from "../compatibility/compatibility-matrix.js";
|
|
4
|
+
import { normalizeMcpConfig } from "../core/mcp-config-normalizer.js";
|
|
4
5
|
import { readJsonFile } from "../core/read-json-file.js";
|
|
5
6
|
import { probeRuntimeConfig, remoteReliabilityGatePassed } from "../core/runtime-probe.js";
|
|
6
7
|
import { formatFindingFingerprintLine, withFindingFingerprints } from "../reporting/finding-fingerprint.js";
|
|
@@ -15,9 +16,6 @@ function buildFinding(severity, id, message, impact, suggestedFix, evidence) {
|
|
|
15
16
|
...(evidence ? { evidence } : {})
|
|
16
17
|
};
|
|
17
18
|
}
|
|
18
|
-
function isPlainObject(value) {
|
|
19
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
20
|
-
}
|
|
21
19
|
async function fileExists(targetPath) {
|
|
22
20
|
try {
|
|
23
21
|
const details = await stat(targetPath);
|
|
@@ -32,6 +30,13 @@ function isPathWithinRoot(rootPath, candidatePath) {
|
|
|
32
30
|
return (relativePath === "" ||
|
|
33
31
|
(!relativePath.startsWith("..") && !path.isAbsolute(relativePath)));
|
|
34
32
|
}
|
|
33
|
+
function isMcpConfigPathOutsideRootError(error) {
|
|
34
|
+
return error instanceof Error && (error.message === "MCP config path resolves outside the package root." ||
|
|
35
|
+
error.message === "Manifest MCP config path resolves outside the package root.");
|
|
36
|
+
}
|
|
37
|
+
function buildMcpConfigPathOutsideRootFinding(configPath) {
|
|
38
|
+
return 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.", { configPath, field: "configPath" });
|
|
39
|
+
}
|
|
35
40
|
function buildStaticMcpFindings(configPath, parsedConfig) {
|
|
36
41
|
if (!configPath) {
|
|
37
42
|
return {
|
|
@@ -41,29 +46,24 @@ function buildStaticMcpFindings(configPath, parsedConfig) {
|
|
|
41
46
|
]
|
|
42
47
|
};
|
|
43
48
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const servers = parsedConfig.mcpServers;
|
|
53
|
-
if (!isPlainObject(servers) || Object.keys(servers).length === 0) {
|
|
49
|
+
const normalizedConfig = normalizeMcpConfig(parsedConfig);
|
|
50
|
+
if (!normalizedConfig.ok) {
|
|
51
|
+
if (normalizedConfig.field === "server" && normalizedConfig.invalidServerNames) {
|
|
52
|
+
return {
|
|
53
|
+
serverCount: 0,
|
|
54
|
+
findings: normalizedConfig.invalidServerNames.map((serverName) => 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 \`${configPath}\` to an object.`, { configPath, serverName, field: "server" }))
|
|
55
|
+
};
|
|
56
|
+
}
|
|
54
57
|
return {
|
|
55
58
|
serverCount: 0,
|
|
56
59
|
findings: [
|
|
57
|
-
buildFinding("fail", "mcp.config.invalid_shape", "The MCP config must contain a
|
|
60
|
+
buildFinding("fail", "mcp.config.invalid_shape", "The MCP config must contain a valid MCP server map.", "Without server entries, MCP clients cannot discover any package capabilities.", `Use a direct server map, \`mcp_servers\`, or \`mcpServers\` in \`${configPath}\`.`, { configPath, field: normalizedConfig.field })
|
|
58
61
|
]
|
|
59
62
|
};
|
|
60
63
|
}
|
|
64
|
+
const servers = normalizedConfig.servers;
|
|
61
65
|
const findings = [];
|
|
62
66
|
for (const [serverName, serverConfig] of Object.entries(servers)) {
|
|
63
|
-
if (!isPlainObject(serverConfig)) {
|
|
64
|
-
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 \`${configPath}\` to an object.`, { configPath, serverName, field: "server" }));
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
67
|
const command = serverConfig.command;
|
|
68
68
|
const url = serverConfig.url;
|
|
69
69
|
if (typeof command !== "string" && typeof url !== "string") {
|
|
@@ -90,7 +90,18 @@ function mergeReportStatus(findings, security) {
|
|
|
90
90
|
export async function buildGenericMcpDoctor(targetPath, environment = {}, options = {}) {
|
|
91
91
|
const rootPath = path.resolve(targetPath);
|
|
92
92
|
const compatibility = await buildCompatibilityMatrix(rootPath, environment);
|
|
93
|
-
|
|
93
|
+
let mcpConfigPath;
|
|
94
|
+
let mcpConfigPathOutsideRoot = false;
|
|
95
|
+
try {
|
|
96
|
+
mcpConfigPath = await readMcpConfigPath(rootPath);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
if (!isMcpConfigPathOutsideRootError(error)) {
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
mcpConfigPath = null;
|
|
103
|
+
mcpConfigPathOutsideRoot = true;
|
|
104
|
+
}
|
|
94
105
|
const canonicalRootPath = await realpath(rootPath).catch(() => null);
|
|
95
106
|
const canonicalMcpConfigPath = mcpConfigPath
|
|
96
107
|
? await realpath(mcpConfigPath).catch(() => null)
|
|
@@ -98,18 +109,17 @@ export async function buildGenericMcpDoctor(targetPath, environment = {}, option
|
|
|
98
109
|
let parsedConfig = null;
|
|
99
110
|
let staticFindings = [];
|
|
100
111
|
let serverCount = 0;
|
|
101
|
-
if (
|
|
112
|
+
if (mcpConfigPathOutsideRoot) {
|
|
113
|
+
staticFindings = [buildMcpConfigPathOutsideRootFinding(".mcp.json")];
|
|
114
|
+
}
|
|
115
|
+
else if (!mcpConfigPath || !(await fileExists(mcpConfigPath))) {
|
|
102
116
|
staticFindings = buildStaticMcpFindings(null, null).findings;
|
|
103
117
|
}
|
|
104
118
|
else if (!canonicalRootPath ||
|
|
105
119
|
!canonicalMcpConfigPath ||
|
|
106
120
|
!isPathWithinRoot(canonicalRootPath, canonicalMcpConfigPath)) {
|
|
107
121
|
staticFindings = [
|
|
108
|
-
|
|
109
|
-
configPath: path.relative(rootPath, mcpConfigPath).replaceAll("\\", "/"),
|
|
110
|
-
resolvedPath: canonicalMcpConfigPath ?? mcpConfigPath,
|
|
111
|
-
field: "configPath"
|
|
112
|
-
})
|
|
122
|
+
buildMcpConfigPathOutsideRootFinding(path.relative(rootPath, mcpConfigPath).replaceAll("\\", "/"))
|
|
113
123
|
];
|
|
114
124
|
}
|
|
115
125
|
else {
|
|
@@ -128,9 +138,12 @@ export async function buildGenericMcpDoctor(targetPath, environment = {}, option
|
|
|
128
138
|
];
|
|
129
139
|
}
|
|
130
140
|
}
|
|
131
|
-
const security = buildSecurityAuditFromFindings(rootPath,
|
|
132
|
-
|
|
133
|
-
|
|
141
|
+
const security = buildSecurityAuditFromFindings(rootPath, [
|
|
142
|
+
...staticFindings.filter((finding) => finding.id === "mcp.config.path_outside_root"),
|
|
143
|
+
...(mcpConfigPath && parsedConfig !== null
|
|
144
|
+
? auditMcpServerConfig(rootPath, parsedConfig, { configPath: mcpConfigPath })
|
|
145
|
+
: [])
|
|
146
|
+
]);
|
|
134
147
|
const runtimeResult = options.runtime &&
|
|
135
148
|
mcpConfigPath !== null &&
|
|
136
149
|
parsedConfig !== null &&
|
|
@@ -156,7 +169,10 @@ export async function buildGenericMcpDoctor(targetPath, environment = {}, option
|
|
|
156
169
|
exitCode: status === "fail" ? 1 : 0,
|
|
157
170
|
mcpConfigPath,
|
|
158
171
|
serverCount,
|
|
159
|
-
findings: [
|
|
172
|
+
findings: [
|
|
173
|
+
...fingerprintedFindings,
|
|
174
|
+
...security.findings.filter((finding) => !fingerprintedFindings.some((staticFinding) => staticFinding.fingerprint === finding.fingerprint))
|
|
175
|
+
],
|
|
160
176
|
security,
|
|
161
177
|
compatibility,
|
|
162
178
|
...(runtimeResult ? { runtimeScorecard: runtimeResult.scorecard } : {}),
|
|
@@ -53,6 +53,123 @@ export const ruleCatalog = [
|
|
|
53
53
|
fix: "Add a short, specific `description` field to `.codex-plugin/plugin.json`.",
|
|
54
54
|
example: '{ "description": "Validates GitHub PR automation workflows before release." }'
|
|
55
55
|
},
|
|
56
|
+
{
|
|
57
|
+
id: "plugin.manifest.invalid_field",
|
|
58
|
+
category: "package",
|
|
59
|
+
defaultSeverity: "fail",
|
|
60
|
+
summary: "A plugin manifest optional field is invalid.",
|
|
61
|
+
why: "Malformed optional metadata cannot be interpreted reliably by Codex clients.",
|
|
62
|
+
fix: "Use the official type for the affected field in `.codex-plugin/plugin.json`.",
|
|
63
|
+
example: '{ "keywords": ["validation", "mcp"] }'
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "plugin.manifest.invalid_path",
|
|
67
|
+
category: "package",
|
|
68
|
+
defaultSeverity: "fail",
|
|
69
|
+
summary: "A plugin manifest path is not a safe package-relative path.",
|
|
70
|
+
why: "Paths outside the plugin package can expose files that are not part of the plugin bundle.",
|
|
71
|
+
fix: "Use a `./` path that remains inside the plugin package.",
|
|
72
|
+
example: '{ "skills": "./skills" }'
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: "plugin.app.missing_file",
|
|
76
|
+
category: "package",
|
|
77
|
+
defaultSeverity: "fail",
|
|
78
|
+
summary: "The plugin manifest points to a missing .app.json file.",
|
|
79
|
+
why: "Codex cannot load an app manifest that is absent or not a regular file.",
|
|
80
|
+
fix: "Create the referenced app manifest as a regular JSON file inside the plugin package.",
|
|
81
|
+
example: '{ "apps": "./apps/example.app.json" }'
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: "plugin.app.invalid_json",
|
|
85
|
+
category: "package",
|
|
86
|
+
defaultSeverity: "fail",
|
|
87
|
+
summary: "The referenced .app.json file is not valid JSON.",
|
|
88
|
+
why: "Codex cannot parse the app manifest.",
|
|
89
|
+
fix: "Fix the JSON syntax in the referenced app manifest.",
|
|
90
|
+
example: "Use syntactically valid JSON in the referenced .app.json file."
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: "plugin.app.invalid_path",
|
|
94
|
+
category: "package",
|
|
95
|
+
defaultSeverity: "fail",
|
|
96
|
+
summary: "A plugin app path is not a safe package-relative path.",
|
|
97
|
+
why: "Paths outside the plugin package can expose files that are not part of the plugin bundle.",
|
|
98
|
+
fix: "Use an `./` app path that remains inside the plugin package.",
|
|
99
|
+
example: '{ "apps": "./apps/example.app.json" }'
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: "plugin.hook.missing_file",
|
|
103
|
+
category: "package",
|
|
104
|
+
defaultSeverity: "fail",
|
|
105
|
+
summary: "The plugin lifecycle hook source file is missing.",
|
|
106
|
+
why: "Codex cannot load a hook configuration file that is absent or not a regular file.",
|
|
107
|
+
fix: "Create the referenced hook configuration JSON file inside the plugin package.",
|
|
108
|
+
example: '{ "hooks": "./hooks/hooks.json" }'
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: "plugin.hook.invalid_json",
|
|
112
|
+
category: "package",
|
|
113
|
+
defaultSeverity: "fail",
|
|
114
|
+
summary: "The plugin lifecycle hook source is not valid JSON.",
|
|
115
|
+
why: "Codex cannot parse the hook configuration.",
|
|
116
|
+
fix: "Fix the JSON syntax in the hook configuration file.",
|
|
117
|
+
example: "Use syntactically valid JSON in the hook configuration file."
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: "plugin.hook.invalid_shape",
|
|
121
|
+
category: "package",
|
|
122
|
+
defaultSeverity: "fail",
|
|
123
|
+
summary: "The plugin lifecycle hook configuration has an invalid shape.",
|
|
124
|
+
why: "Codex cannot safely interpret malformed lifecycle hook metadata.",
|
|
125
|
+
fix: "Use the official hook configuration schema for the affected field.",
|
|
126
|
+
example: '{ "hooks": { "PreToolUse": [{ "hooks": [{ "type": "command", "command": "node hooks/check.js" }] }] } }'
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
id: "plugin.hook.invalid_path",
|
|
130
|
+
category: "package",
|
|
131
|
+
defaultSeverity: "fail",
|
|
132
|
+
summary: "The plugin lifecycle hook source is not a safe package-relative path.",
|
|
133
|
+
why: "Paths outside the plugin package can expose unreviewed files to hook configuration loading.",
|
|
134
|
+
fix: "Use a `./` hook configuration path that stays within the plugin package.",
|
|
135
|
+
example: '{ "hooks": "./hooks/hooks.json" }'
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: "plugin.hook.unsupported_event",
|
|
139
|
+
category: "package",
|
|
140
|
+
defaultSeverity: "fail",
|
|
141
|
+
summary: "A plugin lifecycle hook event is not supported.",
|
|
142
|
+
why: "Codex will not invoke hook events outside the official lifecycle event set.",
|
|
143
|
+
fix: "Use one of the official plugin lifecycle hook events.",
|
|
144
|
+
example: '{ "hooks": { "SessionStart": [] } }'
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: "plugin.hook.unsupported_handler",
|
|
148
|
+
category: "package",
|
|
149
|
+
defaultSeverity: "warn",
|
|
150
|
+
summary: "A plugin lifecycle hook uses a handler type the host skips.",
|
|
151
|
+
why: "Prompt and agent lifecycle handlers are not executed by this host.",
|
|
152
|
+
fix: "Use a command handler for behavior that must run in this host.",
|
|
153
|
+
example: '{ "type": "command", "command": "node hooks/check.js" }'
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
id: "plugin.hook.async_unsupported",
|
|
157
|
+
category: "package",
|
|
158
|
+
defaultSeverity: "warn",
|
|
159
|
+
summary: "A plugin lifecycle hook requests unsupported asynchronous execution.",
|
|
160
|
+
why: "The host does not support asynchronous lifecycle hook execution.",
|
|
161
|
+
fix: "Remove `async: true` or run the command synchronously.",
|
|
162
|
+
example: '{ "type": "command", "command": "node hooks/check.js" }'
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
id: "plugin.hook.matcher_ignored",
|
|
166
|
+
category: "package",
|
|
167
|
+
defaultSeverity: "warn",
|
|
168
|
+
summary: "A plugin lifecycle hook matcher is ignored for this event.",
|
|
169
|
+
why: "This lifecycle event does not use matcher filtering.",
|
|
170
|
+
fix: "Remove the matcher from this event group.",
|
|
171
|
+
example: '{ "hooks": { "Stop": [{ "hooks": [] }] } }'
|
|
172
|
+
},
|
|
56
173
|
{
|
|
57
174
|
id: "plugin.heuristic.description.too_long",
|
|
58
175
|
category: "package",
|
|
@@ -138,10 +255,19 @@ export const ruleCatalog = [
|
|
|
138
255
|
id: "plugin.mcp.invalid_shape",
|
|
139
256
|
category: "mcp",
|
|
140
257
|
defaultSeverity: "fail",
|
|
141
|
-
summary: "The `.mcp.json` file does not
|
|
142
|
-
why: "Codex
|
|
143
|
-
fix: "
|
|
144
|
-
example: '{ "
|
|
258
|
+
summary: "The `.mcp.json` file does not contain one non-empty supported server map.",
|
|
259
|
+
why: "Codex accepts exactly a direct server map, an `mcp_servers` wrapper, or the legacy `mcpServers` wrapper.",
|
|
260
|
+
fix: "Use exactly one non-empty direct server map, `mcp_servers`, or legacy `mcpServers` wrapper.",
|
|
261
|
+
example: '{ "doctor": { "command": "node", "args": ["server.js"] } }'
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
id: "plugin.mcp.ambiguous_shape",
|
|
265
|
+
category: "mcp",
|
|
266
|
+
defaultSeverity: "fail",
|
|
267
|
+
summary: "The `.mcp.json` file uses an ambiguous MCP config layout.",
|
|
268
|
+
why: "Codex cannot safely choose between multiple wrapper layouts when they appear in one configuration file.",
|
|
269
|
+
fix: "Use exactly one supported layout: a direct server map, `mcp_servers`, or `mcpServers`.",
|
|
270
|
+
example: '{ "weather": { "command": "node", "args": ["server.js"] } }'
|
|
145
271
|
},
|
|
146
272
|
{
|
|
147
273
|
id: "plugin.mcp.server.invalid",
|
package/dist/run-cli.js
CHANGED
|
@@ -169,7 +169,7 @@ function parseRemoteNetworkFlags(flags, runtime) {
|
|
|
169
169
|
return { allowNetwork, allowLocalNetwork, allowSessionLifecycle, requireRemoteReliability };
|
|
170
170
|
}
|
|
171
171
|
function printUsage(io) {
|
|
172
|
-
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 [--allow-network [--allow-local-network]]] [--sandbox docker] [--require-runtime-approval --runtime-approval-digest <digest>] [--verbose-runtime] [--explain] [--no-animations] [--ascii] [--changed-since <ref>]\n codex-plugin-doctor audit --installed [filter] [--policy codex-publish|mcp-strict|security] [--security] [--compat] [--json] [--output <path>] [--cache] [--changed]\n codex-plugin-doctor audit deps <path> [--policy codex-publish|mcp-strict|security] [--recommend] [--json|--sarif] [--output <path>]\n codex-plugin-doctor mcp <path> [--runtime [--allow-network [--allow-local-network]]] [--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 suppress add <path> [--fingerprint <sha256> --reason <text> --expires-at YYYY-MM-DD] [--config <path>] [--json]\n codex-plugin-doctor suppress list <path> [--config <path>] [--json]\n codex-plugin-doctor suppress remove <path> [--fingerprint <sha256>|--index <n>] [--config <path>] [--json]\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 watch <path> [--runtime] [--json] [--output <path>] [--debounce-ms <ms>] [--max-iterations <n>] [--fail-fast] [--accumulate-json <path>]\n codex-plugin-doctor doctor [npm <package>|contract|corpus [--manifest <corpus.json>] [--json] [--output <path>]|corpus metrics --manifest <corpus.json> [--json|--markdown] [--output <path>] [--min-precision <0..1>] [--min-recall <0..1>] [--max-false-positive-rate <0..1>]|runtime-plan <path> [--sandbox docker] [--json|--markdown] [--output <path>]|runtime-policy <path> [--sandbox docker] [--json] [--output <path>]|review-bundle <path> --output <dir> --sign-key-env NAME [--json] [--allow-dirty] [--allow-untagged]|review-bundle verify <bundle-dir> --target <path> --sign-key-env NAME [--json] [--output <path>] [--failures-only]|review-bundle diff --before <dir> --after <dir> [--json]|attest <path> [--sign-key-env NAME]|attest verify <attestation.json> --target <path> --sign-key-env NAME|release-evidence <path> --sign-key-env NAME [--runtime [--allow-network [--allow-local-network]]] [--sandbox docker] [--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 [--runtime [--allow-network [--allow-local-network]]] [--sandbox docker] [--allow-dirty] [--allow-untagged] [--require-runtime-approval --runtime-approval-digest <digest>] [--upload]|mcp <path> [--runtime [--allow-network [--allow-local-network]]]|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 init-git-hooks [path] [--force] [--json]\n codex-plugin-doctor init-git-hooks [path] --remove [--json]\n codex-plugin-doctor completion bash|zsh|fish\n codex-plugin-doctor config validate <path> [--json]\n codex-plugin-doctor release check <path> [--json] [--runtime [--allow-network [--allow-local-network]]] [--sandbox docker]\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");
|
|
172
|
+
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 [--allow-network [--allow-local-network]]] [--sandbox docker] [--require-runtime-approval --runtime-approval-digest <digest>] [--verbose-runtime] [--explain] [--no-animations] [--ascii] [--changed-since <ref>] [--fail-on <rule-id>]\n codex-plugin-doctor audit --installed [filter] [--policy codex-publish|mcp-strict|security] [--security] [--compat] [--json] [--output <path>] [--cache] [--changed]\n codex-plugin-doctor audit deps <path> [--policy codex-publish|mcp-strict|security] [--recommend] [--json|--sarif] [--output <path>]\n codex-plugin-doctor mcp <path> [--runtime [--allow-network [--allow-local-network]]] [--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 suppress add <path> [--fingerprint <sha256> --reason <text> --expires-at YYYY-MM-DD] [--config <path>] [--json]\n codex-plugin-doctor suppress list <path> [--config <path>] [--json]\n codex-plugin-doctor suppress remove <path> [--fingerprint <sha256>|--index <n>] [--config <path>] [--json]\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 watch <path> [--runtime] [--json] [--output <path>] [--debounce-ms <ms>] [--max-iterations <n>] [--fail-fast] [--accumulate-json <path>]\n codex-plugin-doctor doctor [npm <package>|contract|corpus [--manifest <corpus.json>] [--json] [--output <path>]|corpus metrics --manifest <corpus.json> [--json|--markdown] [--output <path>] [--min-precision <0..1>] [--min-recall <0..1>] [--max-false-positive-rate <0..1>]|runtime-plan <path> [--sandbox docker] [--json|--markdown] [--output <path>]|runtime-policy <path> [--sandbox docker] [--json] [--output <path>]|review-bundle <path> --output <dir> --sign-key-env NAME [--json] [--allow-dirty] [--allow-untagged]|review-bundle verify <bundle-dir> --target <path> --sign-key-env NAME [--json] [--output <path>] [--failures-only]|review-bundle diff --before <dir> --after <dir> [--json]|attest <path> [--sign-key-env NAME]|attest verify <attestation.json> --target <path> --sign-key-env NAME|release-evidence <path> --sign-key-env NAME [--runtime [--allow-network [--allow-local-network]]] [--sandbox docker] [--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 [--runtime [--allow-network [--allow-local-network]]] [--sandbox docker] [--allow-dirty] [--allow-untagged] [--require-runtime-approval --runtime-approval-digest <digest>] [--upload]|mcp <path> [--runtime [--allow-network [--allow-local-network]]]|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 init-git-hooks [path] [--force] [--json]\n codex-plugin-doctor init-git-hooks [path] --remove [--json]\n codex-plugin-doctor completion bash|zsh|fish\n codex-plugin-doctor config validate <path> [--json]\n codex-plugin-doctor release check <path> [--json] [--runtime [--allow-network [--allow-local-network]]] [--sandbox docker]\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");
|
|
173
173
|
io.writeStderr("Registry readiness: codex-plugin-doctor registry check <server.json|directory> [--json] [--output <path>] [--require-registry-readiness]\n"
|
|
174
174
|
+ " codex-plugin-doctor registry inspect <server-name> --allow-network [--json] [--output <path>] [--require-registry-readiness]\n"
|
|
175
175
|
+ "Registry publication preflight: codex-plugin-doctor registry preflight <server.json|directory> [--allow-network] [--json] [--output <path>] [--require-publish-ready]");
|
|
@@ -2108,7 +2108,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
2108
2108
|
? remainingArgs
|
|
2109
2109
|
: remainingArgs.slice(1);
|
|
2110
2110
|
const jsonOutput = sizeFlags.includes("--json");
|
|
2111
|
-
const
|
|
2111
|
+
const npmPack = sizeFlags.includes("--npm");
|
|
2112
|
+
const report = await buildDoctorSize(sizeTarget, { npmPack });
|
|
2112
2113
|
const output = jsonOutput
|
|
2113
2114
|
? renderDoctorSizeJson(report)
|
|
2114
2115
|
: renderDoctorSize(report);
|
|
@@ -2905,6 +2906,13 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
2905
2906
|
: normalizedFlags[runtimeApprovalDigestIndex + 1];
|
|
2906
2907
|
const changedSinceIndex = normalizedFlags.indexOf("--changed-since");
|
|
2907
2908
|
const changedSinceRef = changedSinceIndex === -1 ? null : normalizedFlags[changedSinceIndex + 1];
|
|
2909
|
+
const failOnRules = [];
|
|
2910
|
+
for (let i = 0; i < normalizedFlags.length; i += 1) {
|
|
2911
|
+
if (normalizedFlags[i] === "--fail-on" && normalizedFlags[i + 1] && !normalizedFlags[i + 1].startsWith("--")) {
|
|
2912
|
+
failOnRules.push(normalizedFlags[i + 1]);
|
|
2913
|
+
i += 1;
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2908
2916
|
const baselineIndex = normalizedFlags.indexOf("--baseline");
|
|
2909
2917
|
const baselinePath = baselineIndex === -1 ? null : normalizedFlags[baselineIndex + 1];
|
|
2910
2918
|
if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
|
|
@@ -3150,6 +3158,28 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
3150
3158
|
failOnWarnings: doctorConfig.failOnWarnings
|
|
3151
3159
|
})
|
|
3152
3160
|
: configuredResult;
|
|
3161
|
+
if (failOnRules.length > 0) {
|
|
3162
|
+
result.findings = result.findings.map((finding) => {
|
|
3163
|
+
if (finding.severity === "warn" && failOnRules.includes(finding.id)) {
|
|
3164
|
+
return { ...finding, severity: "fail" };
|
|
3165
|
+
}
|
|
3166
|
+
return finding;
|
|
3167
|
+
});
|
|
3168
|
+
const hasFail = result.findings.some((f) => f.severity === "fail");
|
|
3169
|
+
const hasWarn = result.findings.some((f) => f.severity === "warn");
|
|
3170
|
+
if (hasFail) {
|
|
3171
|
+
result.status = "fail";
|
|
3172
|
+
result.exitCode = 1;
|
|
3173
|
+
}
|
|
3174
|
+
else if (hasWarn) {
|
|
3175
|
+
result.status = "warn";
|
|
3176
|
+
result.exitCode = 0;
|
|
3177
|
+
}
|
|
3178
|
+
else {
|
|
3179
|
+
result.status = "pass";
|
|
3180
|
+
result.exitCode = 0;
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3153
3183
|
if (renderer) {
|
|
3154
3184
|
if (result.status === "fail") {
|
|
3155
3185
|
renderer.stopFailure("Validation failed");
|
|
@@ -10,6 +10,7 @@ export interface SecurityAudit {
|
|
|
10
10
|
};
|
|
11
11
|
findings: Finding[];
|
|
12
12
|
}
|
|
13
|
+
export declare function auditHookCommand(rootPath: string, sourcePath: string, event: string, field: string, command: string): Finding[];
|
|
13
14
|
export declare function auditMcpServerConfig(rootPath: string, parsedConfig: unknown, options?: {
|
|
14
15
|
configPath?: string;
|
|
15
16
|
}): Finding[];
|