codex-plugin-doctor 1.59.0 → 1.60.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -19
- package/dist/core/output-contract.js +62 -0
- package/dist/core/shell-completion.js +23 -0
- package/dist/core/submission-archive-preflight.d.ts +24 -0
- package/dist/core/submission-archive-preflight.js +259 -0
- package/dist/core/submission-archive-reader.d.ts +21 -0
- package/dist/core/submission-archive-reader.js +624 -0
- package/dist/core/submission-archive-ruleset.d.ts +58 -0
- package/dist/core/submission-archive-ruleset.js +61 -0
- package/dist/core/submission-assets.d.ts +3 -1
- package/dist/core/submission-assets.js +52 -31
- package/dist/core/submission-package-reader.d.ts +15 -0
- package/dist/core/submission-package-reader.js +274 -0
- package/dist/core/submission-preflight.d.ts +2 -0
- package/dist/core/submission-preflight.js +2 -2
- package/dist/core/submission-skill-metadata.d.ts +5 -3
- package/dist/core/submission-skill-metadata.js +117 -136
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/reporting/render-submission-archive-report.d.ts +4 -0
- package/dist/reporting/render-submission-archive-report.js +52 -0
- package/dist/run-cli.js +107 -1
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -131,21 +131,27 @@ codex-plugin-doctor registry preflight path/to/server.json --allow-network --req
|
|
|
131
131
|
Preflight checks the exact npm package version, then the exact Registry version
|
|
132
132
|
and latest Registry record on fixed public hosts. An existing exact Registry
|
|
133
133
|
version is an immutable collision and must be replaced by a version bump. The
|
|
134
|
-
command is advisory: it never authenticates, publishes, or changes npm or
|
|
135
|
-
Registry records. See [MCP Registry Publication Preflight](./docs/architecture/mcp-registry-publication-preflight.md).
|
|
136
|
-
|
|
137
|
-
### Public Directory Submission Preflight
|
|
138
|
-
|
|
139
|
-
Check a plugin package for deterministic public-directory submission issues without contacting a portal:
|
|
140
|
-
|
|
141
|
-
```bash
|
|
142
|
-
codex-plugin-doctor doctor submission <path>
|
|
143
|
-
codex-plugin-doctor doctor submission <path> --json
|
|
144
|
-
codex-plugin-doctor doctor submission <path> --markdown
|
|
145
|
-
codex-plugin-doctor doctor submission <path> --require-ready
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
134
|
+
command is advisory: it never authenticates, publishes, or changes npm or
|
|
135
|
+
Registry records. See [MCP Registry Publication Preflight](./docs/architecture/mcp-registry-publication-preflight.md).
|
|
136
|
+
|
|
137
|
+
### Public Directory Submission Preflight
|
|
138
|
+
|
|
139
|
+
Check a plugin package for deterministic public-directory submission issues without contacting a portal:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
codex-plugin-doctor doctor submission <path>
|
|
143
|
+
codex-plugin-doctor doctor submission <path> --json
|
|
144
|
+
codex-plugin-doctor doctor submission <path> --markdown
|
|
145
|
+
codex-plugin-doctor doctor submission <path> --require-ready
|
|
146
|
+
codex-plugin-doctor doctor submission archive <zip>
|
|
147
|
+
codex-plugin-doctor doctor submission archive <zip> --json --output submission-archive.json
|
|
148
|
+
codex-plugin-doctor doctor submission archive <zip> --markdown --output submission-archive.md
|
|
149
|
+
codex-plugin-doctor doctor submission archive <zip> --require-ready
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The preflight is offline and non-executing: it does not submit a package, make network requests, start MCP servers, verify domains, or handle OAuth credentials. Its automatic `status` is `pass` or `fail`; a passing automatic result remains `manual_review_required` until portal-only review is complete. It never claims directory acceptance. See [Public Directory Submission Preflight](./docs/architecture/public-directory-submission-preflight.md).
|
|
153
|
+
|
|
154
|
+
The archive variant accepts an existing skills-only ZIP and does not extract archive entries to disk, execute package code, or repair the archive. It reports malformed ZIP structures and deterministic package blockers; unsupported portal rules remain manual or `coverage: unavailable` rather than an automatic pass.
|
|
149
155
|
|
|
150
156
|
Output formats:
|
|
151
157
|
|
|
@@ -493,9 +499,9 @@ jobs:
|
|
|
493
499
|
runs-on: ubuntu-latest
|
|
494
500
|
steps:
|
|
495
501
|
- uses: actions/checkout@v5
|
|
496
|
-
- uses: Esquetta/CodexPluginDoctor@v1.
|
|
497
|
-
with:
|
|
498
|
-
version: "1.
|
|
502
|
+
- uses: Esquetta/CodexPluginDoctor@v1.60.0
|
|
503
|
+
with:
|
|
504
|
+
version: "1.60.0"
|
|
499
505
|
path: .
|
|
500
506
|
runtime: "true"
|
|
501
507
|
policy: codex-publish
|
|
@@ -507,7 +513,7 @@ jobs:
|
|
|
507
513
|
review-bundle-verify: "true"
|
|
508
514
|
```
|
|
509
515
|
|
|
510
|
-
The action writes `codex-plugin-doctor-summary.md`, `codex-plugin-doctor-report.json`, `codex-plugin-doctor-action-manifest.json`, optional `codex-plugin-doctor.sarif`, optional validation corpus and quality metrics reports, optional `output-contract.json`, and optional signed `review-bundle/` files to `codex-plugin-doctor-reports`, appends the Markdown report to the GitHub Actions step summary, uploads the report directory as an artifact, and then returns the real validation exit code. Set `submission: "true"` to add offline submission preflight reports; set `require-submission-ready: "true"` only with that opt-in to make automatic blockers fail the job. Review bundle generation requires a signing key environment variable such as `CODEX_PLUGIN_DOCTOR_SIGNING_KEY`. For runtime probing, SARIF output, corpus quality regression gates, corpus and contract artifacts, review bundle artifacts, installed plugin cache checks, CI policy presets, and pinned release examples, see [GitHub Action Usage](./docs/guides/github-action.md).
|
|
516
|
+
The action writes `codex-plugin-doctor-summary.md`, `codex-plugin-doctor-report.json`, `codex-plugin-doctor-action-manifest.json`, optional `codex-plugin-doctor.sarif`, optional validation corpus and quality metrics reports, optional `output-contract.json`, and optional signed `review-bundle/` files to `codex-plugin-doctor-reports`, appends the Markdown report to the GitHub Actions step summary, uploads the report directory as an artifact, and then returns the real validation exit code. Set `submission: "true"` to add offline submission preflight reports; set `require-submission-ready: "true"` only with that opt-in to make automatic blockers fail the job. Review bundle generation requires a signing key environment variable such as `CODEX_PLUGIN_DOCTOR_SIGNING_KEY`. For runtime probing, SARIF output, corpus quality regression gates, corpus and contract artifacts, review bundle artifacts, installed plugin cache checks, CI policy presets, and pinned release examples, see [GitHub Action Usage](./docs/guides/github-action.md).
|
|
511
517
|
|
|
512
518
|
To self-test this repository after cloning it:
|
|
513
519
|
|
|
@@ -262,6 +262,68 @@ const publicSchemaDefinitions = [
|
|
|
262
262
|
}
|
|
263
263
|
}
|
|
264
264
|
},
|
|
265
|
+
{
|
|
266
|
+
id: "doctor.submission.archive.json",
|
|
267
|
+
command: "codex-plugin-doctor doctor submission archive <zip> --json",
|
|
268
|
+
required: [
|
|
269
|
+
"schemaVersion",
|
|
270
|
+
"rulesetVersion",
|
|
271
|
+
"status",
|
|
272
|
+
"readiness",
|
|
273
|
+
"archive",
|
|
274
|
+
"summary",
|
|
275
|
+
"archiveChecks",
|
|
276
|
+
"submission",
|
|
277
|
+
"findings",
|
|
278
|
+
"coverage",
|
|
279
|
+
"manualChecklist"
|
|
280
|
+
],
|
|
281
|
+
properties: {
|
|
282
|
+
rulesetVersion: { const: "openai-directory-archive-2026-08-23" },
|
|
283
|
+
status: { type: "string", enum: ["pass", "fail"] },
|
|
284
|
+
readiness: { type: "string", enum: ["blocked", "manual_review_required"] },
|
|
285
|
+
archive: {
|
|
286
|
+
type: "object",
|
|
287
|
+
required: ["fileName", "compressedBytes", "uncompressedBytes", "entryCount", "rootLayout"],
|
|
288
|
+
properties: {
|
|
289
|
+
fileName: { type: "string" },
|
|
290
|
+
compressedBytes: { type: "integer", minimum: 0 },
|
|
291
|
+
uncompressedBytes: { type: "integer", minimum: 0 },
|
|
292
|
+
entryCount: { type: "integer", minimum: 0 },
|
|
293
|
+
rootLayout: { type: "string", enum: ["archive-root", "single-top-level-directory", "unavailable"] }
|
|
294
|
+
},
|
|
295
|
+
additionalProperties: false
|
|
296
|
+
},
|
|
297
|
+
summary: {
|
|
298
|
+
type: "object",
|
|
299
|
+
required: ["passed", "warnings", "blockers", "manualChecks"],
|
|
300
|
+
properties: {
|
|
301
|
+
passed: { type: "integer", minimum: 0 },
|
|
302
|
+
warnings: { type: "integer", minimum: 0 },
|
|
303
|
+
blockers: { type: "integer", minimum: 0 },
|
|
304
|
+
manualChecks: { type: "integer", minimum: 0 }
|
|
305
|
+
},
|
|
306
|
+
additionalProperties: false
|
|
307
|
+
},
|
|
308
|
+
archiveChecks: { type: "array" },
|
|
309
|
+
submission: { type: ["object", "null"] },
|
|
310
|
+
findings: { type: "array" },
|
|
311
|
+
coverage: {
|
|
312
|
+
type: "array",
|
|
313
|
+
items: {
|
|
314
|
+
type: "object",
|
|
315
|
+
required: ["id", "status", "reason"],
|
|
316
|
+
properties: {
|
|
317
|
+
id: { type: "string", pattern: "^plugin\\.submission\\.archive\\." },
|
|
318
|
+
status: { type: "string", enum: ["automatic", "manual", "unavailable"] },
|
|
319
|
+
reason: { type: "string" }
|
|
320
|
+
},
|
|
321
|
+
additionalProperties: false
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
manualChecklist: { type: "array" }
|
|
325
|
+
}
|
|
326
|
+
},
|
|
265
327
|
{
|
|
266
328
|
id: "doctor.installed.check.json",
|
|
267
329
|
command: "codex-plugin-doctor check --installed --json",
|
|
@@ -18,6 +18,7 @@ const topLevelCommands = [
|
|
|
18
18
|
"config"
|
|
19
19
|
];
|
|
20
20
|
const doctorCommands = ["submission"];
|
|
21
|
+
const submissionTargets = ["archive"];
|
|
21
22
|
const submissionFlags = ["--json", "--markdown", "--output", "--require-ready"];
|
|
22
23
|
const fishSubmissionCondition = "__fish_seen_subcommand_from doctor; and __fish_seen_subcommand_from submission";
|
|
23
24
|
function bashCompletion() {
|
|
@@ -30,6 +31,7 @@ function bashCompletion() {
|
|
|
30
31
|
"",
|
|
31
32
|
` local commands="${topLevelCommands.join(" ")}"`,
|
|
32
33
|
` local doctor_commands="${doctorCommands.join(" ")}"`,
|
|
34
|
+
` local submission_targets="${submissionTargets.join(" ")}"`,
|
|
33
35
|
` local submission_flags="${submissionFlags.join(" ")}"`,
|
|
34
36
|
"",
|
|
35
37
|
" case \"${prev}\" in",
|
|
@@ -41,8 +43,25 @@ function bashCompletion() {
|
|
|
41
43
|
" COMPREPLY=( $(compgen -W \"${doctor_commands}\" -- \"${cur}\") )",
|
|
42
44
|
" return 0",
|
|
43
45
|
" ;;",
|
|
46
|
+
" submission)",
|
|
47
|
+
" COMPREPLY=( $(compgen -W \"${submission_targets}\" -- \"${cur}\") )",
|
|
48
|
+
" return 0",
|
|
49
|
+
" ;;",
|
|
44
50
|
" esac",
|
|
45
51
|
"",
|
|
52
|
+
" if [[ ${COMP_WORDS[1]} == \"doctor\" && ${COMP_WORDS[2]} == \"submission\" && ${COMP_WORDS[3]} == \"archive\" ]]; then",
|
|
53
|
+
" case \"${cur}\" in",
|
|
54
|
+
" --*)",
|
|
55
|
+
" COMPREPLY=( $(compgen -W \"${submission_flags}\" -- \"${cur}\") )",
|
|
56
|
+
" return 0",
|
|
57
|
+
" ;;",
|
|
58
|
+
" *)",
|
|
59
|
+
" COMPREPLY=( $(compgen -f -- \"${cur}\") )",
|
|
60
|
+
" return 0",
|
|
61
|
+
" ;;",
|
|
62
|
+
" esac",
|
|
63
|
+
" fi",
|
|
64
|
+
"",
|
|
46
65
|
" case \"${cur}\" in",
|
|
47
66
|
" --*)",
|
|
48
67
|
" if [[ ${COMP_WORDS[1]} == \"doctor\" && ${COMP_WORDS[2]} == \"submission\" ]]; then",
|
|
@@ -77,6 +96,8 @@ function zshCompletion() {
|
|
|
77
96
|
"",
|
|
78
97
|
" if [[ \"$words[2]\" == \"doctor\" && \"$words[3]\" == \"submission\" ]]; then",
|
|
79
98
|
" _arguments -C \\",
|
|
99
|
+
" '3:archive target:(archive)' \\",
|
|
100
|
+
" '4:ZIP archive:_files' \\",
|
|
80
101
|
" '*--json[Output as JSON]' \\",
|
|
81
102
|
" '*--markdown[Output as Markdown]' \\",
|
|
82
103
|
" '*--output[Write to file]:file:_files' \\",
|
|
@@ -108,6 +129,8 @@ function fishCompletion() {
|
|
|
108
129
|
`complete -c codex-plugin-doctor -n "${fishSubmissionCondition}" -l markdown -d 'Output as Markdown'`,
|
|
109
130
|
`complete -c codex-plugin-doctor -n "${fishSubmissionCondition}" -l output -d 'Write to file' -r`,
|
|
110
131
|
`complete -c codex-plugin-doctor -n "${fishSubmissionCondition}" -l require-ready -d 'Fail when automatic checks are blocked'`,
|
|
132
|
+
`complete -c codex-plugin-doctor -n "${fishSubmissionCondition}; and __fish_seen_subcommand_from archive" -F`,
|
|
133
|
+
`complete -c codex-plugin-doctor -n "${fishSubmissionCondition}; and not __fish_seen_subcommand_from archive" -a "${submissionTargets.join(" ")}" -d 'ZIP archive'`,
|
|
111
134
|
`complete -c codex-plugin-doctor -n "__fish_seen_subcommand_from doctor; and not __fish_seen_subcommand_from ${doctorCommands.join(" ")}" -a "${doctorCommands.join(" ")}"`,
|
|
112
135
|
"complete -c codex-plugin-doctor -l runtime -d 'Enable runtime probes'",
|
|
113
136
|
"complete -c codex-plugin-doctor -l policy -d 'Apply policy' -x -a 'codex-publish mcp-strict security'",
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type SubmissionArchiveCoverage, type SubmissionArchiveFinding } from "./submission-archive-reader.js";
|
|
2
|
+
import { type SubmissionCheck, type SubmissionFinding, type SubmissionManualCheck, type SubmissionPreflightReport } from "./submission-preflight.js";
|
|
3
|
+
export type SubmissionArchiveRootLayout = "archive-root" | "single-top-level-directory" | "unavailable";
|
|
4
|
+
export interface SubmissionArchivePreflightReport {
|
|
5
|
+
schemaVersion: "1.0.0";
|
|
6
|
+
rulesetVersion: "openai-directory-archive-2026-08-23";
|
|
7
|
+
status: "pass" | "fail";
|
|
8
|
+
readiness: "blocked" | "manual_review_required";
|
|
9
|
+
archive: {
|
|
10
|
+
fileName: string;
|
|
11
|
+
compressedBytes: number;
|
|
12
|
+
uncompressedBytes: number;
|
|
13
|
+
entryCount: number;
|
|
14
|
+
rootLayout: SubmissionArchiveRootLayout;
|
|
15
|
+
};
|
|
16
|
+
summary: SubmissionPreflightReport["summary"];
|
|
17
|
+
archiveChecks: readonly SubmissionCheck[];
|
|
18
|
+
submission: SubmissionPreflightReport | null;
|
|
19
|
+
findings: readonly SubmissionArchiveFinding[] | readonly (SubmissionArchiveFinding | SubmissionFinding)[];
|
|
20
|
+
coverage: readonly SubmissionArchiveCoverage[];
|
|
21
|
+
manualChecklist: readonly SubmissionManualCheck[];
|
|
22
|
+
}
|
|
23
|
+
export declare function submissionArchiveExitCode(report: SubmissionArchivePreflightReport, requireReady: boolean): 0 | 1;
|
|
24
|
+
export declare function buildSubmissionArchivePreflight(archivePath: string): Promise<SubmissionArchivePreflightReport>;
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { validateSubmissionAssetsFromReader } from "./submission-assets.js";
|
|
2
|
+
import { inspectSubmissionArchive } from "./submission-archive-reader.js";
|
|
3
|
+
import { submissionArchiveRuleset } from "./submission-archive-ruleset.js";
|
|
4
|
+
import { validateSubmissionListing } from "./submission-preflight.js";
|
|
5
|
+
import { submissionManualChecks } from "./submission-ruleset.js";
|
|
6
|
+
import { validateSubmissionSkillMetadataFromReader } from "./submission-skill-metadata.js";
|
|
7
|
+
const manifestPaths = [".codex-plugin/plugin.json", ".agent-plugin/plugin.json", ".claude-plugin/plugin.json"];
|
|
8
|
+
const maxManifestBytes = 1024 * 1024;
|
|
9
|
+
function archiveFinding(id, severity, message, portalCode) {
|
|
10
|
+
return portalCode === undefined ? { id, severity, message } : { id, severity, message, portalCode };
|
|
11
|
+
}
|
|
12
|
+
function archiveCoverage() {
|
|
13
|
+
return submissionArchiveRuleset.portalRules.map(({ id, status, reason }) => ({ id, status, reason }));
|
|
14
|
+
}
|
|
15
|
+
function checkStatus(findings) {
|
|
16
|
+
return findings.some((finding) => finding.severity === "fail")
|
|
17
|
+
? "fail"
|
|
18
|
+
: findings.some((finding) => finding.severity === "warn") ? "warn" : "pass";
|
|
19
|
+
}
|
|
20
|
+
function submissionCheck(id, findings) {
|
|
21
|
+
return { id, status: checkStatus(findings), findingIds: findings.map((finding) => finding.id) };
|
|
22
|
+
}
|
|
23
|
+
function manualChecklist() {
|
|
24
|
+
return submissionManualChecks.map((item) => ({
|
|
25
|
+
id: item.id,
|
|
26
|
+
label: item.label,
|
|
27
|
+
state: item.mcpOnly ? "not_applicable" : "required"
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
function reportSummary(checks, findings, checklist) {
|
|
31
|
+
return {
|
|
32
|
+
passed: checks.filter((check) => check.status === "pass").length,
|
|
33
|
+
warnings: findings.filter((finding) => finding.severity === "warn").length,
|
|
34
|
+
blockers: findings.filter((finding) => finding.severity === "fail").length,
|
|
35
|
+
manualChecks: checklist.filter((item) => item.state === "required").length
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function normalizePath(value, allowRoot = false) {
|
|
39
|
+
if (value === "" && allowRoot)
|
|
40
|
+
return "";
|
|
41
|
+
if (typeof value !== "string" || value === "" || value.startsWith("/") || value.startsWith("\\")
|
|
42
|
+
|| /^[A-Za-z]:/u.test(value) || value.includes("\\") || /[\u0000-\u001F\u007F]/u.test(value))
|
|
43
|
+
return null;
|
|
44
|
+
const segments = value.split("/");
|
|
45
|
+
return segments.some((segment) => segment === "" || segment === "." || segment === "..") ? null : segments.join("/");
|
|
46
|
+
}
|
|
47
|
+
function discoverRoot(entries) {
|
|
48
|
+
const paths = entries.map((entry) => entry.packagePath);
|
|
49
|
+
if (paths.length === 0) {
|
|
50
|
+
return { discovery: null, findings: [archiveFinding("plugin.submission.archive.root_missing", "fail", "Archive does not contain a plugin root.")] };
|
|
51
|
+
}
|
|
52
|
+
const rootManifests = manifestPaths.filter((manifestPath) => paths.includes(manifestPath));
|
|
53
|
+
const nestedRoots = new Set();
|
|
54
|
+
for (const entryPath of paths) {
|
|
55
|
+
const separator = entryPath.indexOf("/");
|
|
56
|
+
if (separator > 0 && manifestPaths.includes(entryPath.slice(separator + 1))) {
|
|
57
|
+
nestedRoots.add(entryPath.slice(0, separator));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (rootManifests.length + nestedRoots.size > 1) {
|
|
61
|
+
return { discovery: null, findings: [archiveFinding("plugin.submission.archive.root_ambiguous", "fail", "Archive contains more than one plugin root.")] };
|
|
62
|
+
}
|
|
63
|
+
if (rootManifests.length === 1) {
|
|
64
|
+
return {
|
|
65
|
+
discovery: { rootPrefix: "", rootLayout: "archive-root", manifestPath: rootManifests[0], manifestFormat: rootManifests[0] },
|
|
66
|
+
findings: []
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (nestedRoots.size === 0) {
|
|
70
|
+
return { discovery: null, findings: [archiveFinding("plugin.submission.archive.manifest_missing", "fail", "Archive root does not contain a recognized plugin manifest.")] };
|
|
71
|
+
}
|
|
72
|
+
const hasRootFile = entries.some((entry) => !entry.packagePath.includes("/") && entry.resolvedKind === "file");
|
|
73
|
+
const topLevels = new Set(paths.map((entryPath) => entryPath.split("/", 1)[0]));
|
|
74
|
+
if (hasRootFile || nestedRoots.size !== 1 || topLevels.size !== 1) {
|
|
75
|
+
return { discovery: null, findings: [archiveFinding("plugin.submission.archive.root_siblings", "fail", "A top-level plugin directory cannot have siblings.")] };
|
|
76
|
+
}
|
|
77
|
+
const topLevel = [...nestedRoots][0];
|
|
78
|
+
const prefix = `${topLevel}/`;
|
|
79
|
+
const nestedManifests = manifestPaths.filter((manifestPath) => paths.includes(`${prefix}${manifestPath}`));
|
|
80
|
+
if (nestedManifests.length === 0) {
|
|
81
|
+
return { discovery: null, findings: [archiveFinding("plugin.submission.archive.manifest_missing", "fail", "Archive root does not contain a recognized plugin manifest.")] };
|
|
82
|
+
}
|
|
83
|
+
if (nestedManifests.length > 1) {
|
|
84
|
+
return { discovery: null, findings: [archiveFinding("plugin.submission.archive.root_ambiguous", "fail", "Archive contains more than one plugin manifest.")] };
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
discovery: {
|
|
88
|
+
rootPrefix: prefix,
|
|
89
|
+
rootLayout: "single-top-level-directory",
|
|
90
|
+
manifestPath: `${prefix}${nestedManifests[0]}`,
|
|
91
|
+
manifestFormat: nestedManifests[0]
|
|
92
|
+
},
|
|
93
|
+
findings: []
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function rootedReader(inspection, rootPrefix) {
|
|
97
|
+
if (inspection.reader === null)
|
|
98
|
+
return null;
|
|
99
|
+
const entries = inspection.entries
|
|
100
|
+
.filter((entry) => entry.packagePath.startsWith(rootPrefix))
|
|
101
|
+
.map((entry) => ({ ...entry, packagePath: entry.packagePath.slice(rootPrefix.length) }));
|
|
102
|
+
const byPath = new Map(entries.map((entry) => [entry.packagePath, entry]));
|
|
103
|
+
const reader = inspection.reader;
|
|
104
|
+
function directoryEntry(packagePath) {
|
|
105
|
+
if (entries.some((entry) => entry.packagePath.startsWith(`${packagePath}/`))) {
|
|
106
|
+
return { packagePath, kind: "directory", resolvedKind: "directory", size: 0, safeResolution: "safe" };
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
async list(directory) {
|
|
112
|
+
const normalizedDirectory = normalizePath(directory, true);
|
|
113
|
+
if (normalizedDirectory === null)
|
|
114
|
+
return [];
|
|
115
|
+
const prefix = normalizedDirectory === "" ? "" : `${normalizedDirectory}/`;
|
|
116
|
+
const children = new Map();
|
|
117
|
+
for (const entry of entries) {
|
|
118
|
+
if (!entry.packagePath.startsWith(prefix))
|
|
119
|
+
continue;
|
|
120
|
+
const remaining = entry.packagePath.slice(prefix.length);
|
|
121
|
+
if (remaining === "")
|
|
122
|
+
continue;
|
|
123
|
+
const [name] = remaining.split("/", 1);
|
|
124
|
+
const packagePath = normalizedDirectory === "" ? name : `${normalizedDirectory}/${name}`;
|
|
125
|
+
if (remaining.includes("/")) {
|
|
126
|
+
children.set(packagePath, directoryEntry(packagePath));
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
children.set(packagePath, entry);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return [...children.values()].sort((left, right) => left.packagePath.localeCompare(right.packagePath));
|
|
133
|
+
},
|
|
134
|
+
async stat(packagePath) {
|
|
135
|
+
const normalizedPath = normalizePath(packagePath);
|
|
136
|
+
return normalizedPath === null ? null : byPath.get(normalizedPath) ?? directoryEntry(normalizedPath);
|
|
137
|
+
},
|
|
138
|
+
async read(packagePath, maxBytes) {
|
|
139
|
+
const normalizedPath = normalizePath(packagePath);
|
|
140
|
+
if (normalizedPath === null || !byPath.has(normalizedPath))
|
|
141
|
+
return null;
|
|
142
|
+
return reader.read(`${rootPrefix}${normalizedPath}`, maxBytes);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
async function parseManifest(reader, manifestPath) {
|
|
147
|
+
const bytes = await reader.read(manifestPath, maxManifestBytes).catch(() => null);
|
|
148
|
+
if (bytes === null)
|
|
149
|
+
return null;
|
|
150
|
+
try {
|
|
151
|
+
const parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
152
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null;
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function hasImmediateSkill(entries) {
|
|
159
|
+
return entries.some((entry) => /^skills\/[^/]+\/SKILL\.md$/u.test(entry.packagePath) && entry.resolvedKind === "file");
|
|
160
|
+
}
|
|
161
|
+
function exclusionFindings(manifest, entries) {
|
|
162
|
+
const findings = [];
|
|
163
|
+
const interfaceValue = manifest.interface;
|
|
164
|
+
if (manifest.mcpServers !== undefined || entries.some((entry) => entry.packagePath === ".mcp.json")) {
|
|
165
|
+
findings.push(archiveFinding("plugin.submission.archive.mcp_excluded", "warn", "MCP configuration requires the MCP-backed submission flow.", "mcp_configuration_excluded"));
|
|
166
|
+
}
|
|
167
|
+
if (manifest.apps !== undefined || entries.some((entry) => entry.packagePath === ".app.json")) {
|
|
168
|
+
findings.push(archiveFinding("plugin.submission.archive.app_excluded", "warn", "App configuration requires the MCP-backed submission flow.", "app_configuration_excluded"));
|
|
169
|
+
}
|
|
170
|
+
if (typeof interfaceValue === "object" && interfaceValue !== null && !Array.isArray(interfaceValue)
|
|
171
|
+
&& "screenshots" in interfaceValue && interfaceValue.screenshots !== undefined) {
|
|
172
|
+
findings.push(archiveFinding("plugin.submission.archive.screenshot_excluded", "warn", "Screenshots require the MCP-backed submission flow.", "screenshot_configuration_excluded"));
|
|
173
|
+
}
|
|
174
|
+
return findings;
|
|
175
|
+
}
|
|
176
|
+
async function nestedSubmission(manifest, reader) {
|
|
177
|
+
const targetType = "skills-only";
|
|
178
|
+
const listing = validateSubmissionListing(manifest, targetType);
|
|
179
|
+
const components = [];
|
|
180
|
+
const assets = (await validateSubmissionAssetsFromReader(manifest, reader)).findings;
|
|
181
|
+
const skills = (await validateSubmissionSkillMetadataFromReader(manifest, targetType, reader)).findings;
|
|
182
|
+
const checks = [
|
|
183
|
+
submissionCheck("listing", listing),
|
|
184
|
+
submissionCheck("components", components),
|
|
185
|
+
submissionCheck("assets", assets),
|
|
186
|
+
submissionCheck("skills", skills)
|
|
187
|
+
];
|
|
188
|
+
const findings = [...listing, ...components, ...assets, ...skills];
|
|
189
|
+
const manual = manualChecklist();
|
|
190
|
+
const summary = reportSummary(checks, findings, manual);
|
|
191
|
+
return {
|
|
192
|
+
schemaVersion: "1.0.0",
|
|
193
|
+
rulesetVersion: "openai-directory-2026-08-15",
|
|
194
|
+
targetType,
|
|
195
|
+
status: summary.blockers > 0 ? "fail" : "pass",
|
|
196
|
+
readiness: summary.blockers > 0 ? "blocked" : "manual_review_required",
|
|
197
|
+
summary,
|
|
198
|
+
checks,
|
|
199
|
+
findings,
|
|
200
|
+
manualChecklist: manual
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
export function submissionArchiveExitCode(report, requireReady) {
|
|
204
|
+
return requireReady && report.status === "fail" ? 1 : 0;
|
|
205
|
+
}
|
|
206
|
+
export async function buildSubmissionArchivePreflight(archivePath) {
|
|
207
|
+
const inspection = await inspectSubmissionArchive(archivePath);
|
|
208
|
+
const coverage = [...inspection.coverage, ...archiveCoverage()];
|
|
209
|
+
const manual = manualChecklist();
|
|
210
|
+
const archiveFindings = [...inspection.findings];
|
|
211
|
+
let rootLayout = "unavailable";
|
|
212
|
+
let submission = null;
|
|
213
|
+
if (!archiveFindings.some((finding) => finding.severity === "fail") && inspection.reader !== null) {
|
|
214
|
+
const root = discoverRoot(inspection.entries);
|
|
215
|
+
archiveFindings.push(...root.findings);
|
|
216
|
+
if (root.discovery !== null) {
|
|
217
|
+
rootLayout = root.discovery.rootLayout;
|
|
218
|
+
const reader = rootedReader(inspection, root.discovery.rootPrefix);
|
|
219
|
+
const manifest = reader === null ? null : await parseManifest(reader, root.discovery.manifestPath.slice(root.discovery.rootPrefix.length));
|
|
220
|
+
if (manifest === null || reader === null) {
|
|
221
|
+
archiveFindings.push(archiveFinding("plugin.submission.archive.manifest_missing", "fail", "Plugin manifest must be a bounded UTF-8 JSON object."));
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
const entries = inspection.entries
|
|
225
|
+
.filter((entry) => entry.packagePath.startsWith(root.discovery.rootPrefix))
|
|
226
|
+
.map((entry) => ({ ...entry, packagePath: entry.packagePath.slice(root.discovery.rootPrefix.length) }));
|
|
227
|
+
if (!hasImmediateSkill(entries)) {
|
|
228
|
+
archiveFindings.push(archiveFinding("plugin.submission.archive.skill_missing", "fail", "Archive must contain an immediate skills/<skill>/SKILL.md entrypoint."));
|
|
229
|
+
}
|
|
230
|
+
archiveFindings.push(...exclusionFindings(manifest, entries));
|
|
231
|
+
submission = await nestedSubmission(manifest, reader);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const findings = [...archiveFindings, ...(submission?.findings ?? [])];
|
|
236
|
+
const blockers = findings.filter((finding) => finding.severity === "fail").length;
|
|
237
|
+
const summary = submission === null
|
|
238
|
+
? { passed: 0, warnings: findings.filter((finding) => finding.severity === "warn").length, blockers, manualChecks: manual.filter((item) => item.state === "required").length }
|
|
239
|
+
: { ...submission.summary, warnings: findings.filter((finding) => finding.severity === "warn").length, blockers };
|
|
240
|
+
return {
|
|
241
|
+
schemaVersion: "1.0.0",
|
|
242
|
+
rulesetVersion: submissionArchiveRuleset.version,
|
|
243
|
+
status: blockers > 0 ? "fail" : "pass",
|
|
244
|
+
readiness: blockers > 0 ? "blocked" : "manual_review_required",
|
|
245
|
+
archive: {
|
|
246
|
+
fileName: inspection.fileName,
|
|
247
|
+
compressedBytes: inspection.compressedBytes,
|
|
248
|
+
uncompressedBytes: inspection.uncompressedBytes,
|
|
249
|
+
entryCount: inspection.entryCount,
|
|
250
|
+
rootLayout
|
|
251
|
+
},
|
|
252
|
+
summary,
|
|
253
|
+
archiveChecks: [],
|
|
254
|
+
submission,
|
|
255
|
+
findings,
|
|
256
|
+
coverage,
|
|
257
|
+
manualChecklist: manual
|
|
258
|
+
};
|
|
259
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { SubmissionPackageEntry, SubmissionPackageReader } from "./submission-package-reader.js";
|
|
2
|
+
import type { SubmissionFinding } from "./submission-preflight.js";
|
|
3
|
+
export interface SubmissionArchiveFinding extends SubmissionFinding {
|
|
4
|
+
id: `plugin.submission.archive.${string}`;
|
|
5
|
+
}
|
|
6
|
+
export interface SubmissionArchiveCoverage {
|
|
7
|
+
id: string;
|
|
8
|
+
status: "automatic" | "manual" | "unavailable";
|
|
9
|
+
reason: string;
|
|
10
|
+
}
|
|
11
|
+
export interface SubmissionArchiveInspection {
|
|
12
|
+
fileName: string;
|
|
13
|
+
compressedBytes: number;
|
|
14
|
+
uncompressedBytes: number;
|
|
15
|
+
entryCount: number;
|
|
16
|
+
entries: readonly SubmissionPackageEntry[];
|
|
17
|
+
findings: readonly SubmissionArchiveFinding[];
|
|
18
|
+
coverage: readonly SubmissionArchiveCoverage[];
|
|
19
|
+
reader: SubmissionPackageReader | null;
|
|
20
|
+
}
|
|
21
|
+
export declare function inspectSubmissionArchive(zipPath: string): Promise<SubmissionArchiveInspection>;
|