Files
quixos-protocol/src/execution-world/files.ts
T

175 lines
8.1 KiB
TypeScript
Raw Normal View History

import { constants } from "node:fs";
import { lstat, open, realpath } from "node:fs/promises";
import path from "node:path";
import { createHash } from "node:crypto";
import {
artifactPath,
parsePackageArtifacts,
checkExecutionWorld,
checkedWorldDigest,
type ArtifactReader,
type ArtifactFile,
type PackageArtifacts,
type CheckedWorld,
} from "./artifacts.js";
import { type ExecutionPlan, ExecutionPlanError } from "./plan.js";
const maxFileBytes = 256 * 1024 * 1024;
const metadataLimit = 16 * 1024 * 1024;
const fail = (where: string, message: string): never => {
throw new ExecutionPlanError("ARTIFACT_FILE_INVALID", where, message);
};
/** Roots must be trusted immutable output directories. Refuse symlinks, including
* interior directory links. Hash verification is still required before serving bytes. */
export async function artifactFileReader(roots: Readonly<Record<string, string>>): Promise<ArtifactReader> {
const directories = new Map<string, string>();
for (const [revision, root] of Object.entries(roots)) {
if (!path.isAbsolute(root) || path.normalize(root) !== root || (await realpath(root)) !== root)
fail(revision, "Output root must be a canonical absolute directory");
const stat = await lstat(root);
if (!stat.isDirectory() || stat.isSymbolicLink()) fail(root, "Output root is not a directory");
directories.set(revision, root);
}
return async (revision, relative, limit) => {
const root = directories.get(revision);
if (!root) fail(revision, "Unknown package output");
artifactPath(relative);
if (!Number.isSafeInteger(limit) || limit < 0 || limit > maxFileBytes) fail(relative, "Invalid file byte limit");
const components = relative.split("/");
let resolved = root!;
for (let index = 0; index < components.length; index++) {
resolved = path.join(resolved, components[index]!);
const stat = await lstat(resolved);
if (stat.isSymbolicLink() || (index < components.length - 1 ? !stat.isDirectory() : !stat.isFile()))
fail(relative, "Only real directories and regular files are allowed");
}
const handle = await open(resolved, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
try {
const stat = await handle.stat();
if (!stat.isFile() || stat.size > limit) fail(relative, "File exceeds its declared bound");
// A fixed allocation and one extra byte detect growth without readFile's unbounded allocation.
const buffer = Buffer.alloc(stat.size + 1);
let offset = 0;
while (offset < buffer.length) {
const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, offset);
if (!bytesRead) break;
offset += bytesRead;
}
if (offset !== stat.size) fail(relative, "File changed while reading");
return buffer.subarray(0, offset);
} finally {
await handle.close();
}
};
}
export interface LoadedArtifactWorld {
world: CheckedWorld;
digest: string;
manifests: PackageArtifacts[];
read: ArtifactReader;
}
/** Immutable-output loader. No module code is imported into the coordinator. */
export async function loadArtifactWorld(
plan: ExecutionPlan,
roots: Readonly<Record<string, string>>,
): Promise<LoadedArtifactWorld> {
const read = await artifactFileReader(roots),
manifests: PackageArtifacts[] = [];
const expected = plan.definition.packages.map((p) => p.revision).sort();
if (JSON.stringify(Object.keys(roots).sort()) !== JSON.stringify(expected))
fail("world", "Output roots must exactly match the installed plan");
for (const revision of expected) {
const bytes = await read(revision, "share/quixos/package-artifacts.json", metadataLimit);
const manifest = parsePackageArtifacts(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)));
if (manifest.packageRevisionId !== revision) fail(revision, "Output belongs to another package revision");
manifests.push(manifest);
}
const world = await checkExecutionWorld(plan, manifests, read);
return { world, digest: checkedWorldDigest(world), manifests, read };
}
export interface BrowserArtifact {
url: string;
implementation: string;
packageRevisionId: string;
registryExport: string;
exports: readonly string[];
}
export interface ArtifactResponse {
status: 200 | 304;
headers: Record<string, string>;
body: Uint8Array;
}
/** Shared runtime imports use content identity, independent of the importing package/world.
* The file remains covered by every importing implementation's checked manifest. */
export function contentArtifactUrl(file: Pick<ArtifactFile, "path" | "digest">): string {
if (!/^[0-9a-f]{64}$/.test(file.digest)) fail(file.path, "Invalid content digest");
artifactPath(file.path);
return `/__quixos/artifacts/content/${file.digest}/${encodeURIComponent(path.basename(file.path))}`;
}
/** This registry is placed behind the workspace gateway's existing authentication.
* Lookup uses an explicit URL table; request paths never become filesystem paths. */
export class BrowserArtifactGateway {
readonly entries: readonly BrowserArtifact[];
private readonly routes = new Map<string, { revision: string; file: ArtifactFile }>();
private readonly read: ArtifactReader;
constructor(loaded: LoadedArtifactWorld) {
this.read = loaded.read;
const entries: BrowserArtifact[] = [];
for (const manifest of loaded.manifests)
for (const implementation of manifest.implementations) {
if (
implementation.target !== "target:org.quixos.execution.javascript@1" ||
implementation.compatibility !== "quixos-js-module-v1"
)
continue;
const prefix = `/__quixos/artifacts/${loaded.digest}/${implementation.contentDigest}/`;
for (const file of implementation.files) {
if (!/^[a-zA-Z0-9.+-]+\/[a-zA-Z0-9.+-]+$/.test(file.mediaType)) fail(file.path, "Invalid asset MIME type");
const url = prefix + file.path.split("/").map(encodeURIComponent).join("/");
const previous = this.routes.get(url);
if (
previous &&
(previous.file.digest !== file.digest ||
previous.file.bytes !== file.bytes ||
previous.file.mediaType !== file.mediaType)
)
fail(url, "Conflicting browser asset route");
this.routes.set(url, { revision: manifest.packageRevisionId, file: Object.freeze({ ...file }) });
// Exact shared-file names are a packaging convention, never a path lookup capability.
if (file.path.startsWith("share/quixos/shared/")) {
const contentUrl = contentArtifactUrl(file);
const existing = this.routes.get(contentUrl);
if (existing && (existing.file.bytes !== file.bytes || existing.file.mediaType !== file.mediaType))
fail(contentUrl, "Conflicting shared asset metadata");
this.routes.set(contentUrl, { revision: manifest.packageRevisionId, file: Object.freeze({ ...file }) });
}
}
entries.push({
url: prefix + implementation.path.split("/").map(encodeURIComponent).join("/"),
implementation: implementation.id,
packageRevisionId: manifest.packageRevisionId,
registryExport: implementation.registryExport!,
exports: Object.freeze([...implementation.exports]),
});
}
this.entries = Object.freeze(entries.map((entry) => Object.freeze(entry)));
}
async response(url: string, ifNoneMatch?: string): Promise<ArtifactResponse | undefined> {
const route = this.routes.get(url);
if (!route) return undefined;
const bytes = await this.read(route.revision, route.file.path, route.file.bytes);
if (bytes.length !== route.file.bytes || createHash("sha256").update(bytes).digest("hex") !== route.file.digest)
fail(url, "Browser asset changed after world validation");
const etag = `"sha256-${route.file.digest}"`;
const headers = {
"content-type": route.file.mediaType,
"cache-control": "private, max-age=31536000, immutable",
etag,
"x-content-type-options": "nosniff",
};
return ifNoneMatch === etag
? { status: 304, headers, body: new Uint8Array() }
: { status: 200, headers, body: bytes };
}
}