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.
@@ -0,0 +1,274 @@
1
+ import { lstat, open, readdir, realpath, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ const invalidPackagePathMessage = "Invalid package path.";
4
+ const invalidMaxBytesMessage = "maxBytes must be a nonnegative safe integer.";
5
+ function packageEntryKind(stats) {
6
+ if (stats.isFile()) {
7
+ return "file";
8
+ }
9
+ if (stats.isDirectory()) {
10
+ return "directory";
11
+ }
12
+ if (stats.isSymbolicLink()) {
13
+ return "symlink";
14
+ }
15
+ return "other";
16
+ }
17
+ function resolvedEntryKind(stats) {
18
+ const kind = packageEntryKind(stats);
19
+ return kind === "symlink" ? "other" : kind;
20
+ }
21
+ function isWithinRoot(rootPath, candidatePath) {
22
+ const relativePath = path.relative(rootPath, candidatePath);
23
+ return relativePath === "" || (relativePath !== ".."
24
+ && !relativePath.startsWith(`..${path.sep}`)
25
+ && !path.isAbsolute(relativePath));
26
+ }
27
+ function normalizePackagePath(packagePath, options) {
28
+ if (typeof packagePath !== "string") {
29
+ throw new Error(invalidPackagePathMessage);
30
+ }
31
+ if (packagePath === "") {
32
+ if (options.allowRoot) {
33
+ return "";
34
+ }
35
+ throw new Error(invalidPackagePathMessage);
36
+ }
37
+ if (/[\u0000-\u001F\u007F]/u.test(packagePath)
38
+ || packagePath.startsWith("/")
39
+ || packagePath.startsWith("\\")
40
+ || /^[a-zA-Z]:/.test(packagePath)
41
+ || packagePath.includes("\\")) {
42
+ throw new Error(invalidPackagePathMessage);
43
+ }
44
+ const pathWithoutTrailingSlash = options.allowTrailingSlash && packagePath.endsWith("/")
45
+ ? packagePath.slice(0, -1)
46
+ : packagePath;
47
+ const segments = pathWithoutTrailingSlash.split("/");
48
+ if (pathWithoutTrailingSlash === ""
49
+ || segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
50
+ throw new Error(invalidPackagePathMessage);
51
+ }
52
+ return segments.join("/");
53
+ }
54
+ function nativePathFor(rootPath, packagePath) {
55
+ const candidatePath = path.resolve(rootPath, ...packagePath.split("/"));
56
+ return isWithinRoot(rootPath, candidatePath) ? candidatePath : null;
57
+ }
58
+ function validMaxBytes(maxBytes) {
59
+ return Number.isSafeInteger(maxBytes) && maxBytes >= 0;
60
+ }
61
+ export function createDirectorySubmissionPackageReader(rootPath) {
62
+ const nativeRootPath = path.resolve(rootPath);
63
+ async function canonicalRootPath() {
64
+ try {
65
+ return await realpath(nativeRootPath);
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
71
+ async function ancestorsStayWithinRoot(packagePath, rootCanonicalPath) {
72
+ const segments = packagePath.split("/");
73
+ let ancestorPath = nativeRootPath;
74
+ for (const segment of segments.slice(0, -1)) {
75
+ ancestorPath = path.join(ancestorPath, segment);
76
+ try {
77
+ const canonicalAncestorPath = await realpath(ancestorPath);
78
+ if (!isWithinRoot(rootCanonicalPath, canonicalAncestorPath)) {
79
+ return "outside";
80
+ }
81
+ }
82
+ catch {
83
+ return "unavailable";
84
+ }
85
+ }
86
+ return "safe";
87
+ }
88
+ async function entryFor(packagePath) {
89
+ const candidatePath = nativePathFor(nativeRootPath, packagePath);
90
+ if (candidatePath === null) {
91
+ return null;
92
+ }
93
+ try {
94
+ const rootCanonicalPath = await canonicalRootPath();
95
+ if (rootCanonicalPath === null) {
96
+ return null;
97
+ }
98
+ const ancestorResolution = await ancestorsStayWithinRoot(packagePath, rootCanonicalPath);
99
+ if (ancestorResolution === "outside") {
100
+ return {
101
+ packagePath,
102
+ kind: "other",
103
+ resolvedKind: null,
104
+ size: 0,
105
+ safeResolution: "outside"
106
+ };
107
+ }
108
+ if (ancestorResolution === "unavailable") {
109
+ return null;
110
+ }
111
+ const entryStats = await lstat(candidatePath);
112
+ const kind = packageEntryKind(entryStats);
113
+ try {
114
+ const canonicalCandidatePath = await realpath(candidatePath);
115
+ const safeResolution = isWithinRoot(rootCanonicalPath, canonicalCandidatePath);
116
+ if (!safeResolution) {
117
+ return {
118
+ packagePath,
119
+ kind,
120
+ resolvedKind: null,
121
+ size: entryStats.size,
122
+ safeResolution: "outside"
123
+ };
124
+ }
125
+ const targetStats = await stat(candidatePath);
126
+ const resolvedPackagePath = path.relative(rootCanonicalPath, canonicalCandidatePath).split(path.sep).join("/");
127
+ return {
128
+ packagePath,
129
+ ...(resolvedPackagePath === packagePath ? {} : { resolvedPackagePath }),
130
+ kind,
131
+ resolvedKind: resolvedEntryKind(targetStats),
132
+ size: targetStats.size,
133
+ safeResolution: "safe"
134
+ };
135
+ }
136
+ catch {
137
+ return kind === "symlink"
138
+ ? {
139
+ packagePath,
140
+ kind,
141
+ resolvedKind: null,
142
+ size: entryStats.size,
143
+ safeResolution: "unavailable"
144
+ }
145
+ : null;
146
+ }
147
+ }
148
+ catch {
149
+ return null;
150
+ }
151
+ }
152
+ return {
153
+ async list(directory) {
154
+ const normalizedDirectory = normalizePackagePath(directory, {
155
+ allowRoot: true,
156
+ allowTrailingSlash: true
157
+ });
158
+ const directoryPath = normalizedDirectory === ""
159
+ ? nativeRootPath
160
+ : nativePathFor(nativeRootPath, normalizedDirectory);
161
+ if (directoryPath === null) {
162
+ return [];
163
+ }
164
+ try {
165
+ if (normalizedDirectory !== "") {
166
+ const directoryEntry = await entryFor(normalizedDirectory);
167
+ if (directoryEntry?.resolvedKind !== "directory" || directoryEntry.safeResolution !== "safe") {
168
+ return [];
169
+ }
170
+ }
171
+ else if (await canonicalRootPath() === null) {
172
+ return [];
173
+ }
174
+ const childNames = await readdir(directoryPath);
175
+ const entries = new Array(childNames.length).fill(null);
176
+ let nextChildIndex = 0;
177
+ async function readNextChild() {
178
+ while (nextChildIndex < childNames.length) {
179
+ const childIndex = nextChildIndex;
180
+ nextChildIndex += 1;
181
+ const childName = childNames[childIndex];
182
+ const childPackagePath = normalizedDirectory === ""
183
+ ? childName
184
+ : `${normalizedDirectory}/${childName}`;
185
+ try {
186
+ const normalizedChildPath = normalizePackagePath(childPackagePath, {
187
+ allowRoot: false,
188
+ allowTrailingSlash: false
189
+ });
190
+ entries[childIndex] = await entryFor(normalizedChildPath);
191
+ }
192
+ catch {
193
+ entries[childIndex] = null;
194
+ }
195
+ }
196
+ }
197
+ const workerCount = Math.min(16, childNames.length);
198
+ const workers = Array.from({ length: workerCount }, async () => readNextChild());
199
+ await Promise.all(workers);
200
+ return entries
201
+ .filter((entry) => entry !== null)
202
+ .sort((left, right) => left.packagePath.localeCompare(right.packagePath));
203
+ }
204
+ catch {
205
+ return [];
206
+ }
207
+ },
208
+ async stat(packagePath) {
209
+ return entryFor(normalizePackagePath(packagePath, {
210
+ allowRoot: false,
211
+ allowTrailingSlash: false
212
+ }));
213
+ },
214
+ async read(packagePath, maxBytes) {
215
+ const normalizedPackagePath = normalizePackagePath(packagePath, {
216
+ allowRoot: false,
217
+ allowTrailingSlash: false
218
+ });
219
+ if (!validMaxBytes(maxBytes)) {
220
+ throw new Error(invalidMaxBytesMessage);
221
+ }
222
+ const entry = await entryFor(normalizedPackagePath);
223
+ if (entry?.resolvedKind !== "file" || entry.safeResolution !== "safe" || entry.size > maxBytes) {
224
+ return null;
225
+ }
226
+ const candidatePath = nativePathFor(nativeRootPath, normalizedPackagePath);
227
+ if (candidatePath === null) {
228
+ return null;
229
+ }
230
+ let fileHandle = null;
231
+ try {
232
+ fileHandle = await open(candidatePath, "r");
233
+ const handleStats = await fileHandle.stat();
234
+ if (!handleStats.isFile()
235
+ || !Number.isSafeInteger(handleStats.size)
236
+ || handleStats.size < 0
237
+ || handleStats.size > maxBytes) {
238
+ return null;
239
+ }
240
+ const rootCanonicalPath = await canonicalRootPath();
241
+ const canonicalCandidatePath = await realpath(candidatePath);
242
+ const pathStats = await stat(candidatePath);
243
+ if (rootCanonicalPath === null
244
+ || !isWithinRoot(rootCanonicalPath, canonicalCandidatePath)
245
+ || !pathStats.isFile()
246
+ || handleStats.dev !== pathStats.dev
247
+ || handleStats.ino !== pathStats.ino) {
248
+ return null;
249
+ }
250
+ const content = Buffer.alloc(handleStats.size);
251
+ let offset = 0;
252
+ while (offset < content.length) {
253
+ const { bytesRead } = await fileHandle.read(content, offset, content.length - offset, offset);
254
+ if (bytesRead === 0) {
255
+ return null;
256
+ }
257
+ offset += bytesRead;
258
+ }
259
+ const probe = Buffer.alloc(1);
260
+ const { bytesRead: extraBytesRead } = await fileHandle.read(probe, 0, 1, offset);
261
+ if (extraBytesRead !== 0) {
262
+ return null;
263
+ }
264
+ return Uint8Array.from(content);
265
+ }
266
+ catch {
267
+ return null;
268
+ }
269
+ finally {
270
+ await fileHandle?.close().catch(() => undefined);
271
+ }
272
+ }
273
+ };
274
+ }
@@ -0,0 +1,36 @@
1
+ export interface SubmissionFinding {
2
+ id: `plugin.submission.${string}`;
3
+ severity: "warn" | "fail";
4
+ message: string;
5
+ portalCode?: string;
6
+ evidence?: Record<string, string | number | boolean | null>;
7
+ }
8
+ export interface SubmissionCheck {
9
+ id: "listing" | "components" | "assets" | "skills";
10
+ status: "pass" | "warn" | "fail";
11
+ findingIds: string[];
12
+ }
13
+ export interface SubmissionManualCheck {
14
+ id: string;
15
+ label: string;
16
+ state: "required" | "not_applicable";
17
+ }
18
+ export interface SubmissionPreflightReport {
19
+ schemaVersion: "1.0.0";
20
+ rulesetVersion: "openai-directory-2026-08-15";
21
+ targetType: "skills-only" | "mcp-backed";
22
+ status: "pass" | "fail";
23
+ readiness: "blocked" | "manual_review_required";
24
+ summary: {
25
+ passed: number;
26
+ warnings: number;
27
+ blockers: number;
28
+ manualChecks: number;
29
+ };
30
+ checks: SubmissionCheck[];
31
+ findings: SubmissionFinding[];
32
+ manualChecklist: SubmissionManualCheck[];
33
+ }
34
+ export type SubmissionTargetType = SubmissionPreflightReport["targetType"];
35
+ export declare function validateSubmissionListing(manifest: Record<string, unknown>, targetType: SubmissionTargetType): SubmissionFinding[];
36
+ export declare function buildSubmissionPreflight(targetPath: string): Promise<SubmissionPreflightReport>;
@@ -0,0 +1,289 @@
1
+ import { lstat, readFile, realpath, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { validateSubmissionAssets } from "./submission-assets.js";
4
+ import { submissionManualChecks, submissionRuleset } from "./submission-ruleset.js";
5
+ import { validateSubmissionSkillMetadata } from "./submission-skill-metadata.js";
6
+ const packageNamePattern = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
7
+ const semverPattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
8
+ const singleLineUnsupported = /[\u0000-\u001F\u007F\u2028\u2029\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF]/u;
9
+ const multilineUnsupported = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F\u2028\u2029\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF]/u;
10
+ const knownInterfaceFields = new Set([
11
+ "displayName", "shortDescription", "longDescription", "developerName", "category",
12
+ "capabilities", "websiteURL", "supportURL", "privacyPolicyURL", "termsOfServiceURL",
13
+ "brandColor", "brandColorDark", "defaultPrompt", "composerIcon", "logo", "screenshots"
14
+ ]);
15
+ const maxSubmissionManifestBytes = 1024 * 1024;
16
+ function isRecord(value) {
17
+ return typeof value === "object" && value !== null && !Array.isArray(value);
18
+ }
19
+ function finding(id, severity, message, evidence) {
20
+ return evidence === undefined ? { id, severity, message } : { id, severity, message, evidence };
21
+ }
22
+ function invalidField(field, evidence) {
23
+ return finding(`plugin.submission.interface.${field}`, "fail", "Listing field is invalid.", {
24
+ field,
25
+ ...evidence
26
+ });
27
+ }
28
+ function hasValidText(value, maximum, multiline = false) {
29
+ return typeof value === "string"
30
+ && value.trim().length > 0
31
+ && value.length <= maximum
32
+ && !(multiline ? multilineUnsupported : singleLineUnsupported).test(value);
33
+ }
34
+ function validateRequiredText(source, property, field, limit, multiline, findings) {
35
+ if (!hasValidText(source[property], limit, multiline)) {
36
+ findings.push(invalidField(field, { limit }));
37
+ }
38
+ }
39
+ function validatePackage(manifest, findings) {
40
+ const name = manifest.name;
41
+ if (typeof name !== "string" || name.length > submissionRuleset.limits.packageName || !packageNamePattern.test(name)) {
42
+ findings.push(finding("plugin.submission.package.name", "fail", "Package name is invalid.", {
43
+ field: "name", limit: submissionRuleset.limits.packageName
44
+ }));
45
+ }
46
+ const version = manifest.version;
47
+ if (typeof version !== "string" || version.length > submissionRuleset.limits.version || !semverPattern.test(version)) {
48
+ findings.push(finding("plugin.submission.package.version", "fail", "Package version is invalid.", {
49
+ field: "version", limit: submissionRuleset.limits.version
50
+ }));
51
+ }
52
+ }
53
+ function validateCapabilities(value, findings) {
54
+ if (value === undefined) {
55
+ return;
56
+ }
57
+ if (!Array.isArray(value) || value.length > submissionRuleset.limits.capabilities) {
58
+ findings.push(invalidField("capabilities", { limit: submissionRuleset.limits.capabilities }));
59
+ }
60
+ if (!Array.isArray(value)) {
61
+ return;
62
+ }
63
+ if (value.some((capability) => !hasValidText(capability, submissionRuleset.limits.capability))) {
64
+ findings.push(invalidField("capability", { limit: submissionRuleset.limits.capability }));
65
+ }
66
+ }
67
+ function normalizePrompt(prompt) {
68
+ return prompt.normalize("NFKC").trim().replace(/\s+/gu, " ");
69
+ }
70
+ function validateDefaultPrompt(value, findings) {
71
+ if (value === undefined) {
72
+ return;
73
+ }
74
+ const prompts = typeof value === "string" ? [value] : value;
75
+ if (!Array.isArray(prompts) || prompts.length > submissionRuleset.limits.starterPrompts) {
76
+ findings.push(invalidField("default_prompt", { limit: submissionRuleset.limits.starterPrompts }));
77
+ return;
78
+ }
79
+ const normalized = new Set();
80
+ const invalid = prompts.some((prompt) => {
81
+ if (!hasValidText(prompt, submissionRuleset.limits.starterPrompt) || prompt.includes("@")) {
82
+ return true;
83
+ }
84
+ const normalizedPrompt = normalizePrompt(prompt);
85
+ if (normalized.has(normalizedPrompt)) {
86
+ return true;
87
+ }
88
+ normalized.add(normalizedPrompt);
89
+ return false;
90
+ });
91
+ if (invalid) {
92
+ findings.push(invalidField("default_prompt", { limit: submissionRuleset.limits.starterPrompt }));
93
+ }
94
+ }
95
+ function isValidHttpsUrl(value) {
96
+ if (typeof value !== "string" || value.length === 0 || value.length > submissionRuleset.limits.url
97
+ || value.trim() !== value || singleLineUnsupported.test(value)) {
98
+ return false;
99
+ }
100
+ try {
101
+ const parsed = new URL(value);
102
+ return parsed.protocol === "https:" && parsed.hostname.length > 0
103
+ && parsed.username.length === 0 && parsed.password.length === 0 && parsed.hash.length === 0;
104
+ }
105
+ catch {
106
+ return false;
107
+ }
108
+ }
109
+ export function validateSubmissionListing(manifest, targetType) {
110
+ const findings = [];
111
+ validatePackage(manifest, findings);
112
+ const listing = manifest.interface;
113
+ if (!isRecord(listing)) {
114
+ findings.push(finding("plugin.submission.interface.required", "fail", "Listing interface mapping is required.", { field: "interface" }));
115
+ return findings;
116
+ }
117
+ validateRequiredText(listing, "displayName", "display_name", submissionRuleset.limits.displayName, false, findings);
118
+ validateRequiredText(listing, "shortDescription", "short_description", submissionRuleset.limits.shortDescription, false, findings);
119
+ validateRequiredText(listing, "longDescription", "long_description", submissionRuleset.limits.longDescription, true, findings);
120
+ validateRequiredText(listing, "developerName", "developer_name", submissionRuleset.limits.developerName, false, findings);
121
+ validateRequiredText(listing, "category", "category", 80, false, findings);
122
+ if (typeof listing.category === "string" && !submissionRuleset.categories.includes(listing.category)) {
123
+ findings.push(invalidField("category"));
124
+ }
125
+ validateCapabilities(listing.capabilities, findings);
126
+ validateDefaultPrompt(listing.defaultPrompt, findings);
127
+ if (targetType === "mcp-backed") {
128
+ for (const property of ["websiteURL", "supportURL", "privacyPolicyURL", "termsOfServiceURL"]) {
129
+ if (!isValidHttpsUrl(listing[property])) {
130
+ findings.push(finding("plugin.submission.interface.url", "fail", "Required listing URL is invalid.", {
131
+ field: property, limit: submissionRuleset.limits.url
132
+ }));
133
+ }
134
+ }
135
+ }
136
+ for (const key of Object.keys(listing)) {
137
+ if (!knownInterfaceFields.has(key)) {
138
+ findings.push(finding("plugin.submission.interface.unknown_field", "warn", "Listing interface contains an unsupported field.", { field: key }));
139
+ }
140
+ }
141
+ return findings;
142
+ }
143
+ function isWithin(rootPath, candidatePath) {
144
+ const relative = path.relative(rootPath, candidatePath);
145
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
146
+ }
147
+ async function validateApp(rootPath, declaration) {
148
+ if (declaration === undefined) {
149
+ return [];
150
+ }
151
+ const invalid = () => [finding("plugin.submission.component.app", "fail", "App declaration must reference a contained parseable root file.", { path: ".app.json" })];
152
+ const invalidPath = () => [finding("plugin.submission.app.invalid_path", "fail", "App declaration resolves outside the package.", { path: ".app.json" })];
153
+ if (declaration !== "./.app.json") {
154
+ return invalid();
155
+ }
156
+ const candidatePath = path.resolve(rootPath, ".app.json");
157
+ try {
158
+ const [canonicalRoot, canonicalCandidate, details] = await Promise.all([
159
+ realpath(rootPath),
160
+ realpath(candidatePath),
161
+ stat(candidatePath)
162
+ ]);
163
+ if (!isWithin(canonicalRoot, canonicalCandidate)) {
164
+ return invalidPath();
165
+ }
166
+ if (!details.isFile() || details.size > 5 * 1024 * 1024) {
167
+ return invalid();
168
+ }
169
+ JSON.parse(await readFile(candidatePath, "utf8"));
170
+ return [];
171
+ }
172
+ catch {
173
+ return invalid();
174
+ }
175
+ }
176
+ function checkStatus(findings) {
177
+ if (findings.some((item) => item.severity === "fail")) {
178
+ return "fail";
179
+ }
180
+ return findings.some((item) => item.severity === "warn") ? "warn" : "pass";
181
+ }
182
+ function check(id, findings) {
183
+ return { id, status: checkStatus(findings), findingIds: findings.map((item) => item.id) };
184
+ }
185
+ function checklist(targetType) {
186
+ return submissionManualChecks.map((item) => ({
187
+ id: item.id,
188
+ label: item.label,
189
+ state: item.mcpOnly && targetType === "skills-only" ? "not_applicable" : "required"
190
+ }));
191
+ }
192
+ function invalidPackageReport() {
193
+ const findings = [finding("plugin.submission.package.invalid", "fail", "Plugin manifest is missing or invalid.")];
194
+ const checks = [check("listing", findings), check("components", []), check("assets", []), check("skills", [])];
195
+ return {
196
+ schemaVersion: "1.0.0",
197
+ rulesetVersion: submissionRuleset.version,
198
+ targetType: "skills-only",
199
+ status: "fail",
200
+ readiness: "blocked",
201
+ summary: { passed: 3, warnings: 0, blockers: 1, manualChecks: 3 },
202
+ checks,
203
+ findings,
204
+ manualChecklist: checklist("skills-only")
205
+ };
206
+ }
207
+ function oversizedPackageReport() {
208
+ const findings = [finding("plugin.submission.package.too_large", "fail", "Plugin manifest exceeds the submission preflight size limit.", {
209
+ path: ".codex-plugin/plugin.json", limit: maxSubmissionManifestBytes
210
+ })];
211
+ const checks = [check("listing", findings), check("components", []), check("assets", []), check("skills", [])];
212
+ return {
213
+ schemaVersion: "1.0.0",
214
+ rulesetVersion: submissionRuleset.version,
215
+ targetType: "skills-only",
216
+ status: "fail",
217
+ readiness: "blocked",
218
+ summary: { passed: 3, warnings: 0, blockers: 1, manualChecks: 3 },
219
+ checks,
220
+ findings,
221
+ manualChecklist: checklist("skills-only")
222
+ };
223
+ }
224
+ async function discoverSubmissionPackage(targetPath) {
225
+ const rootPath = path.resolve(targetPath);
226
+ const manifestPath = path.join(rootPath, ".codex-plugin", "plugin.json");
227
+ try {
228
+ const linkDetails = await lstat(manifestPath);
229
+ if (!linkDetails.isFile())
230
+ return null;
231
+ const details = await stat(manifestPath);
232
+ if (details.size > maxSubmissionManifestBytes)
233
+ return "too_large";
234
+ const content = new TextDecoder("utf-8", { fatal: true }).decode(await readFile(manifestPath));
235
+ return { rootPath, manifestPath, manifest: JSON.parse(content) };
236
+ }
237
+ catch {
238
+ return null;
239
+ }
240
+ }
241
+ export async function buildSubmissionPreflight(targetPath) {
242
+ if (typeof targetPath !== "string") {
243
+ return invalidPackageReport();
244
+ }
245
+ const discovered = await discoverSubmissionPackage(targetPath);
246
+ if (discovered === "too_large") {
247
+ return oversizedPackageReport();
248
+ }
249
+ if (!discovered || !isRecord(discovered.manifest)) {
250
+ return invalidPackageReport();
251
+ }
252
+ const manifest = discovered.manifest;
253
+ const targetType = manifest.mcpServers !== undefined || manifest.apps !== undefined
254
+ ? "mcp-backed"
255
+ : "skills-only";
256
+ const listingFindings = validateSubmissionListing(manifest, targetType);
257
+ const componentFindings = await validateApp(discovered.rootPath, manifest.apps);
258
+ const assetFindings = (await validateSubmissionAssets(discovered)).findings;
259
+ const skillFindings = (await validateSubmissionSkillMetadata(discovered, targetType)).findings;
260
+ const interfaceValue = manifest.interface;
261
+ if (targetType === "skills-only" && isRecord(interfaceValue) && interfaceValue.screenshots !== undefined) {
262
+ componentFindings.push(finding("plugin.submission.component.excluded", "fail", "Screenshots are excluded for skills-only submissions.", { field: "screenshots" }));
263
+ }
264
+ const findings = [...listingFindings, ...componentFindings, ...assetFindings, ...skillFindings];
265
+ const checks = [
266
+ check("listing", listingFindings),
267
+ check("components", componentFindings),
268
+ check("assets", assetFindings),
269
+ check("skills", skillFindings)
270
+ ];
271
+ const blockers = findings.filter((item) => item.severity === "fail").length;
272
+ const manualChecklist = checklist(targetType);
273
+ return {
274
+ schemaVersion: "1.0.0",
275
+ rulesetVersion: submissionRuleset.version,
276
+ targetType,
277
+ status: blockers > 0 ? "fail" : "pass",
278
+ readiness: blockers > 0 ? "blocked" : "manual_review_required",
279
+ summary: {
280
+ passed: checks.filter((item) => item.status === "pass").length,
281
+ warnings: findings.filter((item) => item.severity === "warn").length,
282
+ blockers,
283
+ manualChecks: manualChecklist.filter((item) => item.state === "required").length
284
+ },
285
+ checks,
286
+ findings,
287
+ manualChecklist
288
+ };
289
+ }
@@ -0,0 +1,56 @@
1
+ export declare const submissionRuleset: Readonly<{
2
+ version: "openai-directory-2026-08-15";
3
+ reviewedAt: "2026-08-15";
4
+ sources: readonly string[];
5
+ limits: Readonly<{
6
+ packageName: 64;
7
+ version: 64;
8
+ displayName: 30;
9
+ shortDescription: 30;
10
+ longDescription: 4000;
11
+ developerName: 80;
12
+ capabilities: 20;
13
+ capability: 120;
14
+ starterPrompts: 3;
15
+ starterPrompt: 128;
16
+ url: 1024;
17
+ }>;
18
+ categories: readonly string[];
19
+ }>;
20
+ export declare const submissionManualChecks: readonly (Readonly<{
21
+ id: "developer-business-identity";
22
+ label: "Developer and business identity";
23
+ mcpOnly: false;
24
+ }> | Readonly<{
25
+ id: "attestations";
26
+ label: "Required attestations";
27
+ mcpOnly: false;
28
+ }> | Readonly<{
29
+ id: "skill-safety-scan";
30
+ label: "Skill safety scan";
31
+ mcpOnly: false;
32
+ }> | Readonly<{
33
+ id: "demo-video";
34
+ label: "Demo video";
35
+ mcpOnly: true;
36
+ }> | Readonly<{
37
+ id: "tool-tests";
38
+ label: "Exactly 5 positive and 3 negative tool tests";
39
+ mcpOnly: true;
40
+ }> | Readonly<{
41
+ id: "release-notes";
42
+ label: "Release notes";
43
+ mcpOnly: true;
44
+ }> | Readonly<{
45
+ id: "production-domain-verification";
46
+ label: "Production domain verification and current tool scan";
47
+ mcpOnly: true;
48
+ }> | Readonly<{
49
+ id: "tool-annotations";
50
+ label: "Tool annotations and justifications";
51
+ mcpOnly: true;
52
+ }> | Readonly<{
53
+ id: "oauth-reviewer-credentials";
54
+ label: "OAuth reviewer credentials";
55
+ mcpOnly: true;
56
+ }>)[];