diff --git a/nix/check-execution.mjs b/nix/check-execution.mjs index af90822..d4dbe35 100644 --- a/nix/check-execution.mjs +++ b/nix/check-execution.mjs @@ -19,8 +19,16 @@ const checked = await compiler.loadCheckedArtifactWorld( .update(await readFile(compilerPath)) .digest("hex"), ); +const installation = await compiler.loadExecutionInstallation( + plan, + roots, + createHash("sha256") + .update(await readFile(compilerPath)) + .digest("hex"), +); for (const [file, value] of [ ["execution-world.json", checked.world], + ["installation.json", installation], ["kernel.json", compiler.compileKernelProgram(plan)], ["queries.json", compiler.compileQueryProgram(plan)], ["execution-artifacts.json", { codec: "camino-checked-execution-v1", roots, manifests: checked.manifests }], diff --git a/src/execution-world/index.ts b/src/execution-world/index.ts index 1bd02b3..451ff90 100644 --- a/src/execution-world/index.ts +++ b/src/execution-world/index.ts @@ -2,6 +2,7 @@ export * from "./plan.js"; export * from "./artifacts.js"; export * from "./ports.js"; export * from "./files.js"; +export * from "./installation.js"; export * from "./receipts.js"; diff --git a/src/execution-world/installation.ts b/src/execution-world/installation.ts new file mode 100644 index 0000000..fd12b55 --- /dev/null +++ b/src/execution-world/installation.ts @@ -0,0 +1,114 @@ +import path from "node:path"; +import { loadCheckedArtifactWorld } from "./receipts.js"; +import { contentArtifactUrl } from "./files.js"; +import { selectArtifactImplementation, type ArtifactImplementation } from "./artifacts.js"; +import { ExecutionPlanError, type ExecutionPlan } from "./plan.js"; + +export interface PackageInstallationSelection { + readonly browser?: { readonly id?: string }; + readonly service?: { readonly id?: string }; + /** Including a module is optional; executing it still requires host trust policy. */ + readonly module?: { readonly id?: string }; +} +export interface ExecutionInstallation { + readonly codec: "camino-execution-installation-v1"; + readonly plan: string; + readonly world: string; + readonly packages: readonly { + readonly revision: string; + readonly root: string; + readonly service: ArtifactImplementation; + readonly module?: ArtifactImplementation; + readonly browserURL?: string; + readonly browserRegistry?: string; + }[]; + readonly routes: Readonly>; + readonly coreURL?: string; + readonly engineURL?: string; +} +/** Verify candidate receipts before assembling process entrypoints and browser assets. + * This grants no authority to activate a world or run optional modules in process. */ +export async function loadExecutionInstallation( + plan: ExecutionPlan, + roots: Readonly>, + generator: string, + selections: Readonly> = {}, +): Promise { + const loaded = await loadCheckedArtifactWorld(plan, roots, generator); + const fail = (where: string, message: string): never => { + throw new ExecutionPlanError("INSTALLATION_INVALID", where, message); + }; + for (const revision of Object.keys(selections)) + if (!loaded.manifests.some((manifest) => manifest.packageRevisionId === revision)) + fail(revision, "Selection names a package outside this checked world"); + const routes: Record = Object.create(null); + const packages: ExecutionInstallation["packages"][number][] = []; + let coreURL: string | undefined, engineURL: string | undefined; + for (const manifest of loaded.manifests) { + const revision = manifest.packageRevisionId; + const root = roots[revision]!; + const selection = selections[revision] ?? {}; + const exports = plan.definition.packages.find((pkg) => pkg.revision === revision)!.exports; + const service = selectArtifactImplementation(manifest, { + ...selection.service, + target: "server", + compatibility: "quixos-service-v1", + exports: exports.map((entry) => entry.id), + }); + const module = selection.module + ? selectArtifactImplementation(manifest, { + ...selection.module, + target: "server", + compatibility: "quixos-js-module-v1", + exports: exports.map((entry) => entry.id), + }) + : undefined; + const browserExports = exports.filter( + (entry) => entry.execution.kind !== "remote" && entry.execution.targets.includes("browser"), + ); + if (!browserExports.length) { + if (selection.browser) fail(revision, "Package has no browser exports to select"); + packages.push({ revision, root, service, ...(module ? { module } : {}) }); + continue; + } + const browser = selectArtifactImplementation(manifest, { + ...selection.browser, + target: "browser", + compatibility: "quixos-js-module-v1", + exports: browserExports.map((entry) => entry.id), + }); + const entry = browser.files.find((file) => file.path === browser.path)!; + const shared = (name: string) => { + const files = browser.files.filter((file) => file.path.endsWith("/" + name)); + if (files.length !== 1) fail(revision, "Expected one shared " + name); + return contentArtifactUrl(files[0]!); + }; + const core = shared("core.mjs"), + engine = shared("engine.mjs"); + if ((coreURL && coreURL !== core) || (engineURL && engineURL !== engine)) + fail(revision, "Browser packages disagree on the shared runtime identity"); + coreURL = core; + engineURL = engine; + for (const file of browser.files) { + const url = contentArtifactUrl(file); + if (routes[url] && routes[url].mediaType !== file.mediaType) fail(url, "Conflicting artifact media types"); + routes[url] ??= { path: path.join(root, file.path), mediaType: file.mediaType }; + } + packages.push({ + revision, + root, + service, + ...(module ? { module } : {}), + browserURL: contentArtifactUrl(entry), + browserRegistry: browser.registryExport, + }); + } + return { + codec: "camino-execution-installation-v1", + plan: plan.digest, + world: loaded.digest, + packages, + routes, + ...(coreURL ? { coreURL, engineURL } : {}), + }; +}