codex-plugin-doctor 1.59.0 → 1.61.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 +40 -19
- package/dist/core/bounded-http-client.js +1 -0
- package/dist/core/mcp-discovery.d.ts +23 -0
- package/dist/core/mcp-discovery.js +148 -0
- 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-mcp-discovery-report.d.ts +2 -0
- package/dist/reporting/render-mcp-discovery-report.js +14 -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.d.ts +2 -0
- package/dist/run-cli.js +139 -1
- package/package.json +5 -2
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const sources = Object.freeze([
|
|
2
|
+
"https://developers.openai.com/plugins/build/plugins",
|
|
3
|
+
"https://developers.openai.com/plugins/deploy/submission-errors"
|
|
4
|
+
]);
|
|
5
|
+
const portalRules = Object.freeze([
|
|
6
|
+
Object.freeze({ id: "plugin.submission.archive.plugin_name_mismatch", status: "unavailable", reason: "Requires the previously published plugin identity.", source: "submission-errors" }),
|
|
7
|
+
Object.freeze({ id: "plugin.submission.archive.plugin_version_unchanged", status: "unavailable", reason: "Requires the previously published plugin version.", source: "submission-errors" }),
|
|
8
|
+
Object.freeze({ id: "plugin.submission.archive.manifest_normalized", status: "unavailable", reason: "The portal owns exact manifest normalization.", source: "submission-errors" }),
|
|
9
|
+
Object.freeze({ id: "plugin.submission.archive.developer_name_defaulted", status: "unavailable", reason: "Requires the selected verified developer identity.", source: "submission-errors" }),
|
|
10
|
+
Object.freeze({ id: "plugin.submission.archive.path_length", status: "unavailable", reason: "The public reference does not publish a numeric path-length limit.", source: "submission-errors" }),
|
|
11
|
+
Object.freeze({ id: "plugin.submission.archive.normalization_algorithm", status: "unavailable", reason: "The portal does not publish its case and Unicode collision algorithm.", source: "submission-errors" }),
|
|
12
|
+
Object.freeze({ id: "plugin.submission.archive.claude_format_normalized", status: "manual", reason: "The portal owns Claude-format normalization details.", source: "submission-errors" })
|
|
13
|
+
]);
|
|
14
|
+
export const submissionArchiveRuleset = Object.freeze({
|
|
15
|
+
version: "openai-directory-archive-2026-08-23",
|
|
16
|
+
reviewedAt: "2026-08-23",
|
|
17
|
+
sources,
|
|
18
|
+
limits: Object.freeze({
|
|
19
|
+
compressedBytes: 100 * 1000 * 1000,
|
|
20
|
+
entries: 5_000,
|
|
21
|
+
memberBytes: 100 * 1024 * 1024,
|
|
22
|
+
totalBytes: 512 * 1024 * 1024
|
|
23
|
+
}),
|
|
24
|
+
structures: Object.freeze({
|
|
25
|
+
eocdComments: "automatic",
|
|
26
|
+
zip64: "automatic",
|
|
27
|
+
dataDescriptors: "automatic"
|
|
28
|
+
}),
|
|
29
|
+
compression: Object.freeze({
|
|
30
|
+
stored: "automatic",
|
|
31
|
+
deflate: "automatic"
|
|
32
|
+
}),
|
|
33
|
+
collisionAlgorithm: "NFKC + toLowerCase per segment",
|
|
34
|
+
automaticChecks: Object.freeze([
|
|
35
|
+
"plugin.submission.archive.invalid_file",
|
|
36
|
+
"plugin.submission.archive.invalid_zip",
|
|
37
|
+
"plugin.submission.archive.too_large",
|
|
38
|
+
"plugin.submission.archive.entry_count",
|
|
39
|
+
"plugin.submission.archive.member_too_large",
|
|
40
|
+
"plugin.submission.archive.total_too_large",
|
|
41
|
+
"plugin.submission.archive.encrypted",
|
|
42
|
+
"plugin.submission.archive.header_mismatch",
|
|
43
|
+
"plugin.submission.archive.descriptor_invalid",
|
|
44
|
+
"plugin.submission.archive.range_invalid",
|
|
45
|
+
"plugin.submission.archive.crc_mismatch",
|
|
46
|
+
"plugin.submission.archive.path_invalid",
|
|
47
|
+
"plugin.submission.archive.path_duplicate",
|
|
48
|
+
"plugin.submission.archive.path_conflict",
|
|
49
|
+
"plugin.submission.archive.type_unsupported",
|
|
50
|
+
"plugin.submission.archive.normalization_collision",
|
|
51
|
+
"plugin.submission.archive.root_missing",
|
|
52
|
+
"plugin.submission.archive.root_ambiguous",
|
|
53
|
+
"plugin.submission.archive.root_siblings",
|
|
54
|
+
"plugin.submission.archive.manifest_missing",
|
|
55
|
+
"plugin.submission.archive.skill_missing",
|
|
56
|
+
"plugin.submission.archive.mcp_excluded",
|
|
57
|
+
"plugin.submission.archive.app_excluded",
|
|
58
|
+
"plugin.submission.archive.screenshot_excluded"
|
|
59
|
+
]),
|
|
60
|
+
portalRules
|
|
61
|
+
});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import type { DiscoveredPackage } from "../domain/types.js";
|
|
1
|
+
import type { DiscoveredPackage, PluginManifest } from "../domain/types.js";
|
|
2
|
+
import { type SubmissionPackageReader } from "./submission-package-reader.js";
|
|
2
3
|
import type { SubmissionFinding } from "./submission-preflight.js";
|
|
3
4
|
export interface SubmissionAssetResult {
|
|
4
5
|
findings: SubmissionFinding[];
|
|
5
6
|
}
|
|
7
|
+
export declare function validateSubmissionAssetsFromReader(manifest: PluginManifest, reader: SubmissionPackageReader): Promise<SubmissionAssetResult>;
|
|
6
8
|
export declare function validateSubmissionAssets(discoveredPackage: DiscoveredPackage): Promise<SubmissionAssetResult>;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { readFile, stat } from "node:fs/promises";
|
|
2
1
|
import path from "node:path";
|
|
3
2
|
import { inflateSync } from "node:zlib";
|
|
4
3
|
import { XMLParser, XMLValidator } from "fast-xml-parser";
|
|
5
|
-
import {
|
|
4
|
+
import { createDirectorySubmissionPackageReader } from "./submission-package-reader.js";
|
|
6
5
|
const maxAssetBytes = 5 * 1024 * 1024;
|
|
7
6
|
const MAX_DECODED_PNG_BYTES = 72 * 1024 * 1024;
|
|
8
7
|
const minimumDimension = 48;
|
|
@@ -360,40 +359,60 @@ function dimensionFinding(field, packagePath, format, dimensions) {
|
|
|
360
359
|
}
|
|
361
360
|
return null;
|
|
362
361
|
}
|
|
363
|
-
|
|
362
|
+
function assetPackagePath(value) {
|
|
363
|
+
if (!value.startsWith("./"))
|
|
364
|
+
return null;
|
|
365
|
+
const packagePath = value.slice(2);
|
|
366
|
+
if (packagePath === ""
|
|
367
|
+
|| packagePath.startsWith("/")
|
|
368
|
+
|| /^[a-zA-Z]:/u.test(packagePath)
|
|
369
|
+
|| packagePath.includes("\\")
|
|
370
|
+
|| /[\u0000-\u001F\u007F]/u.test(packagePath)) {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
const normalizedPackagePath = path.posix.normalize(packagePath);
|
|
374
|
+
if (normalizedPackagePath === "."
|
|
375
|
+
|| normalizedPackagePath === ""
|
|
376
|
+
|| normalizedPackagePath === ".."
|
|
377
|
+
|| normalizedPackagePath.startsWith("../")
|
|
378
|
+
|| path.posix.isAbsolute(normalizedPackagePath))
|
|
379
|
+
return null;
|
|
380
|
+
return normalizedPackagePath;
|
|
381
|
+
}
|
|
382
|
+
async function validateAsset(reader, field, value) {
|
|
364
383
|
if (value === undefined) {
|
|
365
384
|
return [finding("plugin.submission.asset.required", "Branding asset is required.", assetEvidence(field))];
|
|
366
385
|
}
|
|
367
386
|
if (typeof value !== "string") {
|
|
368
387
|
return [finding("plugin.submission.asset.invalid_path", "Branding asset path is invalid.", assetEvidence(field))];
|
|
369
388
|
}
|
|
370
|
-
const
|
|
371
|
-
if (
|
|
389
|
+
const packagePath = assetPackagePath(value);
|
|
390
|
+
if (packagePath === null) {
|
|
372
391
|
return [finding("plugin.submission.asset.invalid_path", "Branding asset path is invalid.", assetEvidence(field))];
|
|
373
392
|
}
|
|
374
|
-
const extension = extensions.get(path.extname(
|
|
393
|
+
const extension = extensions.get(path.extname(packagePath).toLowerCase());
|
|
375
394
|
if (extension === undefined) {
|
|
376
|
-
return [finding("plugin.submission.asset.unsupported_format", "Branding asset format is unsupported.", assetEvidence(field,
|
|
395
|
+
return [finding("plugin.submission.asset.unsupported_format", "Branding asset format is unsupported.", assetEvidence(field, packagePath))];
|
|
377
396
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
397
|
+
const details = await reader.stat(packagePath).catch(() => null);
|
|
398
|
+
if (details === null) {
|
|
399
|
+
return [finding("plugin.submission.asset.missing", "Branding asset is missing.", assetEvidence(field, packagePath, extension))];
|
|
381
400
|
}
|
|
382
|
-
|
|
383
|
-
return [finding("plugin.submission.asset.
|
|
401
|
+
if (details.safeResolution === "outside") {
|
|
402
|
+
return [finding("plugin.submission.asset.invalid_path", "Branding asset path is invalid.", assetEvidence(field))];
|
|
384
403
|
}
|
|
385
|
-
if (
|
|
386
|
-
return [finding("plugin.submission.asset.
|
|
404
|
+
if (details.safeResolution !== "safe") {
|
|
405
|
+
return [finding("plugin.submission.asset.missing", "Branding asset is missing.", assetEvidence(field, packagePath, extension))];
|
|
387
406
|
}
|
|
388
|
-
if (details.
|
|
389
|
-
return [finding("plugin.submission.asset.
|
|
407
|
+
if (details.resolvedKind !== "file") {
|
|
408
|
+
return [finding("plugin.submission.asset.unsupported_format", "Branding asset must be a regular file.", assetEvidence(field, packagePath, extension))];
|
|
390
409
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
content = await readFile(resolved.path);
|
|
410
|
+
if (details.size > maxAssetBytes) {
|
|
411
|
+
return [finding("plugin.submission.asset.too_large", "Branding asset exceeds the size limit.", { ...assetEvidence(field, packagePath, extension), limit: maxAssetBytes })];
|
|
394
412
|
}
|
|
395
|
-
catch
|
|
396
|
-
|
|
413
|
+
const content = await reader.read(packagePath, maxAssetBytes).catch(() => null);
|
|
414
|
+
if (content === null) {
|
|
415
|
+
return [finding("plugin.submission.asset.missing", "Branding asset cannot be read.", assetEvidence(field, packagePath, extension))];
|
|
397
416
|
}
|
|
398
417
|
if (extension === "svg") {
|
|
399
418
|
let source;
|
|
@@ -401,30 +420,32 @@ async function validateAsset(rootPath, field, value) {
|
|
|
401
420
|
source = new TextDecoder("utf-8", { fatal: true }).decode(content);
|
|
402
421
|
}
|
|
403
422
|
catch {
|
|
404
|
-
return [finding("plugin.submission.asset.unsafe_svg", "SVG must be valid UTF-8.", assetEvidence(field,
|
|
423
|
+
return [finding("plugin.submission.asset.unsafe_svg", "SVG must be valid UTF-8.", assetEvidence(field, packagePath, extension))];
|
|
405
424
|
}
|
|
406
425
|
const dimensions = svgDimensions(source);
|
|
407
426
|
if (dimensions === null) {
|
|
408
|
-
return [finding("plugin.submission.asset.unsafe_svg", "SVG is unsafe or lacks valid dimensions.", assetEvidence(field,
|
|
427
|
+
return [finding("plugin.submission.asset.unsafe_svg", "SVG is unsafe or lacks valid dimensions.", assetEvidence(field, packagePath, extension))];
|
|
409
428
|
}
|
|
410
|
-
const invalidDimensions = dimensionFinding(field,
|
|
429
|
+
const invalidDimensions = dimensionFinding(field, packagePath, extension, dimensions);
|
|
411
430
|
return invalidDimensions === null ? [] : [invalidDimensions];
|
|
412
431
|
}
|
|
413
432
|
const decoded = rasterAsset(content);
|
|
414
433
|
if (decoded === null) {
|
|
415
|
-
return [finding("plugin.submission.asset.decode_failed", "Branding asset could not be decoded.", assetEvidence(field,
|
|
434
|
+
return [finding("plugin.submission.asset.decode_failed", "Branding asset could not be decoded.", assetEvidence(field, packagePath, extension))];
|
|
416
435
|
}
|
|
417
436
|
if (decoded.format !== extension) {
|
|
418
|
-
return [finding("plugin.submission.asset.extension_mismatch", "Branding asset extension does not match its content.", assetEvidence(field,
|
|
437
|
+
return [finding("plugin.submission.asset.extension_mismatch", "Branding asset extension does not match its content.", assetEvidence(field, packagePath, decoded.format, decoded.dimensions))];
|
|
419
438
|
}
|
|
420
|
-
const invalidDimensions = dimensionFinding(field,
|
|
439
|
+
const invalidDimensions = dimensionFinding(field, packagePath, decoded.format, decoded.dimensions);
|
|
421
440
|
return invalidDimensions === null ? [] : [invalidDimensions];
|
|
422
441
|
}
|
|
423
|
-
export async function
|
|
424
|
-
const
|
|
425
|
-
const interfaceValues = isRecord(listing) ? listing : {};
|
|
442
|
+
export async function validateSubmissionAssetsFromReader(manifest, reader) {
|
|
443
|
+
const interfaceValues = isRecord(manifest.interface) ? manifest.interface : {};
|
|
426
444
|
const findings = [];
|
|
427
445
|
for (const field of assetFields)
|
|
428
|
-
findings.push(...await validateAsset(
|
|
446
|
+
findings.push(...await validateAsset(reader, field, interfaceValues[field]));
|
|
429
447
|
return { findings };
|
|
430
448
|
}
|
|
449
|
+
export async function validateSubmissionAssets(discoveredPackage) {
|
|
450
|
+
return validateSubmissionAssetsFromReader(discoveredPackage.manifest, createDirectorySubmissionPackageReader(discoveredPackage.rootPath));
|
|
451
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type SubmissionPackageEntryKind = "file" | "directory" | "symlink" | "other";
|
|
2
|
+
export interface SubmissionPackageEntry {
|
|
3
|
+
packagePath: string;
|
|
4
|
+
kind: SubmissionPackageEntryKind;
|
|
5
|
+
resolvedKind: Exclude<SubmissionPackageEntryKind, "symlink"> | null;
|
|
6
|
+
resolvedPackagePath?: string;
|
|
7
|
+
size: number;
|
|
8
|
+
safeResolution: "safe" | "outside" | "unavailable";
|
|
9
|
+
}
|
|
10
|
+
export interface SubmissionPackageReader {
|
|
11
|
+
list(directory: string): Promise<readonly SubmissionPackageEntry[]>;
|
|
12
|
+
stat(packagePath: string): Promise<SubmissionPackageEntry | null>;
|
|
13
|
+
read(packagePath: string, maxBytes: number): Promise<Uint8Array | null>;
|
|
14
|
+
}
|
|
15
|
+
export declare function createDirectorySubmissionPackageReader(rootPath: string): SubmissionPackageReader;
|
|
@@ -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
|
+
}
|
|
@@ -31,4 +31,6 @@ export interface SubmissionPreflightReport {
|
|
|
31
31
|
findings: SubmissionFinding[];
|
|
32
32
|
manualChecklist: SubmissionManualCheck[];
|
|
33
33
|
}
|
|
34
|
+
export type SubmissionTargetType = SubmissionPreflightReport["targetType"];
|
|
35
|
+
export declare function validateSubmissionListing(manifest: Record<string, unknown>, targetType: SubmissionTargetType): SubmissionFinding[];
|
|
34
36
|
export declare function buildSubmissionPreflight(targetPath: string): Promise<SubmissionPreflightReport>;
|
|
@@ -106,7 +106,7 @@ function isValidHttpsUrl(value) {
|
|
|
106
106
|
return false;
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
|
-
function
|
|
109
|
+
export function validateSubmissionListing(manifest, targetType) {
|
|
110
110
|
const findings = [];
|
|
111
111
|
validatePackage(manifest, findings);
|
|
112
112
|
const listing = manifest.interface;
|
|
@@ -253,7 +253,7 @@ export async function buildSubmissionPreflight(targetPath) {
|
|
|
253
253
|
const targetType = manifest.mcpServers !== undefined || manifest.apps !== undefined
|
|
254
254
|
? "mcp-backed"
|
|
255
255
|
: "skills-only";
|
|
256
|
-
const listingFindings =
|
|
256
|
+
const listingFindings = validateSubmissionListing(manifest, targetType);
|
|
257
257
|
const componentFindings = await validateApp(discovered.rootPath, manifest.apps);
|
|
258
258
|
const assetFindings = (await validateSubmissionAssets(discovered)).findings;
|
|
259
259
|
const skillFindings = (await validateSubmissionSkillMetadata(discovered, targetType)).findings;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import type { DiscoveredPackage } from "../domain/types.js";
|
|
2
|
-
import type
|
|
3
|
-
type
|
|
1
|
+
import type { DiscoveredPackage, PluginManifest } from "../domain/types.js";
|
|
2
|
+
import { type SubmissionPackageReader } from "./submission-package-reader.js";
|
|
3
|
+
import type { SubmissionFinding, SubmissionPreflightReport } from "./submission-preflight.js";
|
|
4
|
+
type TargetType = SubmissionPreflightReport["targetType"];
|
|
4
5
|
export interface SubmissionSkillMetadataResult {
|
|
5
6
|
findings: SubmissionFinding[];
|
|
6
7
|
skillCount: number;
|
|
7
8
|
}
|
|
9
|
+
export declare function validateSubmissionSkillMetadataFromReader(manifest: PluginManifest, targetType: SubmissionPreflightReport["targetType"], reader: SubmissionPackageReader): Promise<SubmissionSkillMetadataResult>;
|
|
8
10
|
export declare function validateSubmissionSkillMetadata(discoveredPackage: DiscoveredPackage, targetType: TargetType): Promise<SubmissionSkillMetadataResult>;
|
|
9
11
|
export {};
|