Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 31 additions & 20 deletions .github/workflows/refresh-runtime-feed.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,18 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.REFRESH_TOKEN || github.token }}
run: |
for channel in stable dev; do
node scripts/runtime-feed-generate.mjs \
--channel "$channel" \
--previous "v2/runtime/${channel}/runtime-release-feed.signed.json" \
--out ".runtime-feed/${channel}.candidate.json" \
--bundles-out ".runtime-feed/${channel}.bundles.json" \
--bundle-dir ".runtime-feed/bundles"
# v2 is frozen for Runtime 0.9.1 and earlier. v3 is the same feed
# plus runtimes that only Runtime 0.10 and later understand.
for version in 2 3; do
for channel in stable dev; do
node scripts/runtime-feed-generate.mjs \
--feed-version "$version" \
--channel "$channel" \
--previous "v${version}/runtime/${channel}/runtime-release-feed.signed.json" \
--out ".runtime-feed/v${version}-${channel}.candidate.json" \
--bundles-out ".runtime-feed/v${version}-${channel}.bundles.json" \
--bundle-dir ".runtime-feed/bundles"
done
done

- name: Publish new immutable bundles
Expand All @@ -55,28 +60,34 @@ jobs:
MSTY_NEXUS_RUNTIME_RELEASE_FEED_KEY_ID: ${{ vars.MSTY_NEXUS_RUNTIME_RELEASE_FEED_KEY_ID || 'msty-runtime-feed-1' }}
run: |
test -n "$MSTY_NEXUS_RUNTIME_RELEASE_FEED_PRIVATE_KEY"
for channel in stable dev; do
candidate=".runtime-feed/${channel}.candidate.json"
if [[ ! -f "$candidate" ]]; then
continue
fi
output="v2/runtime/${channel}/runtime-release-feed.signed.json"
mkdir -p "$(dirname "$output")"
node scripts/runtime-feed-sign.mjs \
--input "$candidate" \
--out "$output" \
--key-id "$MSTY_NEXUS_RUNTIME_RELEASE_FEED_KEY_ID"
node scripts/runtime-feed-verify.mjs "$output" "$channel"
for version in 2 3; do
for channel in stable dev; do
candidate=".runtime-feed/v${version}-${channel}.candidate.json"
if [[ ! -f "$candidate" ]]; then
continue
fi
output="v${version}/runtime/${channel}/runtime-release-feed.signed.json"
mkdir -p "$(dirname "$output")"
node scripts/runtime-feed-sign.mjs \
--feed-version "$version" \
--input "$candidate" \
--out "$output" \
--key-id "$MSTY_NEXUS_RUNTIME_RELEASE_FEED_KEY_ID"
node scripts/runtime-feed-verify.mjs "$output" "$channel" "$version"
done
done

- name: Commit changed feeds
run: |
if [[ -z "$(git status --porcelain -- v2/runtime)" ]]; then
if [[ -z "$(git status --porcelain -- v2/runtime v3/runtime)" ]]; then
echo "nothing to publish"
exit 0
fi
git config user.name "runtime-catalog[bot]"
git config user.email "noreply@cloudstack.llc"
git add v2/runtime
if [[ -d v3/runtime ]]; then
git add v3/runtime
fi
git commit -m "chore: refresh runtime release feeds"
git push
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ https://raw.githubusercontent.com/cloudstack-llc/model-catalog/main/v1/smart-rou
| `v1/featured-models.json` | Curated local models worth downloading, grouped into collections | Curated; resolved against Ollama and Hugging Face | Daily |
| `v1/smart-route-starters.json` | Model-neutral Smart Route starters, grouped by use | Curated | On change |
| `v2/runtime/{stable,dev}/runtime-release-feed.signed.json` | Signed Msty Nexus Runtime releases | Ollama, llama.cpp, and Msty MLX metadata | 4 hours |
| `v3/runtime/{stable,dev}/runtime-release-feed.signed.json` | The same feed for Runtime 0.10 and later, plus newer runtimes | Same as `v2`, plus the Msty image runtime | 4 hours |

# Token pricing

Expand Down Expand Up @@ -82,6 +83,19 @@ https://raw.githubusercontent.com/cloudstack-llc/catalogs/main/v2/runtime/stable
https://raw.githubusercontent.com/cloudstack-llc/catalogs/main/v2/runtime/dev/runtime-release-feed.signed.json
```

The same feed is published at two paths, `v2/runtime` and `v3/runtime`, with
the same schema and signing key.

- `v2` is read by Msty Nexus Runtime 0.9.1 and earlier. Those versions reject
the whole feed when it has a runtime, a field, a backend or an archive
format they do not know. So `v2` never changes shape. It gets only new
versions of the runtimes it lists today.
- `v3` is read by Runtime 0.10 and later, which skip entries they do not
know. It has everything in `v2`, plus the Msty image runtime (`images`)
once that runtime is published.
- Rule: new runtimes, fields and artifact kinds go to `v3` only. The `v2`
generator and verifier refuse any other runtime.

They are not mirrored to R2. The signature is the trust boundary; the hosting
origin supplies bytes but cannot authorize a runtime artifact.

Expand Down
8 changes: 6 additions & 2 deletions scripts/runtime-feed-generate.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { fileURLToPath } from "node:url";
import {
buildRuntimeFeed,
createGitHubClient,
parseFeedVersion,
serializeFeed,
} from "./runtime-feed-lib.mjs";

Expand Down Expand Up @@ -112,6 +113,9 @@ async function previousFeed(path) {
async function main() {
const argv = process.argv.slice(2);
const channel = required(option(argv, "--channel", "stable"), "--channel");
// 2 (default) is the frozen feed for Runtime 0.9.1 and earlier; 3 may also
// list newer runtimes. See FEED_VERSIONS in runtime-feed-lib.mjs.
const feedVersion = parseFeedVersion(required(option(argv, "--feed-version", "2"), "--feed-version"));
const out = resolve(repoRoot, required(option(argv, "--out"), "--out"));
const bundlesOut = resolve(repoRoot, required(option(argv, "--bundles-out"), "--bundles-out"));
const bundleDir = resolve(repoRoot, required(option(argv, "--bundle-dir"), "--bundle-dir"));
Expand Down Expand Up @@ -166,13 +170,13 @@ async function main() {
};
};

const feed = await buildRuntimeFeed({ github, resolveBundle, channel });
const feed = await buildRuntimeFeed({ github, resolveBundle, channel, feedVersion });
const previous = await previousFeed(previousPath);
if (
previous !== undefined &&
JSON.stringify(materialReleases(previous)) === JSON.stringify(materialReleases(feed))
) {
console.log(`unchanged: runtime feed ${channel}`);
console.log(`unchanged: runtime feed v${feedVersion} ${channel}`);
return;
}

Expand Down
100 changes: 91 additions & 9 deletions scripts/runtime-feed-lib.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,28 @@ export const DEFAULT_FOUNDATIONMODELS_ASSET_URL =
// rejects a whole feed that names a runtime it does not know, and 0.5.x
// still reads the stable feed. Add "stable" only after 0.5.x is retired.
export const FOUNDATIONMODELS_CHANNELS = new Set(["dev"]);
export const DEFAULT_IMAGES_ASSET_URL =
"https://nexus-assets.msty.ai/msty-nexus/runtime/images/latest/msty-nexus-images.tgz";

// The feed is published at two paths with the same schema and signing key.
// v2 is read by Msty Nexus Runtime 0.9.1 and earlier, which reject the whole
// feed for a runtime, a field, a backend or an archive format they do not
// know. So v2 is frozen: only new versions of the runtimes below go there.
// v3 is read by Runtime 0.10 and later, which skip entries they do not know.
// New runtimes, fields and artifact kinds go to v3 only.
export const FEED_VERSIONS = [2, 3];
const V2_RUNTIME_IDS = ["ollama", "llamacpp", "mlx", "foundationmodels"];
const V3_ONLY_RUNTIME_IDS = ["images"];

export function parseFeedVersion(value) {
const version = Number(value);
invariant(FEED_VERSIONS.includes(version), `runtime feed version must be one of ${FEED_VERSIONS.join(", ")}`);
return version;
}

function allowedRuntimeIds(feedVersion) {
return parseFeedVersion(feedVersion) === 2 ? V2_RUNTIME_IDS : [...V2_RUNTIME_IDS, ...V3_ONLY_RUNTIME_IDS];
}

const GITHUB_API = "https://api.github.com";
const GITHUB_DIGEST = /^sha256:([0-9a-f]{64})$/;
Expand Down Expand Up @@ -416,6 +438,57 @@ async function buildFoundationModelsRelease(fetchImpl, channel, publishedAt, ass
};
}

// The image runtime follows the Foundation Models asset contract, because MLX
// itself has no dev lane in the feed: the stable channel reads latest/ and
// points at the immutable <version>/ copy, and the dev channel reads the dev
// publish (latest/dev/), which has no versioned copy and is pinned by SHA-256.
// Returns null only when the manifest is not published yet (HTTP 404). Any
// other problem fails the refresh.
async function buildImagesRelease(fetchImpl, channel, publishedAt, assetURL) {
const parsed = new URL(assetURL);
invariant(parsed.protocol === "https:" && parsed.search === "" && parsed.hash === "", "Image runtime asset URL must be immutable HTTPS metadata input");
invariant(parsed.pathname.endsWith("/images/latest/msty-nexus-images.tgz"), "Image runtime asset URL must be the latest msty-nexus-images.tgz alias");
const dev = channel === "dev";
if (dev) assetURL = assetURL.replace("/images/latest/", "/images/latest/dev/");
const prefix = assetURL.slice(0, -"msty-nexus-images.tgz".length);
const probe = await fetchImpl(`${prefix}runtime-manifest.json`, { method: "HEAD", redirect: "follow" });
if (probe.status === 404) return null;
const manifestResponse = await fetchWithRetry(`${prefix}runtime-manifest.json`, { fetchImpl });
const manifest = object(await manifestResponse.json(), "Image runtime manifest");
invariant(
manifest.object === "msty.nexus.images_runtime" && manifest.runtimeId === "images",
"Image runtime manifest identity is invalid",
);
const version = string(manifest.runtimeVersion, "Image runtime version");
invariant(SEMVER.test(version), "Image runtime version must be semantic");
const checksumResponse = await fetchWithRetry(`${prefix}checksums.txt`, { fetchImpl });
const checksumText = await checksumResponse.text();
const checksumLine = checksumText.split(/\r?\n/).find((line) => line.trim().endsWith("msty-nexus-images.tgz"));
const sha256 = String(checksumLine ?? "").trim().split(/\s+/)[0]?.toLowerCase();
invariant(SHA256.test(sha256), "Image runtime checksum file did not contain a valid archive SHA-256");
const versionedURL = dev ? assetURL : assetURL.replace("/images/latest/", `/images/${version}/`);
const head = await fetchWithRetry(versionedURL, { fetchImpl, method: "HEAD" });
const sizeBytes = Number(head.headers.get("content-length"));
integer(sizeBytes, "Image runtime artifact content-length");
return {
runtimeId: "images",
channel,
runtimeVersion: version,
publishedAt,
source: { type: "msty_asset", url: versionedURL },
artifacts: [{
os: "darwin",
arch: "arm64",
backend: "metal",
url: versionedURL,
sha256,
sizeBytes,
archiveFormat: "tar.gz",
executablePath: `msty-nexus-images_${version}_darwin_arm64/msty-nexus-images`,
}],
};
}

async function buildMLXRelease(fetchImpl, channel, publishedAt, mlxAssetURL) {
const prefix = mlxMetadataPrefix(mlxAssetURL);
const manifestResponse = await fetchWithRetry(`${prefix}runtime-manifest.json`, { fetchImpl });
Expand Down Expand Up @@ -461,7 +534,10 @@ export async function buildRuntimeFeed({
mlxAssetURL = DEFAULT_MLX_ASSET_URL,
foundationModelsAssetURL = DEFAULT_FOUNDATIONMODELS_ASSET_URL,
foundationModelsChannels = FOUNDATIONMODELS_CHANNELS,
feedVersion = 2,
imagesAssetURL = DEFAULT_IMAGES_ASSET_URL,
}) {
feedVersion = parseFeedVersion(feedVersion);
invariant(channel === "stable" || channel === "dev", "runtime feed channel must be stable or dev");
invariant(typeof github === "function", "GitHub client is required");
invariant(typeof resolveBundle === "function", "runtime bundle resolver is required");
Expand All @@ -474,23 +550,29 @@ export async function buildRuntimeFeed({
const foundationModels = await buildFoundationModelsRelease(fetchImpl, channel, publishedAt, foundationModelsAssetURL);
if (foundationModels) releases.push(foundationModels);
}
// v2 never asks for the image runtime, so it cannot enter that feed.
if (feedVersion >= 3) {
const images = await buildImagesRelease(fetchImpl, channel, publishedAt, imagesAssetURL);
if (images) releases.push(images);
}
const feed = { schemaVersion: FEED_SCHEMA_VERSION, generatedAt: publishedAt, releases };
validateRuntimeFeed(feed, channel);
validateRuntimeFeed(feed, channel, { feedVersion });
return feed;
}

export function validateRuntimeFeed(feed, expectedChannel) {
export function validateRuntimeFeed(feed, expectedChannel, { feedVersion = 2 } = {}) {
const allowed = allowedRuntimeIds(feedVersion);
object(feed, "feed");
invariant(feed.schemaVersion === FEED_SCHEMA_VERSION, `feed.schemaVersion must be ${FEED_SCHEMA_VERSION}`);
invariant(
Array.isArray(feed.releases) && (feed.releases.length === 3 || feed.releases.length === 4),
"feed must contain the three required runtime releases and at most Apple Foundation Models",
Array.isArray(feed.releases) && feed.releases.length >= 3 && feed.releases.length <= allowed.length,
`v${feedVersion} feed must contain the three required runtime releases and at most ${allowed.length - 3} optional ones`,
);
const byID = new Map();
for (const [releaseIndex, releaseValue] of feed.releases.entries()) {
const release = object(releaseValue, `feed.releases[${releaseIndex}]`);
const runtimeId = string(release.runtimeId, `feed.releases[${releaseIndex}].runtimeId`);
invariant(["ollama", "llamacpp", "mlx", "foundationmodels"].includes(runtimeId), `unsupported runtime ${runtimeId}`);
invariant(allowed.includes(runtimeId), `unsupported runtime ${runtimeId} in a v${feedVersion} feed`);
invariant(
runtimeId !== "foundationmodels" || FOUNDATIONMODELS_CHANNELS.has(expectedChannel),
`foundationmodels is not enabled for the ${expectedChannel} channel`,
Expand Down Expand Up @@ -562,9 +644,9 @@ function publicKeyFromRaw(value) {
return createPublicKey({ key: Buffer.concat([SPKI_ED25519_PREFIX, raw]), format: "der", type: "spki" });
}

export function signRuntimeFeed(feed, privateKeyBase64, keyId = DEFAULT_KEY_ID) {
export function signRuntimeFeed(feed, privateKeyBase64, keyId = DEFAULT_KEY_ID, { feedVersion = 2 } = {}) {
invariant(KEY_ID.test(keyId), "runtime feed signing key ID is invalid");
validateRuntimeFeed(feed, feed.releases?.[0]?.channel);
validateRuntimeFeed(feed, feed.releases?.[0]?.channel, { feedVersion });
const feedJSON = serializeFeed(feed);
const signature = {
algorithm: SIGNATURE_ALGORITHM,
Expand All @@ -574,12 +656,12 @@ export function signRuntimeFeed(feed, privateKeyBase64, keyId = DEFAULT_KEY_ID)
return `{"schemaVersion":${SIGNED_FEED_SCHEMA_VERSION},"feed":${feedJSON},"signature":${JSON.stringify(signature)}}\n`;
}

export function verifySignedRuntimeFeed(text, publicKeyBase64 = DEFAULT_PUBLIC_KEY, expectedChannel) {
export function verifySignedRuntimeFeed(text, publicKeyBase64 = DEFAULT_PUBLIC_KEY, expectedChannel, { feedVersion = 2 } = {}) {
const envelope = object(JSON.parse(text), "signed feed");
invariant(envelope.schemaVersion === SIGNED_FEED_SCHEMA_VERSION, `signed feed schemaVersion must be ${SIGNED_FEED_SCHEMA_VERSION}`);
invariant(envelope.signature?.algorithm === SIGNATURE_ALGORITHM, "signed feed algorithm must be ed25519");
invariant(KEY_ID.test(String(envelope.signature?.keyId ?? "")), "signed feed key ID is invalid");
validateRuntimeFeed(envelope.feed, expectedChannel ?? envelope.feed?.releases?.[0]?.channel);
validateRuntimeFeed(envelope.feed, expectedChannel ?? envelope.feed?.releases?.[0]?.channel, { feedVersion });
const signature = Buffer.from(String(envelope.signature?.value ?? ""), "base64");
invariant(signature.length === 64, "signed feed signature must decode to 64 bytes");
const canonical = `{"schemaVersion":${SIGNED_FEED_SCHEMA_VERSION},"feed":${serializeFeed(envelope.feed)},"signature":${JSON.stringify(envelope.signature)}}`;
Expand Down
4 changes: 3 additions & 1 deletion scripts/runtime-feed-sign.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url";

import {
DEFAULT_KEY_ID,
parseFeedVersion,
signRuntimeFeed,
} from "./runtime-feed-lib.mjs";

Expand All @@ -28,6 +29,7 @@ const keyEnvironment = required(
);
const key = required(process.env[keyEnvironment], keyEnvironment);
const keyId = required(option(argv, "--key-id", DEFAULT_KEY_ID), "--key-id");
const feedVersion = parseFeedVersion(required(option(argv, "--feed-version", "2"), "--feed-version"));
const feed = JSON.parse(await readFile(input, "utf8"));
await writeFile(out, signRuntimeFeed(feed, key, keyId), "utf8");
await writeFile(out, signRuntimeFeed(feed, key, keyId, { feedVersion }), "utf8");
console.log(`signed ${input} as ${out}`);
33 changes: 19 additions & 14 deletions scripts/runtime-feed-verify-all.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,28 @@ import { readdir, readFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";

import { verifySignedRuntimeFeed } from "./runtime-feed-lib.mjs";
import { FEED_VERSIONS, verifySignedRuntimeFeed } from "./runtime-feed-lib.mjs";

const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const root = resolve(repoRoot, "v2/runtime");
let channels;
try {
channels = await readdir(root, { withFileTypes: true });
} catch (error) {
if (error.code !== "ENOENT") throw error;
channels = [];
}

// Each feed version is checked against its own runtime allowlist, so a runtime
// that only v3 may list fails here if it reaches v2. A version directory that
// does not exist yet (v3 before its first refresh) is skipped.
let checked = 0;
for (const entry of channels) {
if (!entry.isDirectory()) continue;
const path = resolve(root, entry.name, "runtime-release-feed.signed.json");
verifySignedRuntimeFeed(await readFile(path, "utf8"), undefined, entry.name);
checked += 1;
for (const feedVersion of FEED_VERSIONS) {
const root = resolve(repoRoot, `v${feedVersion}/runtime`);
let channels;
try {
channels = await readdir(root, { withFileTypes: true });
} catch (error) {
if (error.code !== "ENOENT") throw error;
channels = [];
}
for (const entry of channels) {
if (!entry.isDirectory()) continue;
const path = resolve(root, entry.name, "runtime-release-feed.signed.json");
verifySignedRuntimeFeed(await readFile(path, "utf8"), undefined, entry.name, { feedVersion });
checked += 1;
}
}
console.log(`ok: ${checked} committed runtime release feeds`);
8 changes: 6 additions & 2 deletions scripts/runtime-feed-verify.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ import { fileURLToPath } from "node:url";

import {
DEFAULT_PUBLIC_KEY,
parseFeedVersion,
verifySignedRuntimeFeed,
} from "./runtime-feed-lib.mjs";

const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const path = resolve(repoRoot, process.argv[2] ?? "v2/runtime/stable/runtime-release-feed.signed.json");
const channel = process.argv[3] ?? "stable";
// Third argument: 2 (default) allows only the runtimes of the frozen v2 feed,
// 3 also allows the runtimes that only v3 may list.
const feedVersion = parseFeedVersion(process.argv[4] ?? "2");
const publicKey = process.env.MSTY_NEXUS_RUNTIME_RELEASE_FEED_PUBLIC_KEY ?? DEFAULT_PUBLIC_KEY;
const envelope = verifySignedRuntimeFeed(await readFile(path, "utf8"), publicKey, channel);
console.log(`ok: ${channel} feed with ${envelope.feed.releases.length} runtime releases`);
const envelope = verifySignedRuntimeFeed(await readFile(path, "utf8"), publicKey, channel, { feedVersion });
console.log(`ok: v${feedVersion} ${channel} feed with ${envelope.feed.releases.length} runtime releases`);
Loading
Loading