codex-plugin-doctor 1.58.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 CHANGED
@@ -134,6 +134,25 @@ version is an immutable collision and must be replaced by a version bump. The
134
134
  command is advisory: it never authenticates, publishes, or changes npm or
135
135
  Registry records. See [MCP Registry Publication Preflight](./docs/architecture/mcp-registry-publication-preflight.md).
136
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.
155
+
137
156
  Output formats:
138
157
 
139
158
  - human text output
@@ -480,9 +499,9 @@ jobs:
480
499
  runs-on: ubuntu-latest
481
500
  steps:
482
501
  - uses: actions/checkout@v5
483
- - uses: Esquetta/CodexPluginDoctor@v1.58.0
502
+ - uses: Esquetta/CodexPluginDoctor@v1.60.0
484
503
  with:
485
- version: "1.58.0"
504
+ version: "1.60.0"
486
505
  path: .
487
506
  runtime: "true"
488
507
  policy: codex-publish
@@ -494,7 +513,7 @@ jobs:
494
513
  review-bundle-verify: "true"
495
514
  ```
496
515
 
497
- 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. 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).
498
517
 
499
518
  To self-test this repository after cloning it:
500
519
 
@@ -186,6 +186,144 @@ const publicSchemaDefinitions = [
186
186
  }
187
187
  }
188
188
  },
189
+ {
190
+ id: "doctor.submission.json",
191
+ command: "codex-plugin-doctor doctor submission <path> --json",
192
+ required: [
193
+ "schemaVersion",
194
+ "rulesetVersion",
195
+ "targetType",
196
+ "status",
197
+ "readiness",
198
+ "summary",
199
+ "checks",
200
+ "findings",
201
+ "manualChecklist"
202
+ ],
203
+ properties: {
204
+ rulesetVersion: { const: "openai-directory-2026-08-15" },
205
+ targetType: { type: "string", enum: ["skills-only", "mcp-backed"] },
206
+ status: { type: "string", enum: ["pass", "fail"] },
207
+ readiness: { type: "string", enum: ["blocked", "manual_review_required"] },
208
+ summary: {
209
+ type: "object",
210
+ required: ["passed", "warnings", "blockers", "manualChecks"],
211
+ properties: {
212
+ passed: { type: "integer", minimum: 0 },
213
+ warnings: { type: "integer", minimum: 0 },
214
+ blockers: { type: "integer", minimum: 0 },
215
+ manualChecks: { type: "integer", minimum: 0 }
216
+ },
217
+ additionalProperties: false
218
+ },
219
+ checks: {
220
+ type: "array",
221
+ items: {
222
+ type: "object",
223
+ required: ["id", "status", "findingIds"],
224
+ properties: {
225
+ id: { type: "string", enum: ["listing", "components", "assets", "skills"] },
226
+ status: { type: "string", enum: ["pass", "warn", "fail"] },
227
+ findingIds: { type: "array", items: { type: "string" } }
228
+ },
229
+ additionalProperties: false
230
+ }
231
+ },
232
+ findings: {
233
+ type: "array",
234
+ items: {
235
+ type: "object",
236
+ required: ["id", "severity", "message"],
237
+ properties: {
238
+ id: { type: "string", pattern: "^plugin\\.submission\\." },
239
+ severity: { type: "string", enum: ["warn", "fail"] },
240
+ message: { type: "string" },
241
+ portalCode: { type: "string" },
242
+ evidence: {
243
+ type: "object",
244
+ additionalProperties: { type: ["string", "number", "boolean", "null"] }
245
+ }
246
+ },
247
+ additionalProperties: false
248
+ }
249
+ },
250
+ manualChecklist: {
251
+ type: "array",
252
+ items: {
253
+ type: "object",
254
+ required: ["id", "label", "state"],
255
+ properties: {
256
+ id: { type: "string" },
257
+ label: { type: "string" },
258
+ state: { type: "string", enum: ["required", "not_applicable"] }
259
+ },
260
+ additionalProperties: false
261
+ }
262
+ }
263
+ }
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
+ },
189
327
  {
190
328
  id: "doctor.installed.check.json",
191
329
  command: "codex-plugin-doctor check --installed --json",
@@ -17,6 +17,10 @@ const topLevelCommands = [
17
17
  "explain",
18
18
  "config"
19
19
  ];
20
+ const doctorCommands = ["submission"];
21
+ const submissionTargets = ["archive"];
22
+ const submissionFlags = ["--json", "--markdown", "--output", "--require-ready"];
23
+ const fishSubmissionCondition = "__fish_seen_subcommand_from doctor; and __fish_seen_subcommand_from submission";
20
24
  function bashCompletion() {
21
25
  return [
22
26
  "_codex_plugin_doctor() {",
@@ -26,16 +30,44 @@ function bashCompletion() {
26
30
  " prev=\"${COMP_WORDS[COMP_CWORD-1]}\"",
27
31
  "",
28
32
  ` local commands="${topLevelCommands.join(" ")}"`,
33
+ ` local doctor_commands="${doctorCommands.join(" ")}"`,
34
+ ` local submission_targets="${submissionTargets.join(" ")}"`,
35
+ ` local submission_flags="${submissionFlags.join(" ")}"`,
29
36
  "",
30
37
  " case \"${prev}\" in",
31
38
  " codex-plugin-doctor)",
32
39
  " COMPREPLY=( $(compgen -W \"${commands}\" -- \"${cur}\") )",
33
40
  " return 0",
34
41
  " ;;",
42
+ " doctor)",
43
+ " COMPREPLY=( $(compgen -W \"${doctor_commands}\" -- \"${cur}\") )",
44
+ " return 0",
45
+ " ;;",
46
+ " submission)",
47
+ " COMPREPLY=( $(compgen -W \"${submission_targets}\" -- \"${cur}\") )",
48
+ " return 0",
49
+ " ;;",
35
50
  " esac",
36
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
+ "",
37
65
  " case \"${cur}\" in",
38
66
  " --*)",
67
+ " if [[ ${COMP_WORDS[1]} == \"doctor\" && ${COMP_WORDS[2]} == \"submission\" ]]; then",
68
+ " COMPREPLY=( $(compgen -W \"${submission_flags}\" -- \"${cur}\") )",
69
+ " return 0",
70
+ " fi",
39
71
  " local flags=\"--json --output --runtime --policy --help\"",
40
72
  " COMPREPLY=( $(compgen -W \"${flags}\" -- \"${cur}\") )",
41
73
  " return 0",
@@ -60,9 +92,22 @@ function zshCompletion() {
60
92
  " typeset -A opt_args",
61
93
  "",
62
94
  ` local commands=(${topLevelCommands.join(" ")})`,
95
+ ` local doctor_commands=(${doctorCommands.join(" ")})`,
96
+ "",
97
+ " if [[ \"$words[2]\" == \"doctor\" && \"$words[3]\" == \"submission\" ]]; then",
98
+ " _arguments -C \\",
99
+ " '3:archive target:(archive)' \\",
100
+ " '4:ZIP archive:_files' \\",
101
+ " '*--json[Output as JSON]' \\",
102
+ " '*--markdown[Output as Markdown]' \\",
103
+ " '*--output[Write to file]:file:_files' \\",
104
+ " '*--require-ready[Fail when automatic checks are blocked]'",
105
+ " return",
106
+ " fi",
63
107
  "",
64
108
  " _arguments -C \\",
65
109
  " '1:command:(${commands})' \\",
110
+ " '2:doctor command:(${doctor_commands})' \\",
66
111
  " '*--json[Output as JSON]' \\",
67
112
  " '*--output[Write to file]:file:_files' \\",
68
113
  " '*--runtime[Enable runtime probes]' \\",
@@ -80,6 +125,13 @@ function fishCompletion() {
80
125
  "complete -c codex-plugin-doctor -s h -l help -d 'Show help'",
81
126
  "complete -c codex-plugin-doctor -l json -d 'Output as JSON'",
82
127
  "complete -c codex-plugin-doctor -l output -d 'Write to file' -r",
128
+ `complete -c codex-plugin-doctor -n "${fishSubmissionCondition}" -l json -d 'Output as JSON'`,
129
+ `complete -c codex-plugin-doctor -n "${fishSubmissionCondition}" -l markdown -d 'Output as Markdown'`,
130
+ `complete -c codex-plugin-doctor -n "${fishSubmissionCondition}" -l output -d 'Write to file' -r`,
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'`,
134
+ `complete -c codex-plugin-doctor -n "__fish_seen_subcommand_from doctor; and not __fish_seen_subcommand_from ${doctorCommands.join(" ")}" -a "${doctorCommands.join(" ")}"`,
83
135
  "complete -c codex-plugin-doctor -l runtime -d 'Enable runtime probes'",
84
136
  "complete -c codex-plugin-doctor -l policy -d 'Apply policy' -x -a 'codex-publish mcp-strict security'",
85
137
  ""
@@ -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>;