From ab41e67dc56df39350f0935b6e8642d72774a541 Mon Sep 17 00:00:00 2001 From: "Timothy J. Aveni" Date: Sun, 20 Sep 2026 00:25:53 -0700 Subject: [PATCH] Build checked portable package artifacts and shared runtime assets --- flake.nix | 1 + nix/checked-artifacts.nix | 41 +++++++++++++ src/execution-world/files.ts | 15 +++++ src/execution-world/index.ts | 4 ++ src/execution-world/ports.ts | 7 ++- src/execution-world/receipts.ts | 102 ++++++++++++++++++++++++++++++++ test/artifact-files.test.ts | 28 +++++++++ test/emit-portable-reader.ts | 20 +++++++ 8 files changed, 216 insertions(+), 2 deletions(-) create mode 100644 nix/checked-artifacts.nix create mode 100644 src/execution-world/receipts.ts diff --git a/flake.nix b/flake.nix index 35c21a0..3256fe8 100644 --- a/flake.nix +++ b/flake.nix @@ -75,6 +75,7 @@ runHook preInstall mkdir -p "$out" install -Dm644 nix/checked-package.nix "$out/share/checked-package.nix" + install -Dm644 nix/checked-artifacts.nix "$out/share/checked-artifacts.nix" substitute nix/checked-candidate.nix "$out/share/checked-candidate.nix" \ --replace-fail '@nixpkgs@' '${pkgs.path}' cp --reflink=auto --recursive grammar "$out/grammar" diff --git a/nix/checked-artifacts.nix b/nix/checked-artifacts.nix new file mode 100644 index 0000000..0505663 --- /dev/null +++ b/nix/checked-artifacts.nix @@ -0,0 +1,41 @@ +# Candidate artifact evaluation. This is separate from the old server activation path; +# the new coordinator consumes its manifest/receipt, not the legacy server ABI. +{ + source, + plan, + generator, + core, + sharedRuntime, + typescript, + packageRevisionId, + system ? builtins.currentSystem, +}: +let + packageSource = builtins.path { + path = /. + source; + name = "quixos-package-source"; + filter = + path: _: + !(builtins.elem (baseNameOf path) [ + ".git" + ".jj" + ]); + }; + package = builtins.getFlake ( + "path:" + builtins.unsafeDiscardStringContext (toString packageSource) + ); + checked = + package.quixosPackages.${system}.checkedArtifacts + or (throw "Package ${packageRevisionId} lacks checkedArtifacts for the portable execution world."); +in +checked { + inherit packageRevisionId; + plan = builtins.path { + path = /. + plan; + name = "candidate-execution-plan.json"; + }; + generator = builtins.storePath generator; + core = builtins.storePath core; + sharedRuntime = builtins.storePath sharedRuntime; + typescript = builtins.storePath typescript; +} diff --git a/src/execution-world/files.ts b/src/execution-world/files.ts index 765a590..601b8f1 100644 --- a/src/execution-world/files.ts +++ b/src/execution-world/files.ts @@ -99,6 +99,13 @@ export interface ArtifactResponse { headers: Record; 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): 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 { @@ -124,6 +131,14 @@ export class BrowserArtifactGateway { ) 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("/"), diff --git a/src/execution-world/index.ts b/src/execution-world/index.ts index efd6ed6..335c596 100644 --- a/src/execution-world/index.ts +++ b/src/execution-world/index.ts @@ -2,3 +2,7 @@ export * from "./plan.js"; export * from "./artifacts.js"; export * from "./ports.js"; export * from "./files.js"; + +export * from "./receipts.js"; + +export { generatePortableTypeScriptBindings } from "../bindings/portable.js"; diff --git a/src/execution-world/ports.ts b/src/execution-world/ports.ts index 666e102..7896193 100644 --- a/src/execution-world/ports.ts +++ b/src/execution-world/ports.ts @@ -3,7 +3,8 @@ export const transactionPortAbi = "quixos-transaction-ports-v1"; export type PortAction = | { kind: "state"; primitive: "read" | "write" | "edit" } - | { kind: "edge"; primitive: "resolve" | "connect" | "disconnect" } + | { kind: "edge"; primitive: "resolve" | "disconnect" } + | { kind: "edge"; primitive: "connect"; key: string } | { kind: "interface"; operationId: string; key?: string } | { kind: "query" } | { kind: "creation"; key: string }; @@ -19,6 +20,7 @@ export interface PortInvocation { } export interface PackageInvocation { abi: typeof transactionPortAbi; + workspace: string; frame: string; invocation: string; exportId: string; @@ -66,7 +68,8 @@ export function checkPortInvocation(value: unknown): PortInvocation { action.kind === "state" ? exact(["kind", "primitive"]) && ["read", "write", "edit"].includes(action.primitive as string) : action.kind === "edge" - ? exact(["kind", "primitive"]) && ["resolve", "connect", "disconnect"].includes(action.primitive as string) + ? (exact(["kind", "primitive"]) && ["resolve", "disconnect"].includes(action.primitive as string)) || + (exact(["kind", "primitive", "key"]) && action.primitive === "connect" && identifier(action.key)) : action.kind === "interface" ? (exact(["kind", "operationId"]) || (exact(["kind", "operationId", "key"]) && identifier(action.key))) && identifier(action.operationId) diff --git a/src/execution-world/receipts.ts b/src/execution-world/receipts.ts new file mode 100644 index 0000000..693d4ee --- /dev/null +++ b/src/execution-world/receipts.ts @@ -0,0 +1,102 @@ +import { createHash } from "node:crypto"; +import { generatePortableTypeScriptBindings } from "../bindings/portable.js"; +import { type ExecutionPlan, ExecutionPlanError, hexDigest } from "./plan.js"; +import { type ArtifactReader, type PackageArtifacts } from "./artifacts.js"; +import { loadArtifactWorld, type LoadedArtifactWorld } from "./files.js"; + +export interface ArtifactReceipt { + schemaVersion: 1; + packageRevisionId: string; + bindingDigest: string; + sourceDigest: string; + manifestDigest: string; + generatorDigest: string; + generated: { target: string; path: string; digest: string; bytes: number }[]; +} +const hashBytes = (bytes: Uint8Array) => createHash("sha256").update(bytes).digest("hex"); +/** Deterministic expected receipt. A trusted build must also typecheck and bundle these exact sources. + * This is consistency evidence inside a trusted Nix build path, not remote build attestation. */ +export function expectedArtifactReceipt( + plan: ExecutionPlan, + manifest: PackageArtifacts, + generatorDigest: string, +): { receipt: ArtifactReceipt; sources: Map } { + if (!/^[0-9a-f]{64}$/.test(generatorDigest)) throw new Error("Invalid trusted generator digest"); + const pkg = plan.definition.packages.find((p) => p.revision === manifest.packageRevisionId); + const schemaPackage = plan.definition.bindingSchema.packages.find((p) => p.revisionId === manifest.packageRevisionId); + if ( + !pkg || + !schemaPackage || + manifest.bindingDigest !== pkg.bindingDigest || + manifest.sourceDigest !== pkg.sourceDigest + ) + throw new ExecutionPlanError( + "INVALID_RECEIPT", + manifest.packageRevisionId, + "Receipt package/schema/source mismatch", + ); + const available = new Set(manifest.implementations.map((i) => i.target)); + const targets = [ + ...new Set( + schemaPackage.exports.flatMap((e) => (e.execution && e.execution.kind !== "remote" ? e.execution.targets : [])), + ), + ] + .filter((t) => available.has(t)) + .sort(); + const sources = new Map(); + const generated = targets.map((target) => { + if (!/^[a-zA-Z0-9_-]+$/.test(target)) throw new Error("Unsupported generated target filename"); + const path = `share/quixos/generated/${target}.ts`; + const bytes = new TextEncoder().encode( + generatePortableTypeScriptBindings(plan.definition.bindingSchema, pkg.revision, { target }), + ); + sources.set(path, bytes); + return { target, path, digest: hashBytes(bytes), bytes: bytes.length }; + }); + return { + receipt: { + schemaVersion: 1, + packageRevisionId: pkg.revision, + bindingDigest: pkg.bindingDigest, + sourceDigest: pkg.sourceDigest, + manifestDigest: hexDigest(manifest), + generatorDigest, + generated, + }, + sources, + }; +} +export async function checkArtifactReceipt( + plan: ExecutionPlan, + manifest: PackageArtifacts, + receipt: unknown, + generatorDigest: string, + read: ArtifactReader, +): Promise { + const expected = expectedArtifactReceipt(plan, manifest, generatorDigest); + if (hexDigest(receipt) !== hexDigest(expected.receipt)) + throw new ExecutionPlanError( + "INVALID_RECEIPT", + manifest.packageRevisionId, + "Receipt differs from the trusted compiler/schema/artifact expectation", + ); + for (const file of expected.receipt.generated) { + const actual = await read(manifest.packageRevisionId, file.path, file.bytes); + if (actual.length !== file.bytes || hashBytes(actual) !== file.digest) + throw new ExecutionPlanError("INVALID_RECEIPT", file.path, "Generated bindings were replaced or omitted"); + } +} +/** Activation uses this entrypoint with its trusted generator identity, not the manifest-only loader. */ +export async function loadCheckedArtifactWorld( + plan: ExecutionPlan, + roots: Readonly>, + generatorDigest: string, +): Promise { + const loaded = await loadArtifactWorld(plan, roots); + for (const manifest of loaded.manifests) { + const bytes = await loaded.read(manifest.packageRevisionId, "share/quixos/build-receipt.json", 16 * 1024 * 1024); + const receipt: unknown = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)); + await checkArtifactReceipt(plan, manifest, receipt, generatorDigest, loaded.read); + } + return loaded; +} diff --git a/test/artifact-files.test.ts b/test/artifact-files.test.ts index 45796e1..cf1b03b 100644 --- a/test/artifact-files.test.ts +++ b/test/artifact-files.test.ts @@ -10,6 +10,7 @@ import { loadArtifactWorld, BrowserArtifactGateway, implementationDigest, + contentArtifactUrl, } from "../src/execution-world/index.js"; import { loadReaderWorkspace } from "./reader-fixture.js"; import { buildArtifactFixture } from "./artifact-fixture.js"; @@ -52,6 +53,8 @@ test("gateway serves only declared browser assets with correct Wasm MIME and con digest: createHash("sha256").update(wasm).digest("hex"), mediaType: "application/wasm", }); + const shared = { ...browser.files[0]!, path: "share/quixos/shared/core.mjs" }; + browser.files.push(shared); const { contentDigest, ...body } = browser; browser.contentDigest = implementationDigest(body); for (const implementation of fixture.manifest.implementations) @@ -65,6 +68,7 @@ test("gateway serves only declared browser assets with correct Wasm MIME and con const loaded = await loadArtifactWorld(plan, { [fixture.manifest.packageRevisionId]: root }); const gateway = new BrowserArtifactGateway(loaded); assert.equal(gateway.entries.length, 1); + assert.deepEqual((await gateway.response(contentArtifactUrl(shared)))?.body, Buffer.from(fixture.bytes)); const entry = gateway.entries[0]!; const response = await gateway.response(entry.url); assert.equal(response?.status, 200); @@ -86,3 +90,27 @@ test("gateway serves only declared browser assets with correct Wasm MIME and con await rm(root, { recursive: true, force: true }); } }); + +test("checked receipts bind regenerated code, generator identity and manifests", async () => { + const { expectedArtifactReceipt, checkArtifactReceipt } = await import("../src/execution-world/receipts.js"); + const plan = compileExecutionPlan(await loadReaderWorkspace()); + const fixture = buildArtifactFixture(plan); + const generator = "a".repeat(64); + const { receipt, sources } = expectedArtifactReceipt(plan, fixture.manifest, generator); + const read = async (_revision: string, file: string) => sources.get(file)!; + await checkArtifactReceipt(plan, fixture.manifest, receipt, generator, read); + await assert.rejects( + checkArtifactReceipt(plan, fixture.manifest, { ...receipt, generatorDigest: "b".repeat(64) }, generator, read), + /trusted compiler/, + ); + await assert.rejects( + checkArtifactReceipt(plan, fixture.manifest, { ...receipt, manifestDigest: "b".repeat(64) }, generator, read), + /trusted compiler/, + ); + await assert.rejects( + checkArtifactReceipt(plan, fixture.manifest, receipt, generator, async () => + new TextEncoder().encode("substituted"), + ), + /replaced or omitted/, + ); +}); diff --git a/test/emit-portable-reader.ts b/test/emit-portable-reader.ts index 1b2dd7c..cd08753 100644 --- a/test/emit-portable-reader.ts +++ b/test/emit-portable-reader.ts @@ -11,3 +11,23 @@ for (const target of ["browser", "server"]) generatePortableTypeScriptBindings(plan.definition.bindingSchema, "package:reader-runtime@1", { target }), ); await writeFile("dist/portable-reader/plan.json", JSON.stringify(plan)); +const serviceWorkspace = structuredClone(plan.definition.workspace); +serviceWorkspace.atoms = []; +serviceWorkspace.conformances = []; +serviceWorkspace.constructors = []; +serviceWorkspace.interfaceImports = []; +serviceWorkspace.sharedAttachments = []; +serviceWorkspace.packageImports[0]!.queries = []; +serviceWorkspace.packageImports[0]!.checkedQueries = []; +const original = serviceWorkspace.packageImports[0]!.exports.find((e) => e.kind === "function")!; +serviceWorkspace.packageImports[0]!.exports = [ + { + ...original, + kind: "function", + execution: { kind: "remote" }, + inputType: { kind: "scalar", name: "string" }, + outputType: { kind: "scalar", name: "string" }, + dependencyPorts: [], + }, +]; +await writeFile("dist/portable-reader/service-plan.json", JSON.stringify(compileExecutionPlan(serviceWorkspace)));