codex-plugin-doctor 1.57.0 → 1.59.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 +20 -6
- 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.js +16 -1
- 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/output-contract.js +76 -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/shell-completion.js +29 -0
- package/dist/core/submission-assets.d.ts +6 -0
- package/dist/core/submission-assets.js +430 -0
- package/dist/core/submission-preflight.d.ts +34 -0
- package/dist/core/submission-preflight.js +289 -0
- package/dist/core/submission-ruleset.d.ts +56 -0
- package/dist/core/submission-ruleset.js +38 -0
- package/dist/core/submission-skill-metadata.d.ts +9 -0
- package/dist/core/submission-skill-metadata.js +301 -0
- 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/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/mcp/generic-mcp-doctor.js +45 -29
- package/dist/reporting/render-submission-report.d.ts +5 -0
- package/dist/reporting/render-submission-report.js +68 -0
- package/dist/rules/rule-catalog.js +130 -4
- package/dist/run-cli.js +96 -0
- 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 +5 -1
|
@@ -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
|
+
function validateListing(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 = validateListing(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
|
+
}>)[];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export const submissionRuleset = Object.freeze({
|
|
2
|
+
version: "openai-directory-2026-08-15",
|
|
3
|
+
reviewedAt: "2026-08-15",
|
|
4
|
+
sources: Object.freeze([
|
|
5
|
+
"https://developers.openai.com/plugins/build/plugins",
|
|
6
|
+
"https://developers.openai.com/plugins/deploy/app-review",
|
|
7
|
+
"https://developers.openai.com/plugins/deploy/submission-errors"
|
|
8
|
+
]),
|
|
9
|
+
limits: Object.freeze({
|
|
10
|
+
packageName: 64,
|
|
11
|
+
version: 64,
|
|
12
|
+
displayName: 30,
|
|
13
|
+
shortDescription: 30,
|
|
14
|
+
longDescription: 4000,
|
|
15
|
+
developerName: 80,
|
|
16
|
+
capabilities: 20,
|
|
17
|
+
capability: 120,
|
|
18
|
+
starterPrompts: 3,
|
|
19
|
+
starterPrompt: 128,
|
|
20
|
+
url: 1024
|
|
21
|
+
}),
|
|
22
|
+
categories: Object.freeze([
|
|
23
|
+
"Productivity", "Creativity", "Developer Tools", "Business & Operations",
|
|
24
|
+
"Data & Analytics", "Communication", "Education & Research", "Security",
|
|
25
|
+
"Finance", "Healthcare", "Travel", "Entertainment", "Other"
|
|
26
|
+
])
|
|
27
|
+
});
|
|
28
|
+
export const submissionManualChecks = Object.freeze([
|
|
29
|
+
Object.freeze({ id: "developer-business-identity", label: "Developer and business identity", mcpOnly: false }),
|
|
30
|
+
Object.freeze({ id: "attestations", label: "Required attestations", mcpOnly: false }),
|
|
31
|
+
Object.freeze({ id: "skill-safety-scan", label: "Skill safety scan", mcpOnly: false }),
|
|
32
|
+
Object.freeze({ id: "demo-video", label: "Demo video", mcpOnly: true }),
|
|
33
|
+
Object.freeze({ id: "tool-tests", label: "Exactly 5 positive and 3 negative tool tests", mcpOnly: true }),
|
|
34
|
+
Object.freeze({ id: "release-notes", label: "Release notes", mcpOnly: true }),
|
|
35
|
+
Object.freeze({ id: "production-domain-verification", label: "Production domain verification and current tool scan", mcpOnly: true }),
|
|
36
|
+
Object.freeze({ id: "tool-annotations", label: "Tool annotations and justifications", mcpOnly: true }),
|
|
37
|
+
Object.freeze({ id: "oauth-reviewer-credentials", label: "OAuth reviewer credentials", mcpOnly: true })
|
|
38
|
+
]);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DiscoveredPackage } from "../domain/types.js";
|
|
2
|
+
import type { SubmissionFinding } from "./submission-preflight.js";
|
|
3
|
+
type TargetType = "skills-only" | "mcp-backed";
|
|
4
|
+
export interface SubmissionSkillMetadataResult {
|
|
5
|
+
findings: SubmissionFinding[];
|
|
6
|
+
skillCount: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function validateSubmissionSkillMetadata(discoveredPackage: DiscoveredPackage, targetType: TargetType): Promise<SubmissionSkillMetadataResult>;
|
|
9
|
+
export {};
|