Separate graph execution identity from installation provenance
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { graphExecutionRepresentation } from "./graph-execution.js";
|
||||
import { open, lstat } from "node:fs/promises";
|
||||
import { constants } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
@@ -24,7 +25,7 @@ export async function loadCheckedInstallationCandidate(
|
||||
) {
|
||||
if (!rootPattern.test(root) || !(await lstat(root)).isDirectory())
|
||||
throw Error("Candidate must be an immutable Nix output directory");
|
||||
const [candidate, plan, artifacts, source, entryRequest, kernel, queries] = await Promise.all([
|
||||
const [candidate, plan, artifacts, source, entryRequest, kernel, queries, graph] = await Promise.all([
|
||||
readJson(root, "candidate.json"),
|
||||
readJson(root, "execution-plan.json"),
|
||||
readJson(root, "execution-artifacts.json"),
|
||||
@@ -32,6 +33,7 @@ export async function loadCheckedInstallationCandidate(
|
||||
readJson(root, "entry.json"),
|
||||
readJson(root, "kernel.json"),
|
||||
readJson(root, "queries.json"),
|
||||
readJson(root, "graph-execution.json"),
|
||||
]);
|
||||
const { executionContracts: _legacyContracts, ...workspace } = candidate;
|
||||
if (
|
||||
@@ -81,12 +83,19 @@ export async function loadCheckedInstallationCandidate(
|
||||
["kernel", kernel, "camino-kernel-program-v1"],
|
||||
["queries", queries, "camino-query-program-v1"],
|
||||
] as const)
|
||||
if (program?.codec !== codec || program.plan !== plan.digest || hexDigest(program) !== artifacts.programs?.[name])
|
||||
if (program?.codec !== codec || program.plan !== kernel.plan || hexDigest(program) !== artifacts.programs?.[name])
|
||||
throw new ExecutionPlanError(
|
||||
"CANDIDATE_PROGRAM_MISMATCH",
|
||||
name,
|
||||
"Stored interpreter input must match the checked build and plan",
|
||||
);
|
||||
const expectedGraph = graphExecutionRepresentation(plan, installation, kernel, queries);
|
||||
if (hexDigest(graph) !== hexDigest(expectedGraph) || artifacts.execution !== hexDigest(graph))
|
||||
throw new ExecutionPlanError(
|
||||
"CANDIDATE_EXECUTION_MISMATCH",
|
||||
root,
|
||||
"Stored graph execution differs from checked inputs",
|
||||
);
|
||||
const provenance = {
|
||||
workspaceRevision: workspace.id as string,
|
||||
source: { repository: source.repository as string, commit: source.commit as string },
|
||||
@@ -110,8 +119,9 @@ export async function loadCheckedInstallationCandidate(
|
||||
return {
|
||||
reference,
|
||||
workspace: plan.definition.workspace.workspaceId,
|
||||
world: installation.world,
|
||||
plan: plan.digest,
|
||||
world: hexDigest(graph),
|
||||
plan: kernel.plan as string,
|
||||
graphExecution: graph,
|
||||
provenance,
|
||||
entry,
|
||||
kernel,
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { ExecutionPlanError, hexDigest, type ExecutionPlan } from "./plan.js";
|
||||
import type { ExecutionInstallation } from "./installation.js";
|
||||
|
||||
/** Execution inputs, not build evidence. Nominal revision IDs remain dispatch
|
||||
* keys: replacing a contract with a structurally similar one is not continuity.
|
||||
* Host placement/policy/credentials are compared separately by execution groups. */
|
||||
export function graphExecutionRepresentation(
|
||||
plan: ExecutionPlan,
|
||||
installation: ExecutionInstallation,
|
||||
kernel: unknown,
|
||||
queries: unknown,
|
||||
) {
|
||||
if (installation.plan !== plan.digest || hexDigest(plan.definition) !== plan.digest)
|
||||
throw new ExecutionPlanError("GRAPH_PLAN_MISMATCH", "graph", "Expected one checked installation");
|
||||
if (
|
||||
!kernel ||
|
||||
typeof kernel !== "object" ||
|
||||
Array.isArray(kernel) ||
|
||||
!queries ||
|
||||
typeof queries !== "object" ||
|
||||
Array.isArray(queries)
|
||||
)
|
||||
throw new ExecutionPlanError("GRAPH_PROGRAM_MISMATCH", "graph", "Expected interpreter program records");
|
||||
const input = kernel as Record<string, unknown>,
|
||||
queryInput = queries as Record<string, unknown>;
|
||||
const { plan: key, ...program } = input;
|
||||
if (
|
||||
input.codec !== "camino-kernel-program-v1" ||
|
||||
queryInput.codec !== "camino-query-program-v1" ||
|
||||
queryInput.plan !== key ||
|
||||
key !== hexDigest({ versions: plan.definition.versions, program })
|
||||
)
|
||||
throw new ExecutionPlanError("GRAPH_PROGRAM_MISMATCH", "graph", "Interpreter inputs do not match execution key");
|
||||
return {
|
||||
codec: "camino-graph-execution-v1",
|
||||
versions: plan.definition.versions,
|
||||
kernel,
|
||||
queries,
|
||||
// Include every selected implementation, including acceptance/remote code
|
||||
// and the browser dependency closure, not merely the public export names.
|
||||
packages: installation.packages.map((pkg) => ({
|
||||
revision: pkg.revision,
|
||||
service: pkg.service ?? null,
|
||||
module: pkg.module ?? null,
|
||||
browser: pkg.browserImplementation ?? null,
|
||||
})),
|
||||
controllers: plan.definition.packages.map((pkg) => ({
|
||||
revision: pkg.revision,
|
||||
controllers: pkg.controllers ?? [],
|
||||
})),
|
||||
shared: {
|
||||
core: installation.coreURL ?? null,
|
||||
engine: installation.engineURL ?? null,
|
||||
components: [
|
||||
...new Map(installation.components.flatMap((component) => Object.entries(component.sharedModules))).entries(),
|
||||
].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -19,3 +19,5 @@ export * from "./candidate.js";
|
||||
export { reviewSemanticMajors } from "../capability-model/evolution.js";
|
||||
|
||||
export * from "./public-reflection.js";
|
||||
|
||||
export * from "./graph-execution.js";
|
||||
|
||||
@@ -112,6 +112,7 @@ export interface ExecutionInstallation {
|
||||
readonly root: string;
|
||||
readonly service?: ArtifactImplementation;
|
||||
readonly module?: ArtifactImplementation;
|
||||
readonly browserImplementation?: ArtifactImplementation;
|
||||
readonly browserURL?: string;
|
||||
readonly browserRegistry?: string;
|
||||
}[];
|
||||
@@ -270,6 +271,7 @@ function assembleExecutionInstallation(
|
||||
root,
|
||||
...(service ? { service } : {}),
|
||||
...(module ? { module } : {}),
|
||||
browserImplementation: browser,
|
||||
browserURL: contentArtifactUrl(entry),
|
||||
browserRegistry: browser.registryExport,
|
||||
});
|
||||
|
||||
@@ -36,7 +36,7 @@ export function compileKernelProgram(plan: ExecutionPlan): unknown {
|
||||
}
|
||||
};
|
||||
const ownerBySlot = new Map<string, string>();
|
||||
const edges: unknown[] = [];
|
||||
const edges: { id: string; owner: string; a: unknown; b: unknown }[] = [];
|
||||
const owners = plan.definition.owners.map((owner) => {
|
||||
const slots: unknown[] = [];
|
||||
for (const storage of owner.storage as PersistentAttachment[]) {
|
||||
@@ -166,7 +166,19 @@ export function compileKernelProgram(plan: ExecutionPlan): unknown {
|
||||
kind: "creation",
|
||||
atom: bound.atomId,
|
||||
schema: portableValueSchema(requirement.inputType),
|
||||
recipe: hexDigest({ plan: plan.digest, operation: operation.key, port: dependency.portId }),
|
||||
recipe: hexDigest({
|
||||
codec: "camino-creation-recipe-v1",
|
||||
versions: plan.definition.versions,
|
||||
operation: operation.key,
|
||||
export: entry.id,
|
||||
port: dependency.portId,
|
||||
requirement,
|
||||
binding: bound,
|
||||
owners: owners.filter((owner) => owner.atom === bound.atomId),
|
||||
edges: edges.filter((edge) =>
|
||||
owners.some((owner) => owner.atom === bound.atomId && owner.id === edge.owner),
|
||||
),
|
||||
}),
|
||||
owners: bound.owners.map((seed) => {
|
||||
const owner = owners.find((o) => o.atom === bound.atomId && o.interface === seed.interfaceRevisionId);
|
||||
if (!owner)
|
||||
@@ -230,11 +242,12 @@ export function compileKernelProgram(plan: ExecutionPlan): unknown {
|
||||
output: schema(entry.outputType),
|
||||
})),
|
||||
}));
|
||||
return {
|
||||
const body = {
|
||||
codec: "camino-kernel-program-v1",
|
||||
plan: plan.digest,
|
||||
model: { workspace: workspace.workspaceId, atoms: workspace.atoms.map((atom) => atom.id), owners, edges },
|
||||
operations,
|
||||
packages,
|
||||
};
|
||||
// The interpreter's plan key covers its real input, not source/build evidence.
|
||||
return { ...body, plan: hexDigest({ versions: plan.definition.versions, program: body }) };
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { compileKernelProgram } from "./kernel.js";
|
||||
import { compileInspectionTemplates } from "./inspection.js";
|
||||
import { portableValueSchema } from "../bindings/portable.js";
|
||||
import { ExecutionPlanError, hexDigest, type ExecutionPlan } from "./plan.js";
|
||||
@@ -26,7 +27,7 @@ export function compileQueryProgram(plan: ExecutionPlan): unknown {
|
||||
const interfaces = plan.definition.workspace.interfaceImports;
|
||||
return {
|
||||
codec: "camino-query-program-v1",
|
||||
plan: plan.digest,
|
||||
plan: (compileKernelProgram(plan) as { plan: string }).plan,
|
||||
queries: [
|
||||
...compileInspectionTemplates(plan).queries,
|
||||
...plan.definition.queries.map(({ id, query, admittedOperations }) => {
|
||||
|
||||
Reference in New Issue
Block a user