@moldesk/registry 0.0.1 → 0.0.3

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/dist/schema.js ADDED
@@ -0,0 +1,337 @@
1
+ import { z } from "zod";
2
+ const kebabCase = z
3
+ .string()
4
+ .min(1)
5
+ .regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "must be lowercase kebab-case");
6
+ const sha256Hex = z
7
+ .string()
8
+ .regex(/^[0-9a-fA-F]{64}$/, "must be a 64-char hex SHA-256");
9
+ const dockerDigest = z
10
+ .string()
11
+ .regex(/^sha256:[0-9a-fA-F]{64}$/, "must be `sha256:<64 hex>`");
12
+ const httpsUrl = z.string().url("must be a valid URL").refine((v) => v.startsWith("https://"), "must use https protocol");
13
+ const safeVersionSegment = z
14
+ .string()
15
+ .min(1)
16
+ .regex(/^[A-Za-z0-9][A-Za-z0-9._+-]*$/, "must be a safe single path segment");
17
+ const semver = z
18
+ .string()
19
+ .regex(/^(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-]+)*)?$/, "must be a semantic version");
20
+ function isSafeRelativeTarget(target) {
21
+ if (!target || target.length === 0)
22
+ return false;
23
+ if (target.includes("\\"))
24
+ return false;
25
+ if (target.startsWith("/") || /^[A-Za-z]:/.test(target))
26
+ return false;
27
+ const parts = target.split("/");
28
+ for (const part of parts) {
29
+ if (part === "" || part === "." || part === "..")
30
+ return false;
31
+ }
32
+ return true;
33
+ }
34
+ function isSafeRelativeGlob(glob) {
35
+ if (!glob || glob.includes("\\") || glob.startsWith("/") || /^[A-Za-z]:/.test(glob))
36
+ return false;
37
+ return glob.split("/").every((part) => part !== "" && part !== "." && part !== "..");
38
+ }
39
+ const safeTarget = z
40
+ .string()
41
+ .min(1)
42
+ .refine(isSafeRelativeTarget, "must be a safe path relative to the model assets directory (no absolute paths, .., or backslashes)");
43
+ const modelStatus = z.enum(["available", "beta", "planned"]);
44
+ const runtimeKind = z.enum(["python", "docker"]);
45
+ const requirementLevel = z.enum(["required", "recommended", "unsupported"]);
46
+ const category = z.enum(["sequence-design", "structure-prediction", "docking", "other"]);
47
+ const platformId = z.enum(["darwin-arm64", "linux-x64"]);
48
+ /** Closed enum of named, non-shell post-install fixups. Never a free-form command
49
+ * string — this keeps the manifest from becoming a generic shell escape hatch. */
50
+ const postInstallHookSchema = z.enum(["boltz-fix-macos-libomp"]);
51
+ const cudaSchema = z
52
+ .object({
53
+ level: requirementLevel,
54
+ // Minimum driver-supported CUDA version, e.g. "12.1" — compared against the
55
+ // driver's max-supported CUDA (not the local CUDA toolkit) as a major.minor
56
+ // floor, not a semver range.
57
+ minDriverCudaVersion: z
58
+ .string()
59
+ .regex(/^\d+\.\d+$/, "must be a major.minor CUDA version, e.g. \"12.1\"")
60
+ .optional(),
61
+ })
62
+ .strict();
63
+ const sourceSchema = z
64
+ .object({
65
+ repository: httpsUrl,
66
+ revision: z
67
+ .string()
68
+ .regex(/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/i, "must be a full immutable commit digest"),
69
+ })
70
+ .strict();
71
+ const pythonRequirementSchema = z
72
+ .object({
73
+ name: z.string().min(1),
74
+ version: z.string().min(1).optional(),
75
+ source: z.string().min(1).optional(),
76
+ revision: z.string().min(1).optional(),
77
+ // Optional PEP 508 extras, e.g. ["cuda"] for `boltz[cuda]`. Valid with either a
78
+ // PyPI version pin or a git source+revision pin (pip/uv both support
79
+ // `name[extra] @ git+url@rev`).
80
+ extras: z.array(z.string().min(1)).min(1).optional(),
81
+ // sha256 hex digests of the expected wheel/sdist artifact(s), enforced natively
82
+ // by uv/pip's hash-checking install mode when present.
83
+ hashes: z.array(sha256Hex).min(1).optional(),
84
+ })
85
+ .strict();
86
+ const pythonRuntimeSchema = z
87
+ .object({
88
+ kind: z.literal("python"),
89
+ python: z.string().min(1),
90
+ installer: z.literal("uv"),
91
+ estimatedDownloadBytes: z.number().int().nonnegative().optional(),
92
+ estimatedDiskBytes: z.number().int().nonnegative().optional(),
93
+ requirements: z.array(pythonRequirementSchema).min(1),
94
+ // When present, this runtime entry only applies to hosts matching one of these
95
+ // platforms; absent means it applies unconditionally (legacy single-runtime
96
+ // manifests, e.g. ProteinMPNN/LigandMPNN, always match this way).
97
+ platforms: z.array(platformId).min(1).optional(),
98
+ // Per-entry source override, falling back to the manifest-level `source` when
99
+ // absent — needed when different platforms build from genuinely different repos.
100
+ source: sourceSchema.optional(),
101
+ postInstall: z.array(postInstallHookSchema).min(1).optional(),
102
+ // Per-platform-runtime-entry accelerator requirements, same shape as the
103
+ // manifest-level `hardware.nvidiaGpu`/`hardware.cuda`/`hardware.minVramGb`, so a
104
+ // single manifest can declare CUDA on one platform and MPS (no CUDA) on another.
105
+ nvidiaGpu: requirementLevel.optional(),
106
+ cuda: cudaSchema.optional(),
107
+ minVramGb: z.number().positive().optional(),
108
+ // Additional PEP-503-style pip/uv package indexes, ADDED alongside the default
109
+ // PyPI index (never replacing it — a manifest never loses access to ordinary
110
+ // packages like pandas/scipy just because it needs a CUDA-specific extra
111
+ // index). Maps to `UV_EXTRA_INDEX_URL`. For a pinned build whose exact wheels
112
+ // (e.g. a CUDA-specific PyTorch build) live on a PEP-503-compatible extra
113
+ // index rather than default PyPI.
114
+ extraIndexUrls: z.array(httpsUrl).min(1).max(4).optional(),
115
+ // Flat `--find-links` wheel-listing pages (not PEP-503 indexes — a single page
116
+ // listing wheel files directly, e.g. PyTorch Geometric's CUDA extension wheel
117
+ // pages). Maps to `UV_FIND_LINKS`. Kept structurally distinct from
118
+ // `extraIndexUrls` because the two have different pip/uv semantics and mixing
119
+ // them up silently breaks resolution (this is exactly what an earlier version
120
+ // of this schema got wrong for DiffDock-L).
121
+ findLinks: z.array(httpsUrl).min(1).max(4).optional(),
122
+ // Package names (matching a `requirements[].name`) that must resolve to a
123
+ // prebuilt wheel and must never be built from source, mapped to `--only-binary
124
+ // <name>` on every install invocation. Exists so an ABI-sensitive CUDA
125
+ // extension package (torch-scatter, torch-sparse, …) can never silently fall
126
+ // back to a from-source sdist build if wheel resolution ever fails for some
127
+ // reason — install fails closed instead.
128
+ binaryOnly: z.array(z.string().min(1)).min(1).optional(),
129
+ // Requirements installed first, each in its own `uv pip install` invocation, in
130
+ // array order, before anything in `requirements` — for a package (e.g. a pinned
131
+ // `setuptools` version) that a later requirement's own source build depends on
132
+ // being fully resolved beforehand, not just present somewhere in the same
133
+ // resolver graph.
134
+ preInstall: z.array(pythonRequirementSchema).min(1).optional(),
135
+ // Names (matching `requirements[].name`) deferred to their own final install
136
+ // invocation, strictly after every other requirement (including `preInstall`)
137
+ // has finished installing — for a package whose own build process imports an
138
+ // earlier dependency (e.g. an unpackaged git build that imports `torch` at
139
+ // setup time to select CUDA architecture flags), which a single combined
140
+ // resolver invocation cannot reliably sequence.
141
+ buildAfter: z.array(z.string().min(1)).min(1).optional(),
142
+ // PyTorch build selection for uv's native `--torch-backend` (mapped to
143
+ // `UV_TORCH_BACKEND`), which routes torch-ecosystem packages to the matching
144
+ // official PyTorch index. A closed enum, never a free-form index URL or pip
145
+ // argument fragment — extend it only alongside a manifest that actually pins
146
+ // and verifies that backend's wheels.
147
+ torchBackend: z.enum(["cpu", "cu126"]).optional(),
148
+ })
149
+ .strict();
150
+ const dockerRuntimeSchema = z
151
+ .object({
152
+ kind: z.literal("docker"),
153
+ image: z.string().min(1),
154
+ digest: dockerDigest,
155
+ gpu: z.enum(["none", "optional", "required"]),
156
+ estimatedDownloadBytes: z.number().int().nonnegative().optional(),
157
+ estimatedDiskBytes: z.number().int().nonnegative().optional(),
158
+ })
159
+ .strict();
160
+ const runtimeSpecSchema = z.discriminatedUnion("kind", [
161
+ pythonRuntimeSchema,
162
+ dockerRuntimeSchema,
163
+ ]);
164
+ const hardwareSchema = z
165
+ .object({
166
+ platforms: z.array(platformId).min(1).optional(),
167
+ minRamGb: z.number().positive().optional(),
168
+ minDiskGb: z.number().positive().optional(),
169
+ nvidiaGpu: requirementLevel.optional(),
170
+ minVramGb: z.number().positive().optional(),
171
+ cuda: cudaSchema.optional(),
172
+ })
173
+ .strict();
174
+ const assetSchema = z
175
+ .object({
176
+ id: z.string().min(1),
177
+ url: httpsUrl,
178
+ source: z.string().min(1).optional(),
179
+ license: z.string().min(1).optional(),
180
+ sha256: sha256Hex,
181
+ target: safeTarget,
182
+ sizeBytes: z.number().int().nonnegative().optional(),
183
+ archive: z.enum(["none", "tar.gz", "tar", "zip"]).optional(),
184
+ })
185
+ .strict();
186
+ const inputSpecSchema = z
187
+ .object({
188
+ formats: z.array(z.string().min(1)).min(1),
189
+ required: z.boolean(),
190
+ })
191
+ .strict();
192
+ const outputSpecSchema = z
193
+ .object({
194
+ id: z.string().min(1),
195
+ glob: z.string().min(1),
196
+ required: z.boolean(),
197
+ })
198
+ .strict()
199
+ .refine((v) => isSafeRelativeGlob(v.glob), {
200
+ message: "must be a safe path relative to the staged output directory",
201
+ path: ["glob"],
202
+ });
203
+ const adapterVerificationSchema = z
204
+ .object({
205
+ lastVerifiedAt: z.string().datetime({ offset: true }),
206
+ platforms: z.array(z.string().min(1)).min(1),
207
+ notes: z.string().optional(),
208
+ })
209
+ .strict();
210
+ export const allowedCommandTokens = ["{{input}}", "{{outputDir}}", "{{modelDir}}", "{{assetsDir}}"];
211
+ const allowedCommandTokenSet = new Set(allowedCommandTokens);
212
+ function hasOnlyAllowedCommandTokens(value) {
213
+ let remainder = value;
214
+ for (const token of allowedCommandTokenSet)
215
+ remainder = remainder.split(token).join("");
216
+ return !remainder.includes("{{") && !remainder.includes("}}");
217
+ }
218
+ const commandValue = z
219
+ .string()
220
+ .refine((v) => !v.includes("\0"), "must not contain a NUL byte")
221
+ .refine(hasOnlyAllowedCommandTokens, "contains an unsupported command template token");
222
+ export const commandSpecSchema = z
223
+ .object({
224
+ executable: z
225
+ .string()
226
+ .min(1)
227
+ .refine((v) => !v.includes("\0"), "must not contain a NUL byte")
228
+ .refine((v) => !v.includes("{{"), "executable must not contain template tokens"),
229
+ args: z.array(commandValue),
230
+ env: z.record(z.string().min(1), commandValue).optional(),
231
+ })
232
+ .strict();
233
+ export const modelManifestV1Schema = z
234
+ .object({
235
+ schemaVersion: z.literal(1),
236
+ name: kebabCase,
237
+ displayName: z.string().min(1),
238
+ modelVersion: safeVersionSegment,
239
+ adapterVersion: semver,
240
+ description: z.string().min(1),
241
+ category,
242
+ status: modelStatus,
243
+ license: z.string().min(1).optional(),
244
+ homepage: httpsUrl.optional(),
245
+ source: sourceSchema.optional(),
246
+ adapterVerification: adapterVerificationSchema.optional(),
247
+ runtimes: z.array(runtimeSpecSchema).min(1),
248
+ hardware: hardwareSchema,
249
+ assets: z.array(assetSchema).optional(),
250
+ input: inputSpecSchema,
251
+ outputs: z.array(outputSpecSchema).min(1),
252
+ })
253
+ .strict()
254
+ .superRefine((val, ctx) => {
255
+ if (val.hardware.minVramGb !== undefined && val.hardware.nvidiaGpu === undefined) {
256
+ ctx.addIssue({
257
+ code: z.ZodIssueCode.custom,
258
+ message: "hardware.nvidiaGpu must be set (\"required\" or \"recommended\") whenever minVramGb is specified",
259
+ path: ["hardware", "nvidiaGpu"],
260
+ });
261
+ }
262
+ for (const [runtimeIndex, runtime] of val.runtimes.entries()) {
263
+ if (runtime.kind !== "python")
264
+ continue;
265
+ if (runtime.minVramGb !== undefined && runtime.nvidiaGpu === undefined) {
266
+ ctx.addIssue({
267
+ code: z.ZodIssueCode.custom,
268
+ message: "nvidiaGpu must be set (\"required\" or \"recommended\") whenever this runtime entry's minVramGb is specified",
269
+ path: ["runtimes", runtimeIndex, "nvidiaGpu"],
270
+ });
271
+ }
272
+ if (runtime.buildAfter) {
273
+ const requirementNames = new Set(runtime.requirements.map((r) => r.name));
274
+ for (const deferredName of runtime.buildAfter) {
275
+ if (!requirementNames.has(deferredName)) {
276
+ ctx.addIssue({
277
+ code: z.ZodIssueCode.custom,
278
+ message: `buildAfter references "${deferredName}", which is not in this runtime entry's requirements`,
279
+ path: ["runtimes", runtimeIndex, "buildAfter"],
280
+ });
281
+ }
282
+ }
283
+ }
284
+ }
285
+ const names = new Set();
286
+ for (const out of val.outputs) {
287
+ if (names.has(out.id)) {
288
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: `duplicate output id "${out.id}"`, path: ["outputs"] });
289
+ }
290
+ names.add(out.id);
291
+ }
292
+ if (val.assets) {
293
+ const assetIds = new Set();
294
+ const targets = new Set();
295
+ for (const a of val.assets) {
296
+ if (assetIds.has(a.id)) {
297
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: `duplicate asset id "${a.id}"`, path: ["assets"] });
298
+ }
299
+ assetIds.add(a.id);
300
+ if (targets.has(a.target)) {
301
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: `duplicate asset target "${a.target}"`, path: ["assets"] });
302
+ }
303
+ targets.add(a.target);
304
+ }
305
+ }
306
+ if (val.status === "available") {
307
+ if (!val.source) {
308
+ ctx.addIssue({
309
+ code: z.ZodIssueCode.custom,
310
+ message: "available models require source provenance",
311
+ path: ["source"],
312
+ });
313
+ }
314
+ if (!val.adapterVerification) {
315
+ ctx.addIssue({
316
+ code: z.ZodIssueCode.custom,
317
+ message: "available models require adapter verification metadata",
318
+ path: ["adapterVerification"],
319
+ });
320
+ }
321
+ for (const [runtimeIndex, runtime] of val.runtimes.entries()) {
322
+ if (runtime.kind !== "python")
323
+ continue;
324
+ for (const [requirementIndex, requirement] of runtime.requirements.entries()) {
325
+ const pinnedPackage = requirement.version !== undefined;
326
+ const pinnedSource = requirement.source !== undefined && requirement.revision !== undefined;
327
+ if (!pinnedPackage && !pinnedSource) {
328
+ ctx.addIssue({
329
+ code: z.ZodIssueCode.custom,
330
+ message: "available models require every Python dependency to be pinned by version or source revision",
331
+ path: ["runtimes", runtimeIndex, "requirements", requirementIndex],
332
+ });
333
+ }
334
+ }
335
+ }
336
+ }
337
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,335 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { MoldeskError } from "./errors.js";
3
+ import { parseManifest } from "./loader.js";
4
+ import { commandSpecSchema } from "./schema.js";
5
+ function baseManifest() {
6
+ return {
7
+ schemaVersion: 1,
8
+ name: "test-model",
9
+ displayName: "Test Model",
10
+ modelVersion: "1.0.0",
11
+ adapterVersion: "0.1.0",
12
+ description: "A test model",
13
+ category: "other",
14
+ status: "planned",
15
+ runtimes: [
16
+ { kind: "python", python: "3.11", installer: "uv", requirements: [{ name: "torch" }] },
17
+ ],
18
+ hardware: { platforms: ["darwin-arm64"] },
19
+ input: { formats: [".pdb"], required: true },
20
+ outputs: [{ id: "out", glob: "*.fa", required: true }],
21
+ };
22
+ }
23
+ describe("parseManifest", () => {
24
+ it("accepts a minimal valid v1 manifest", () => {
25
+ const m = parseManifest(baseManifest(), "test/manifest.yaml");
26
+ expect(m.name).toBe("test-model");
27
+ expect(m.modelVersion).toBe("1.0.0");
28
+ });
29
+ it("rejects unknown schema versions with a typed error", () => {
30
+ try {
31
+ parseManifest({ ...baseManifest(), schemaVersion: 99 }, "test/manifest.yaml");
32
+ expect.unreachable();
33
+ }
34
+ catch (error) {
35
+ expect(error).toBeInstanceOf(MoldeskError);
36
+ const e = error;
37
+ expect(e.code).toBe("UNKNOWN_SCHEMA_VERSION");
38
+ expect(e.message).toContain("test/manifest.yaml");
39
+ expect(e.remediation).toMatch(/schemaVersion/i);
40
+ }
41
+ });
42
+ it("rejects legacy top-level version without modelVersion", () => {
43
+ const raw = baseManifest();
44
+ delete raw["modelVersion"];
45
+ raw["version"] = "1.0.0";
46
+ try {
47
+ parseManifest(raw, "models/x/manifest.yaml");
48
+ expect.unreachable();
49
+ }
50
+ catch (error) {
51
+ const e = error;
52
+ expect(e.code).toBe("INVALID_MANIFEST");
53
+ expect(e.message).toContain("modelVersion");
54
+ expect(e.details?.["field"]).toBe("version");
55
+ }
56
+ });
57
+ it.each(["modelVersion", "adapterVersion"])("rejects missing %s", (field) => {
58
+ const raw = baseManifest();
59
+ delete raw[field];
60
+ expect(() => parseManifest(raw, "m.yaml")).toThrowError(MoldeskError);
61
+ });
62
+ it("rejects unsafe model versions and invalid adapter semver", () => {
63
+ expect(() => parseManifest({ ...baseManifest(), modelVersion: "../../escape" }, "m.yaml")).toThrowError(MoldeskError);
64
+ expect(() => parseManifest({ ...baseManifest(), adapterVersion: "latest" }, "m.yaml")).toThrowError(MoldeskError);
65
+ });
66
+ it("rejects unsafe asset target paths", () => {
67
+ for (const target of ["../evil.bin", "/abs/path.bin", "a\\b.bin", "a/./b.bin"]) {
68
+ const raw = baseManifest();
69
+ raw["assets"] = [
70
+ { id: "w", url: "https://example.com/w.bin", sha256: "a".repeat(64), target },
71
+ ];
72
+ try {
73
+ parseManifest(raw, "m.yaml");
74
+ expect.unreachable(`target ${target} should fail`);
75
+ }
76
+ catch (error) {
77
+ expect(error.code).toBe("INVALID_MANIFEST");
78
+ }
79
+ }
80
+ });
81
+ it("rejects duplicate model-relevant ids conservatively", () => {
82
+ const raw = baseManifest();
83
+ raw["outputs"] = [
84
+ { id: "dup", glob: "*.a", required: true },
85
+ { id: "dup", glob: "*.b", required: false },
86
+ ];
87
+ expect(() => parseManifest(raw, "m.yaml")).toThrowError(MoldeskError);
88
+ });
89
+ it("rejects unknown keys in high-risk runtime sections", () => {
90
+ const raw = baseManifest();
91
+ raw["runtimes"] = [
92
+ { kind: "python", python: "3.11", installer: "uv", requirements: [{ name: "torch" }], installScript: "curl | sh" },
93
+ ];
94
+ try {
95
+ parseManifest(raw, "m.yaml");
96
+ expect.unreachable();
97
+ }
98
+ catch (error) {
99
+ const e = error;
100
+ expect(e.code).toBe("INVALID_MANIFEST");
101
+ expect(e.message).toContain("m.yaml");
102
+ }
103
+ });
104
+ it("accepts only known command tokens without applying shell restrictions", () => {
105
+ const valid = {
106
+ executable: "/Applications/MoleculeDesk (Preview)/python#1",
107
+ args: ["--input={{input}}", "{{outputDir}}"],
108
+ env: { MODEL_DIR: "{{modelDir}}" },
109
+ };
110
+ expect(commandSpecSchema.parse(valid)).toEqual(valid);
111
+ expect(() => commandSpecSchema.parse({ executable: "tool", args: ["{{unknown}}"] })).toThrow();
112
+ expect(() => commandSpecSchema.parse({ executable: "tool", args: ["{{input"] })).toThrow();
113
+ expect(() => commandSpecSchema.parse({ executable: "{{modelDir}}/tool", args: [] })).toThrow();
114
+ });
115
+ it("validates output globs by path segment", () => {
116
+ expect(() => parseManifest({ ...baseManifest(), outputs: [{ id: "out", glob: "foo..bar/*.cif", required: true }] }, "m.yaml")).not.toThrow();
117
+ for (const glob of ["../*.cif", "/tmp/*.cif", "dir\\*.cif", "dir//*.cif"]) {
118
+ expect(() => parseManifest({ ...baseManifest(), outputs: [{ id: "out", glob, required: true }] }, "m.yaml")).toThrowError(MoldeskError);
119
+ }
120
+ });
121
+ it("requires provenance, verification, and pinned dependencies for available models", () => {
122
+ expect(() => parseManifest({ ...baseManifest(), status: "available" }, "m.yaml")).toThrowError(MoldeskError);
123
+ const available = baseManifest();
124
+ available["status"] = "available";
125
+ available["source"] = {
126
+ repository: "https://example.com/model.git",
127
+ revision: "a".repeat(40),
128
+ };
129
+ available["adapterVerification"] = {
130
+ lastVerifiedAt: "2026-09-07T00:00:00Z",
131
+ platforms: ["darwin-arm64"],
132
+ };
133
+ available["runtimes"] = [
134
+ { kind: "python", python: "3.11", installer: "uv", requirements: [{ name: "torch", version: "2.2.1" }] },
135
+ ];
136
+ expect(() => parseManifest(available, "m.yaml")).not.toThrow();
137
+ });
138
+ it("requires nvidiaGpu whenever minVramGb is set", () => {
139
+ const raw = baseManifest();
140
+ raw["hardware"] = { platforms: ["darwin-arm64"], minVramGb: 16 };
141
+ expect(() => parseManifest(raw, "m.yaml")).toThrowError(MoldeskError);
142
+ raw["hardware"] = { platforms: ["darwin-arm64"], minVramGb: 16, nvidiaGpu: "recommended" };
143
+ expect(() => parseManifest(raw, "m.yaml")).not.toThrow();
144
+ });
145
+ it("validates cuda.minDriverCudaVersion as major.minor", () => {
146
+ const raw = baseManifest();
147
+ raw["hardware"] = {
148
+ platforms: ["darwin-arm64"],
149
+ nvidiaGpu: "required",
150
+ cuda: { level: "required", minDriverCudaVersion: "12.1" },
151
+ };
152
+ expect(() => parseManifest(raw, "m.yaml")).not.toThrow();
153
+ raw["hardware"] = {
154
+ platforms: ["darwin-arm64"],
155
+ nvidiaGpu: "required",
156
+ cuda: { level: "required", minDriverCudaVersion: "12.1.0" },
157
+ };
158
+ expect(() => parseManifest(raw, "m.yaml")).toThrowError(MoldeskError);
159
+ });
160
+ it("accepts per-platform python runtime fields: platforms, source override, postInstall, accelerator, hashes", () => {
161
+ const raw = baseManifest();
162
+ raw["runtimes"] = [
163
+ {
164
+ kind: "python",
165
+ python: "3.11",
166
+ installer: "uv",
167
+ platforms: ["darwin-arm64"],
168
+ source: { repository: "https://github.com/example/darwin-fork", revision: "a".repeat(40) },
169
+ postInstall: ["boltz-fix-macos-libomp"],
170
+ nvidiaGpu: "unsupported",
171
+ requirements: [{ name: "torch", version: "2.6.0", hashes: ["b".repeat(64)] }],
172
+ },
173
+ {
174
+ kind: "python",
175
+ python: "3.11",
176
+ installer: "uv",
177
+ platforms: ["linux-x64"],
178
+ source: { repository: "https://github.com/example/official", revision: "c".repeat(40) },
179
+ nvidiaGpu: "required",
180
+ cuda: { level: "required", minDriverCudaVersion: "12.1" },
181
+ minVramGb: 16,
182
+ requirements: [{ name: "torch", version: "2.2.1" }],
183
+ },
184
+ ];
185
+ expect(() => parseManifest(raw, "m.yaml")).not.toThrow();
186
+ const parsed = parseManifest(raw, "m.yaml");
187
+ expect(parsed.runtimes).toHaveLength(2);
188
+ });
189
+ it("rejects an unlisted postInstall value", () => {
190
+ const raw = baseManifest();
191
+ raw["runtimes"] = [
192
+ {
193
+ kind: "python",
194
+ python: "3.11",
195
+ installer: "uv",
196
+ postInstall: ["curl-pipe-sh"],
197
+ requirements: [{ name: "torch" }],
198
+ },
199
+ ];
200
+ expect(() => parseManifest(raw, "m.yaml")).toThrowError(MoldeskError);
201
+ });
202
+ it("still rejects unknown keys on a python runtime entry (.strict())", () => {
203
+ const raw = baseManifest();
204
+ raw["runtimes"] = [
205
+ {
206
+ kind: "python",
207
+ python: "3.11",
208
+ installer: "uv",
209
+ requirements: [{ name: "torch" }],
210
+ platforms: ["darwin-arm64"],
211
+ extraField: "not allowed",
212
+ },
213
+ ];
214
+ expect(() => parseManifest(raw, "m.yaml")).toThrowError(MoldeskError);
215
+ });
216
+ it("accepts a python runtime entry's extraIndexUrls and findLinks (kept structurally distinct)", () => {
217
+ const raw = baseManifest();
218
+ raw["runtimes"] = [
219
+ {
220
+ kind: "python",
221
+ python: "3.10",
222
+ installer: "uv",
223
+ extraIndexUrls: ["https://download.pytorch.org/whl/cu117"],
224
+ findLinks: ["https://data.pyg.org/whl/torch-1.13.1+cu117.html"],
225
+ binaryOnly: ["torch", "torch-scatter"],
226
+ requirements: [{ name: "torch", version: "1.13.1+cu117", hashes: ["b".repeat(64)] }],
227
+ },
228
+ ];
229
+ const parsed = parseManifest(raw, "m.yaml");
230
+ expect(parsed.runtimes[0]).toMatchObject({
231
+ extraIndexUrls: ["https://download.pytorch.org/whl/cu117"],
232
+ findLinks: ["https://data.pyg.org/whl/torch-1.13.1+cu117.html"],
233
+ binaryOnly: ["torch", "torch-scatter"],
234
+ });
235
+ });
236
+ it("rejects a non-https extraIndexUrls/findLinks URL", () => {
237
+ const raw = baseManifest();
238
+ raw["runtimes"] = [
239
+ { kind: "python", python: "3.11", installer: "uv", extraIndexUrls: ["http://insecure.example/whl"], requirements: [{ name: "torch" }] },
240
+ ];
241
+ expect(() => parseManifest(raw, "m.yaml")).toThrowError(MoldeskError);
242
+ const raw2 = baseManifest();
243
+ raw2["runtimes"] = [
244
+ { kind: "python", python: "3.11", installer: "uv", findLinks: ["http://insecure.example/whl"], requirements: [{ name: "torch" }] },
245
+ ];
246
+ expect(() => parseManifest(raw2, "m.yaml")).toThrowError(MoldeskError);
247
+ });
248
+ it("accepts a known torchBackend and rejects anything outside the closed enum", () => {
249
+ const raw = baseManifest();
250
+ raw["runtimes"] = [
251
+ { kind: "python", python: "3.11", installer: "uv", torchBackend: "cu126", requirements: [{ name: "torch", version: "2.7.1+cu126" }] },
252
+ ];
253
+ expect(parseManifest(raw, "m.yaml").runtimes[0]).toMatchObject({ torchBackend: "cu126" });
254
+ for (const bad of ["auto", "cu999", "--index-url https://evil.example", "https://download.pytorch.org/whl/cu126", ""]) {
255
+ const rawBad = baseManifest();
256
+ rawBad["runtimes"] = [
257
+ { kind: "python", python: "3.11", installer: "uv", torchBackend: bad, requirements: [{ name: "torch" }] },
258
+ ];
259
+ expect(() => parseManifest(rawBad, "m.yaml")).toThrowError(MoldeskError);
260
+ }
261
+ });
262
+ it("rejects an empty extraIndexUrls array", () => {
263
+ const raw = baseManifest();
264
+ raw["runtimes"] = [
265
+ { kind: "python", python: "3.11", installer: "uv", extraIndexUrls: [], requirements: [{ name: "torch" }] },
266
+ ];
267
+ expect(() => parseManifest(raw, "m.yaml")).toThrowError(MoldeskError);
268
+ });
269
+ it("accepts preInstall requirements and a buildAfter list referencing a real requirement", () => {
270
+ const raw = baseManifest();
271
+ raw["runtimes"] = [
272
+ {
273
+ kind: "python",
274
+ python: "3.10",
275
+ installer: "uv",
276
+ preInstall: [{ name: "setuptools", version: "69.5.1" }],
277
+ buildAfter: ["openfold"],
278
+ requirements: [
279
+ { name: "torch", version: "1.13.1+cu117" },
280
+ { name: "openfold", source: "https://github.com/aqlaboratory/openfold", revision: "a".repeat(40) },
281
+ ],
282
+ },
283
+ ];
284
+ const parsed = parseManifest(raw, "m.yaml");
285
+ expect(parsed.runtimes[0]).toMatchObject({ buildAfter: ["openfold"] });
286
+ });
287
+ it("rejects a buildAfter entry that does not match any requirement name", () => {
288
+ const raw = baseManifest();
289
+ raw["runtimes"] = [
290
+ {
291
+ kind: "python",
292
+ python: "3.10",
293
+ installer: "uv",
294
+ buildAfter: ["not-a-real-requirement"],
295
+ requirements: [{ name: "torch", version: "1.13.1+cu117" }],
296
+ },
297
+ ];
298
+ expect(() => parseManifest(raw, "m.yaml")).toThrowError(MoldeskError);
299
+ });
300
+ it("rejects a python requirement with an invalid hash shape", () => {
301
+ const raw = baseManifest();
302
+ raw["runtimes"] = [
303
+ { kind: "python", python: "3.11", installer: "uv", requirements: [{ name: "torch", hashes: ["not-a-hash"] }] },
304
+ ];
305
+ expect(() => parseManifest(raw, "m.yaml")).toThrowError(MoldeskError);
306
+ });
307
+ it("requires nvidiaGpu on a runtime entry whenever that entry sets minVramGb", () => {
308
+ const raw = baseManifest();
309
+ raw["runtimes"] = [
310
+ { kind: "python", python: "3.11", installer: "uv", minVramGb: 16, requirements: [{ name: "torch" }] },
311
+ ];
312
+ expect(() => parseManifest(raw, "m.yaml")).toThrowError(MoldeskError);
313
+ raw["runtimes"] = [
314
+ { kind: "python", python: "3.11", installer: "uv", minVramGb: 16, nvidiaGpu: "required", requirements: [{ name: "torch" }] },
315
+ ];
316
+ expect(() => parseManifest(raw, "m.yaml")).not.toThrow();
317
+ });
318
+ it("existing single-runtime manifests with no platforms field still validate unmodified", () => {
319
+ const m = parseManifest(baseManifest(), "test/manifest.yaml");
320
+ expect(m.runtimes).toHaveLength(1);
321
+ expect(m.runtimes[0].platforms).toBeUndefined();
322
+ });
323
+ it("corrupt fixtures name manifest, field, and remediation", () => {
324
+ try {
325
+ parseManifest({ schemaVersion: 1, name: "Bad Name!" }, "models/bad/manifest.yaml");
326
+ expect.unreachable();
327
+ }
328
+ catch (error) {
329
+ const e = error;
330
+ expect(e.message).toContain("models/bad/manifest.yaml");
331
+ expect(e.remediation).toBeTruthy();
332
+ expect(e.details?.["manifestPath"]).toBe("models/bad/manifest.yaml");
333
+ }
334
+ });
335
+ });