diff --git a/nix/check-execution.mjs b/nix/check-execution.mjs index d4dbe35..585bfc7 100644 --- a/nix/check-execution.mjs +++ b/nix/check-execution.mjs @@ -7,6 +7,7 @@ const [compilerPath, planPath, checksPath, output] = process.argv.slice(2); const compiler = await import(pathToFileURL(compilerPath)); const plan = JSON.parse(await readFile(planPath, "utf8")); const checks = JSON.parse(await readFile(checksPath, "utf8")); +compiler.compileWorkspaceEntry(plan, JSON.parse(await readFile(join(output, "entry.json"), "utf8"))); const roots = Object.create(null); for (const check of checks) { if (Object.hasOwn(roots, check.packageRevisionId)) throw Error("Duplicate checked package revision"); diff --git a/nix/checked-candidate.nix b/nix/checked-candidate.nix index 0cba848..b21a155 100644 --- a/nix/checked-candidate.nix +++ b/nix/checked-candidate.nix @@ -162,6 +162,16 @@ pkgs.runCommand (if contractOnly then "qx-contract" else "qx-checked-candidate") cp ${contract}/graph.json "$out/graph.json" cp ${manifest} "$out/checks.json" ${pkgs.lib.optionalString execution '' + ${ + if builtins.pathExists "${root.directory}/quixos.entry.json" then + '' + cp ${root.directory}/quixos.entry.json "$out/entry.json" + '' + else + '' + echo null > "$out/entry.json" + '' + } cp ${contract}/execution-plan.json "$out/execution-plan.json" ${pkgs.lib.optionalString (!contractOnly) '' ${pkgs.nodejs_24}/bin/node ${./check-execution.mjs} \ diff --git a/src/execution-world/candidate.ts b/src/execution-world/candidate.ts index 24046b7..650c3f5 100644 --- a/src/execution-world/candidate.ts +++ b/src/execution-world/candidate.ts @@ -2,7 +2,7 @@ import { open, lstat } from "node:fs/promises"; import { constants } from "node:fs"; import { join } from "node:path"; import type { WorkspaceRevision } from "../capability-model/types.js"; -import { compileExecutionPlan, hexDigest, ExecutionPlanError } from "./plan.js"; +import { compileExecutionPlan, hexDigest, ExecutionPlanError, type ExecutionPlan } from "./plan.js"; import { loadExecutionInstallation, type PackageInstallationSelection } from "./installation.js"; import { compileKernelProgram } from "./kernel.js"; import { compileQueryProgram } from "./queries.js"; @@ -26,11 +26,12 @@ export async function loadCheckedInstallationCandidate( ) { if (!rootPattern.test(root) || !(await lstat(root)).isDirectory()) throw Error("Candidate must be an immutable Nix output directory"); - const [candidate, recordedPlan, artifacts, source] = await Promise.all([ + const [candidate, recordedPlan, artifacts, source, entryRequest] = await Promise.all([ readJson(root, "candidate.json"), readJson(root, "execution-plan.json"), readJson(root, "execution-artifacts.json"), readJson(root, "source.json"), + readJson(root, "entry.json"), ]); const { executionContracts: _legacyContracts, ...workspace } = candidate; const plan = compileExecutionPlan(workspace as WorkspaceRevision); @@ -70,6 +71,7 @@ export async function loadCheckedInstallationCandidate( "Package artifacts must be immutable Nix roots", ); const installation = await loadExecutionInstallation(plan, artifacts.roots, generator, selections); + const entry = compileWorkspaceEntry(plan, entryRequest); const kernel = compileKernelProgram(plan), queries = compileQueryProgram(plan); // The isolated runner and managed host consume the same compiled programs. @@ -91,7 +93,7 @@ export async function loadCheckedInstallationCandidate( }; const reference = { root, - digest: hexDigest({ plan: plan.digest, world: installation.world, provenance, roots: artifacts.roots }), + digest: hexDigest({ plan: plan.digest, world: installation.world, provenance, roots: artifacts.roots, entry }), }; return { reference, @@ -99,9 +101,47 @@ export async function loadCheckedInstallationCandidate( world: installation.world, plan: plan.digest, provenance, + entry, kernel, queries, installation, executionPlan: plan, }; } + +export function compileWorkspaceEntry(plan: ExecutionPlan, entryRequest: unknown): { operation: string } | null { + let entry: { operation: string } | null = null; + if (entryRequest !== null) { + if ( + typeof entryRequest !== "object" || + Array.isArray(entryRequest) || + Object.keys(entryRequest).sort().join(",") !== "atom,interface,operation" + ) + throw new ExecutionPlanError( + "INVALID_ENTRY_FACTORY", + "entry", + "Entry declaration must name atom, interface and operation identities", + ); + const operation = plan.definition.operations.find( + (op) => + op.atomId === (entryRequest as Record).atom && + op.interfaceRevisionId === (entryRequest as Record).interface && + op.operationId === (entryRequest as Record).operation, + ); + if ( + !operation || + operation.scope !== "class" || + operation.execution.kind !== "transaction" || + operation.input.kind !== "builtin" || + operation.input.name !== "unit" || + operation.output.kind !== "object-ref" + ) + throw new ExecutionPlanError( + "INVALID_ENTRY_FACTORY", + "entry", + "Entry must be a checked unit-input class transaction returning one object reference", + ); + entry = { operation: operation.key }; + } + return entry; +} diff --git a/src/execution-world/receipts.ts b/src/execution-world/receipts.ts index 4dd4b90..57d2665 100644 --- a/src/execution-world/receipts.ts +++ b/src/execution-world/receipts.ts @@ -35,7 +35,11 @@ export function expectedArtifactReceipt( manifest.packageRevisionId, "Receipt package/schema/source mismatch", ); - const available = new Set(manifest.implementations.map((i) => i.target)); + // A process service may implement transactions in any language. Only the + // checked JS module path promises generated TypeScript adapter evidence. + const available = new Set( + manifest.implementations.filter((i) => i.compatibility === "quixos-js-module-v1").map((i) => i.target), + ); const targets = [ ...new Set( schemaPackage.exports.flatMap((e) => (e.execution && e.execution.kind !== "remote" ? e.execution.targets : [])), diff --git a/test/artifact-files.test.ts b/test/artifact-files.test.ts index 754ca2a..c77c55b 100644 --- a/test/artifact-files.test.ts +++ b/test/artifact-files.test.ts @@ -139,3 +139,18 @@ test("remote JS modules witness generated adapters without imposing TypeScript o ); assert.deepEqual(expectedArtifactReceipt(plan, fixture.manifest, generator).receipt.generated, []); }); + +test("transaction process services do not promise generated TypeScript modules", async () => { + const { expectedArtifactReceipt, checkArtifactReceipt } = await import("../src/execution-world/receipts.js"); + const plan = compileExecutionPlan(await loadReaderWorkspace()), + fixture = buildArtifactFixture(plan); + fixture.manifest.implementations = fixture.manifest.implementations.filter( + (i) => i.compatibility === "quixos-service-v1", + ); + const generator = "a".repeat(64), + { receipt } = expectedArtifactReceipt(plan, fixture.manifest, generator); + assert.deepEqual(receipt.generated, []); + await checkArtifactReceipt(plan, fixture.manifest, receipt, generator, async () => { + throw Error("No adapter claimed"); + }); +}); diff --git a/test/emit-managed.ts b/test/emit-managed.ts index 22e59c8..5688fde 100644 --- a/test/emit-managed.ts +++ b/test/emit-managed.ts @@ -6,22 +6,52 @@ import { compileExecutionPlan } from "../src/execution-world/plan.js"; import { compileKernelProgram } from "../src/execution-world/kernel.js"; import { compileQueryProgram } from "../src/execution-world/queries.js"; const source = { repository: "https://example.test/device-owner.git", commit: "a".repeat(40) }; -const pkg = compileCapabilityResourceSource( - `package Owner id "package:owner" revision "package:owner@1" { - function identity id "identity" : int32 -> record { pid: int32; child: int32; } execution remote; +const factory = compileCapabilityResourceSource( + `external atom Entry id "atom:entry"; +interface EntryFactory id "interface:entry-factory" revision "interface:entry-factory@1" { + static operation create id "entry:create" : unit -> atom-ref { + call id "entry:create:call" execution transaction targets [server]; + } }`, { source }, ); -assert.ok(pkg.ok && pkg.resource.kind === "package", JSON.stringify(pkg.diagnostics)); -for (const variant of ["base", "added"]) { - const commit = (variant === "base" ? "a" : "b").repeat(40); +assert.ok(factory.ok && factory.resource.kind === "interface", JSON.stringify(factory.diagnostics)); +for (const variant of ["base", "added", "gateway", "gateway-next"]) { + const gateway = variant.startsWith("gateway"), + commit = { base: "a", added: "b", gateway: "c", "gateway-next": "d" }[variant]!.repeat(40); + const pkg = compileCapabilityResourceSource( + `${gateway ? 'external atom Entry id "atom:entry";' : ""} +package Owner id "package:owner" revision "package:owner@1" { + function identity id "identity" : int32 -> record { pid: int32; child: int32; } execution remote; + ${gateway ? 'function create id "create" : unit -> atom-ref execution transaction targets [server] requires { creation entry id "entry:birth" : Entry input unit; };' : ""} +}`, + { source }, + ); + assert.ok(pkg.ok && pkg.resource.kind === "package", JSON.stringify(pkg.diagnostics)); const result = compileCapabilitySource( `workspace Managed id "workspace:managed-process" revision "workspace:managed-process@${variant}" commit "${commit}" { import package Owner; - ${variant === "added" ? 'atom Independent id "atom:independent";' : ""} + ${variant !== "base" ? 'atom Independent id "atom:independent";' : ""} + ${ + gateway + ? `import interface EntryFactory; atom Entry id "atom:entry"; + conform Entry as EntryFactory id "owner:entry-factory" { + bind create.call to package Owner.create with { entry to creation Entry with {}; }; + }` + : "" + } }`, "workspace.qx", - { packages: new Map([["Owner", pkg.resource.revision]]), packageClosure: [pkg.resource.revision] }, + { + packages: new Map([["Owner", pkg.resource.revision]]), + packageClosure: [pkg.resource.revision], + ...(gateway + ? { + interfaces: new Map([["EntryFactory", factory.resource.revision]]), + interfaceClosure: [factory.resource.revision], + } + : {}), + }, ); assert.ok(result.ok, JSON.stringify(result.diagnostics)); const plan = compileExecutionPlan(result.workspace), @@ -29,6 +59,9 @@ for (const variant of ["base", "added"]) { await mkdir(root, { recursive: true }); for (const [file, value] of Object.entries({ "candidate.json": result.workspace, + "entry.json": gateway + ? { atom: "atom:entry", interface: "interface:entry-factory@1", operation: "entry:create:call" } + : null, "execution-plan.json": plan, "kernel.json": compileKernelProgram(plan), "queries.json": compileQueryProgram(plan),