Reuse verified immutable plans and streamline cooperative hashing
Continue final hosted development reliability qualification.
This commit is contained in:
@@ -26,43 +26,80 @@ export const contentDigest = (value: unknown) =>
|
|||||||
* event loop while verifying large immutable metadata. No validation is skipped.
|
* event loop while verifying large immutable metadata. No validation is skipped.
|
||||||
* This is for asynchronous loading, not a different build/wire identity. */
|
* This is for asynchronous loading, not a different build/wire identity. */
|
||||||
export async function contentDigestAsync(value: unknown): Promise<string> {
|
export async function contentDigestAsync(value: unknown): Promise<string> {
|
||||||
function* parts(value: unknown): Generator<string> {
|
// An explicit stack visits each node once; recursive generator delegation
|
||||||
if (Array.isArray(value)) {
|
// otherwise forwards every token through every ancestor of a deep plan.
|
||||||
yield "[";
|
type Frame = { kind: "value" | "array" | "object"; value: unknown; index?: number; keys?: string[]; wrote?: boolean };
|
||||||
for (let index = 0; index < value.length; index++) {
|
const hash = createHash("sha256"),
|
||||||
if (index) yield ",";
|
stack: Frame[] = [{ kind: "value", value }],
|
||||||
// Match canonicalJson's Array.map behavior for sparse arrays too.
|
active = new WeakSet<object>();
|
||||||
if (index in value) yield* parts(value[index]);
|
|
||||||
}
|
|
||||||
yield "]";
|
|
||||||
} else if (value !== null && typeof value === "object") {
|
|
||||||
if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)
|
|
||||||
throw new Error("Expected a plain JSON object");
|
|
||||||
yield "{";
|
|
||||||
let first = true;
|
|
||||||
for (const key of Object.keys(value).sort(compareText)) {
|
|
||||||
const entry = (value as Record<string, unknown>)[key];
|
|
||||||
if (entry === undefined) continue;
|
|
||||||
if (!first) yield ",";
|
|
||||||
first = false;
|
|
||||||
yield JSON.stringify(key) + ":";
|
|
||||||
yield* parts(entry);
|
|
||||||
}
|
|
||||||
yield "}";
|
|
||||||
} else yield canonicalJson(value);
|
|
||||||
}
|
|
||||||
const hash = createHash("sha256");
|
|
||||||
let buffer = "",
|
let buffer = "",
|
||||||
sliceStarted = performance.now();
|
sliceStarted = performance.now(),
|
||||||
for (const part of parts(value)) {
|
steps = 0;
|
||||||
|
const append = (part: string) => {
|
||||||
buffer += part;
|
buffer += part;
|
||||||
if (buffer.length >= 65536) {
|
if (buffer.length >= 65536) {
|
||||||
hash.update(buffer);
|
hash.update(buffer);
|
||||||
buffer = "";
|
buffer = "";
|
||||||
if (performance.now() - sliceStarted >= 8) {
|
}
|
||||||
await setImmediate();
|
};
|
||||||
sliceStarted = performance.now();
|
while (stack.length) {
|
||||||
|
const frame = stack[stack.length - 1]!;
|
||||||
|
if (frame.kind === "value") {
|
||||||
|
const value = frame.value;
|
||||||
|
if (value !== null && typeof value === "object") {
|
||||||
|
if (active.has(value)) throw new Error("Cannot hash cyclic JSON values");
|
||||||
|
active.add(value);
|
||||||
|
frame.index = 0;
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
frame.kind = "array";
|
||||||
|
append("[");
|
||||||
|
} else {
|
||||||
|
if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)
|
||||||
|
throw new Error("Expected a plain JSON object");
|
||||||
|
frame.kind = "object";
|
||||||
|
frame.keys = Object.keys(value).sort(compareText);
|
||||||
|
frame.wrote = false;
|
||||||
|
append("{");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
append(canonicalJson(value));
|
||||||
|
stack.pop();
|
||||||
}
|
}
|
||||||
|
} else if (frame.kind === "array") {
|
||||||
|
const array = frame.value as unknown[];
|
||||||
|
if (frame.index === array.length) {
|
||||||
|
append("]");
|
||||||
|
active.delete(array);
|
||||||
|
stack.pop();
|
||||||
|
} else {
|
||||||
|
const index = frame.index!;
|
||||||
|
frame.index = index + 1;
|
||||||
|
if (index) append(",");
|
||||||
|
// Match canonicalJson's Array.map behavior for sparse arrays too.
|
||||||
|
if (index in array) stack.push({ kind: "value", value: array[index] });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const object = frame.value as Record<string, unknown>;
|
||||||
|
if (frame.index === frame.keys!.length) {
|
||||||
|
append("}");
|
||||||
|
active.delete(object);
|
||||||
|
stack.pop();
|
||||||
|
} else {
|
||||||
|
const index = frame.index!;
|
||||||
|
frame.index = index + 1;
|
||||||
|
const key = frame.keys![index]!,
|
||||||
|
entry = object[key];
|
||||||
|
if (entry !== undefined) {
|
||||||
|
if (frame.wrote) append(",");
|
||||||
|
frame.wrote = true;
|
||||||
|
append(JSON.stringify(key) + ":");
|
||||||
|
stack.push({ kind: "value", value: entry });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (++steps % 512 === 0 && performance.now() - sliceStarted >= 8) {
|
||||||
|
await setImmediate();
|
||||||
|
sliceStarted = performance.now();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
hash.update(buffer);
|
hash.update(buffer);
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { createHash } from "node:crypto";
|
|
||||||
import { canonicalJson } from "../capability-model/evolution.js";
|
|
||||||
import {
|
import {
|
||||||
|
executionPlanDigestAsync,
|
||||||
type ExecutionPlan,
|
type ExecutionPlan,
|
||||||
type PlanPackage,
|
type PlanPackage,
|
||||||
ExecutionPlanError,
|
ExecutionPlanError,
|
||||||
hexDigest,
|
hexDigest,
|
||||||
hexDigestAsync,
|
|
||||||
executionVersions,
|
executionVersions,
|
||||||
} from "./plan.js";
|
} from "./plan.js";
|
||||||
|
import { createHash } from "node:crypto";
|
||||||
|
import { canonicalJson } from "../capability-model/evolution.js";
|
||||||
|
|
||||||
export interface ArtifactFile {
|
export interface ArtifactFile {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -366,7 +366,7 @@ export async function checkExecutionWorld(
|
|||||||
): Promise<CheckedWorld> {
|
): Promise<CheckedWorld> {
|
||||||
if (canonicalJson(plan.definition.versions) !== canonicalJson(executionVersions))
|
if (canonicalJson(plan.definition.versions) !== canonicalJson(executionVersions))
|
||||||
fail("world", "Unsupported plan codec versions");
|
fail("world", "Unsupported plan codec versions");
|
||||||
if ((await hexDigestAsync(plan.definition)) !== plan.digest) fail("plan", "Plan digest mismatch");
|
if ((await executionPlanDigestAsync(plan)) !== plan.digest) fail("plan", "Plan digest mismatch");
|
||||||
const manifests = inputs.map(parsePackageArtifacts);
|
const manifests = inputs.map(parsePackageArtifacts);
|
||||||
unique(
|
unique(
|
||||||
manifests.map((m) => m.packageRevisionId),
|
manifests.map((m) => m.packageRevisionId),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { graphExecutionRepresentation } from "./graph-execution.js";
|
import { graphExecutionRepresentation } from "./graph-execution.js";
|
||||||
import { lstat } from "node:fs/promises";
|
import { lstat } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { hexDigest, hexDigestAsync, ExecutionPlanError, type ExecutionPlan } from "./plan.js";
|
import { hexDigest, hexDigestAsync, sealExecutionPlan, ExecutionPlanError, type ExecutionPlan } from "./plan.js";
|
||||||
import {
|
import {
|
||||||
loadExecutionInstallation,
|
loadExecutionInstallation,
|
||||||
createClientContractCatalog,
|
createClientContractCatalog,
|
||||||
@@ -72,7 +72,7 @@ export async function loadCheckedInstallationCandidate(
|
|||||||
const [plan, artifacts, source, entryRequest, kernel, queries] = values;
|
const [plan, artifacts, source, entryRequest, kernel, queries] = values;
|
||||||
if (
|
if (
|
||||||
plan?.definition?.codec !== "camino-execution-plan-v1" ||
|
plan?.definition?.codec !== "camino-execution-plan-v1" ||
|
||||||
(await hexDigestAsync(plan.definition)) !== plan.digest ||
|
!(await sealExecutionPlan(plan)) ||
|
||||||
workspaceDigest !== (await hexDigestAsync(plan.definition.workspace))
|
workspaceDigest !== (await hexDigestAsync(plan.definition.workspace))
|
||||||
)
|
)
|
||||||
throw new ExecutionPlanError(
|
throw new ExecutionPlanError(
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { executionPlanDigest, type ExecutionPlan, resolvePlannedOperation, ExecutionPlanError } from "./plan.js";
|
||||||
import type {
|
import type {
|
||||||
InterfaceRevision,
|
InterfaceRevision,
|
||||||
InterfaceOperation,
|
InterfaceOperation,
|
||||||
@@ -6,7 +7,7 @@ import type {
|
|||||||
} from "../capability-model/types.js";
|
} from "../capability-model/types.js";
|
||||||
import { canonicalJson } from "../capability-model/evolution.js";
|
import { canonicalJson } from "../capability-model/evolution.js";
|
||||||
import { portableValueSchema, tsType } from "../bindings/portable.js";
|
import { portableValueSchema, tsType } from "../bindings/portable.js";
|
||||||
import { type ExecutionPlan, resolvePlannedOperation, hexDigest, ExecutionPlanError } from "./plan.js";
|
|
||||||
import type { BindingSchema } from "../bindings/index.js";
|
import type { BindingSchema } from "../bindings/index.js";
|
||||||
import type { PackageBuildContract } from "./build-contract.js";
|
import type { PackageBuildContract } from "./build-contract.js";
|
||||||
import { preparationArtifact, type ReactPreparationArtifact } from "../react-build/declarations.js";
|
import { preparationArtifact, type ReactPreparationArtifact } from "../react-build/declarations.js";
|
||||||
@@ -55,7 +56,7 @@ export function publicOperationContract(
|
|||||||
}
|
}
|
||||||
/** Resolve a public contract to internal dispatch. This does not authorize the receiver. */
|
/** Resolve a public contract to internal dispatch. This does not authorize the receiver. */
|
||||||
export function linkInterfaceOperation(plan: ExecutionPlan, atom: string, contract: PublicOperationContract) {
|
export function linkInterfaceOperation(plan: ExecutionPlan, atom: string, contract: PublicOperationContract) {
|
||||||
if (hexDigest(plan.definition) !== plan.digest)
|
if (executionPlanDigest(plan) !== plan.digest)
|
||||||
throw new ExecutionPlanError("INVALID_PLAN", atom, "Invalid execution plan digest");
|
throw new ExecutionPlanError("INVALID_PLAN", atom, "Invalid execution plan digest");
|
||||||
const iface = plan.definition.workspace.interfaceImports.find((i) => i.revisionId === contract.interface);
|
const iface = plan.definition.workspace.interfaceImports.find((i) => i.revisionId === contract.interface);
|
||||||
const operation = iface?.members.flatMap((m) => m.operations).find((o) => o.id === contract.operation);
|
const operation = iface?.members.flatMap((m) => m.operations).find((o) => o.id === contract.operation);
|
||||||
@@ -97,7 +98,7 @@ export function publicQueryContract(pkg: PackageRevision, queryId: string): Publ
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
export function linkPackageQuery(plan: ExecutionPlan, contract: PublicQueryContract) {
|
export function linkPackageQuery(plan: ExecutionPlan, contract: PublicQueryContract) {
|
||||||
if (hexDigest(plan.definition) !== plan.digest)
|
if (executionPlanDigest(plan) !== plan.digest)
|
||||||
throw new ExecutionPlanError("INVALID_PLAN", contract.query, "Invalid execution plan digest");
|
throw new ExecutionPlanError("INVALID_PLAN", contract.query, "Invalid execution plan digest");
|
||||||
const pkg = plan.definition.workspace.packageImports.find((p) => p.revisionId === contract.package);
|
const pkg = plan.definition.workspace.packageImports.find((p) => p.revisionId === contract.package);
|
||||||
if (!pkg || canonicalJson(publicQueryContract(pkg, contract.query)) !== canonicalJson(contract))
|
if (!pkg || canonicalJson(publicQueryContract(pkg, contract.query)) !== canonicalJson(contract))
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { executionPlanDigestAsync, type ExecutionPlan, hexDigest, hexDigestAsync, ExecutionPlanError } from "./plan.js";
|
||||||
import { portableReflectionValue } from "./reflection.js";
|
import { portableReflectionValue } from "./reflection.js";
|
||||||
import type {
|
import type {
|
||||||
BoundDependency,
|
BoundDependency,
|
||||||
@@ -6,7 +7,6 @@ import type {
|
|||||||
PersistentAttachment,
|
PersistentAttachment,
|
||||||
} from "../capability-model/types.js";
|
} from "../capability-model/types.js";
|
||||||
import { type ExecutionInstallation, type ExecutionGroupIdentity } from "./installation.js";
|
import { type ExecutionInstallation, type ExecutionGroupIdentity } from "./installation.js";
|
||||||
import { type ExecutionPlan, hexDigest, hexDigestAsync, ExecutionPlanError } from "./plan.js";
|
|
||||||
|
|
||||||
export interface ExecutionEnvironment {
|
export interface ExecutionEnvironment {
|
||||||
/** Explicit host placement. Merely including an optional module does not select it. */
|
/** Explicit host placement. Merely including an optional module does not select it. */
|
||||||
@@ -28,7 +28,7 @@ export async function compileExecutionGroups(
|
|||||||
installation: ExecutionInstallation,
|
installation: ExecutionInstallation,
|
||||||
environments: Readonly<Record<string, ExecutionEnvironment>>,
|
environments: Readonly<Record<string, ExecutionEnvironment>>,
|
||||||
): Promise<InstalledExecutionGroup[]> {
|
): Promise<InstalledExecutionGroup[]> {
|
||||||
if (installation.plan !== plan.digest || (await hexDigestAsync(plan.definition)) !== plan.digest)
|
if (installation.plan !== plan.digest || (await executionPlanDigestAsync(plan)) !== plan.digest)
|
||||||
throw new ExecutionPlanError("GROUP_PLAN_MISMATCH", plan.digest, "Expected one checked installation");
|
throw new ExecutionPlanError("GROUP_PLAN_MISMATCH", plan.digest, "Expected one checked installation");
|
||||||
const workspace = plan.definition.workspace;
|
const workspace = plan.definition.workspace;
|
||||||
const packages = new Map(workspace.packageImports.map((p) => [p.revisionId as string, p]));
|
const packages = new Map(workspace.packageImports.map((p) => [p.revisionId as string, p]));
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ExecutionPlanError, hexDigest, type ExecutionPlan } from "./plan.js";
|
import { executionPlanDigest, ExecutionPlanError, hexDigest, type ExecutionPlan } from "./plan.js";
|
||||||
|
|
||||||
import type { ExecutionInstallation } from "./installation.js";
|
import type { ExecutionInstallation } from "./installation.js";
|
||||||
|
|
||||||
/** Execution inputs, not build evidence. Nominal revision IDs remain dispatch
|
/** Execution inputs, not build evidence. Nominal revision IDs remain dispatch
|
||||||
@@ -10,7 +11,7 @@ export function graphExecutionRepresentation(
|
|||||||
kernel: unknown,
|
kernel: unknown,
|
||||||
queries: unknown,
|
queries: unknown,
|
||||||
) {
|
) {
|
||||||
if (installation.plan !== plan.digest || hexDigest(plan.definition) !== plan.digest)
|
if (installation.plan !== plan.digest || executionPlanDigest(plan) !== plan.digest)
|
||||||
throw new ExecutionPlanError("GRAPH_PLAN_MISMATCH", "graph", "Expected one checked installation");
|
throw new ExecutionPlanError("GRAPH_PLAN_MISMATCH", "graph", "Expected one checked installation");
|
||||||
if (
|
if (
|
||||||
!kernel ||
|
!kernel ||
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
import { executionPlanDigestAsync, ExecutionPlanError, type ExecutionPlan } from "./plan.js";
|
||||||
import { compileInspectionTemplates, type InspectionTemplate, type InspectionKernel } from "./inspection.js";
|
import { compileInspectionTemplates, type InspectionTemplate, type InspectionKernel } from "./inspection.js";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { loadCheckedArtifactWorld } from "./receipts.js";
|
import { loadCheckedArtifactWorld } from "./receipts.js";
|
||||||
import { contentArtifactUrl, loadArtifactWorld, type LoadedArtifactWorld } from "./files.js";
|
import { contentArtifactUrl, loadArtifactWorld, type LoadedArtifactWorld } from "./files.js";
|
||||||
import { selectArtifactImplementation, type ArtifactImplementation, type ComponentArtifact } from "./artifacts.js";
|
import { selectArtifactImplementation, type ArtifactImplementation, type ComponentArtifact } from "./artifacts.js";
|
||||||
import { ExecutionPlanError, hexDigestAsync, type ExecutionPlan } from "./plan.js";
|
|
||||||
import {
|
import {
|
||||||
publicOperationContract,
|
publicOperationContract,
|
||||||
publicQueryContract,
|
publicQueryContract,
|
||||||
@@ -47,7 +48,7 @@ export async function clientContractCatalog(
|
|||||||
requestedPackages: readonly string[],
|
requestedPackages: readonly string[],
|
||||||
checkedKernel?: InspectionKernel,
|
checkedKernel?: InspectionKernel,
|
||||||
): Promise<ClientContractCatalog> {
|
): Promise<ClientContractCatalog> {
|
||||||
if (installation.plan !== plan.digest || (await hexDigestAsync(plan.definition)) !== plan.digest)
|
if (installation.plan !== plan.digest || (await executionPlanDigestAsync(plan)) !== plan.digest)
|
||||||
throw new ExecutionPlanError(
|
throw new ExecutionPlanError(
|
||||||
"CATALOG_PLAN_MISMATCH",
|
"CATALOG_PLAN_MISMATCH",
|
||||||
plan.digest,
|
plan.digest,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { executionPlanDigest, type ExecutionPlan, ExecutionPlanError, hexDigest } from "./plan.js";
|
||||||
import { portableReflectionType, portableReflectionValue } from "./reflection.js";
|
import { portableReflectionType, portableReflectionValue } from "./reflection.js";
|
||||||
import type {
|
import type {
|
||||||
ValueType,
|
ValueType,
|
||||||
@@ -7,7 +8,7 @@ import type {
|
|||||||
BoundDependency,
|
BoundDependency,
|
||||||
PackageExport,
|
PackageExport,
|
||||||
} from "../capability-model/types.js";
|
} from "../capability-model/types.js";
|
||||||
import { type ExecutionPlan, ExecutionPlanError, hexDigest } from "./plan.js";
|
|
||||||
import { portableValueSchema } from "../bindings/portable.js";
|
import { portableValueSchema } from "../bindings/portable.js";
|
||||||
import { subscriptionContract } from "../capability-model/journals.js";
|
import { subscriptionContract } from "../capability-model/journals.js";
|
||||||
type PreparedStep = string | null | { case: string; id: string };
|
type PreparedStep = string | null | { case: string; id: string };
|
||||||
@@ -25,7 +26,7 @@ function preparedInputPaths(type: ValueType, path: PreparedStep[] = []): Prepare
|
|||||||
}
|
}
|
||||||
/** Small data-only interpreter input; no compiler, parser, Node module or SQL dependency reaches the browser. */
|
/** Small data-only interpreter input; no compiler, parser, Node module or SQL dependency reaches the browser. */
|
||||||
export function compileKernelProgram(plan: ExecutionPlan): unknown {
|
export function compileKernelProgram(plan: ExecutionPlan): unknown {
|
||||||
if (hexDigest(plan.definition) !== plan.digest)
|
if (executionPlanDigest(plan) !== plan.digest)
|
||||||
throw new ExecutionPlanError("INVALID_PLAN", "kernel", "Plan digest mismatch");
|
throw new ExecutionPlanError("INVALID_PLAN", "kernel", "Plan digest mismatch");
|
||||||
const workspace = plan.definition.workspace;
|
const workspace = plan.definition.workspace;
|
||||||
if (workspace.sharedAttachments.some((attachment) => attachment.kind !== "edge"))
|
if (workspace.sharedAttachments.some((attachment) => attachment.kind !== "edge"))
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { executionPlanDigest, ExecutionPlanError, hexDigest, type ExecutionPlan } from "./plan.js";
|
||||||
import { parse } from "@babel/parser";
|
import { parse } from "@babel/parser";
|
||||||
import { builtinModules } from "node:module";
|
import { builtinModules } from "node:module";
|
||||||
import { ExecutionPlanError, hexDigest, type ExecutionPlan } from "./plan.js";
|
|
||||||
import type { ExecutionInstallation } from "./installation.js";
|
import type { ExecutionInstallation } from "./installation.js";
|
||||||
import type { ArtifactImplementation, ArtifactReader } from "./artifacts.js";
|
import type { ArtifactImplementation, ArtifactReader } from "./artifacts.js";
|
||||||
|
|
||||||
@@ -212,7 +213,7 @@ export async function reportExecutionPlacement(
|
|||||||
installation: ExecutionInstallation,
|
installation: ExecutionInstallation,
|
||||||
read: ArtifactReader,
|
read: ArtifactReader,
|
||||||
) {
|
) {
|
||||||
if (installation.plan !== plan.digest || hexDigest(plan.definition) !== plan.digest)
|
if (installation.plan !== plan.digest || executionPlanDigest(plan) !== plan.digest)
|
||||||
throw new ExecutionPlanError(
|
throw new ExecutionPlanError(
|
||||||
"PLACEMENT_PLAN_MISMATCH",
|
"PLACEMENT_PLAN_MISMATCH",
|
||||||
plan.digest,
|
plan.digest,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { linkQueries, type LinkedQuery } from "../query/link.js";
|
|||||||
import { type BindingSchema } from "../bindings/index.js";
|
import { type BindingSchema } from "../bindings/index.js";
|
||||||
import { unsupportedQueryOperator } from "./query-operators.js";
|
import { unsupportedQueryOperator } from "./query-operators.js";
|
||||||
import { packageBuildContexts } from "./build-contract.js";
|
import { packageBuildContexts } from "./build-contract.js";
|
||||||
|
import { setImmediate } from "node:timers/promises";
|
||||||
|
|
||||||
export class ExecutionPlanError extends Error {
|
export class ExecutionPlanError extends Error {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -31,6 +32,54 @@ export class ExecutionPlanError extends Error {
|
|||||||
export const hexDigest = (value: unknown): string => contentDigest(value).slice("sha256:".length);
|
export const hexDigest = (value: unknown): string => contentDigest(value).slice("sha256:".length);
|
||||||
export const hexDigestAsync = async (value: unknown): Promise<string> =>
|
export const hexDigestAsync = async (value: unknown): Promise<string> =>
|
||||||
(await contentDigestAsync(value)).slice("sha256:".length);
|
(await contentDigestAsync(value)).slice("sha256:".length);
|
||||||
|
// Checked runtime metadata is owned JSON, not live Graph data. Reuse its proof
|
||||||
|
// only after every reachable record is immutable. Weak keys retain neither old
|
||||||
|
// Candidates nor an application cache; unsealed compiler inputs are rehashed.
|
||||||
|
const sealedPlanDigests = new WeakMap<ExecutionPlan, string>();
|
||||||
|
export const executionPlanDigest = (plan: ExecutionPlan): string =>
|
||||||
|
sealedPlanDigests.get(plan) ?? hexDigest(plan.definition);
|
||||||
|
export const executionPlanDigestAsync = async (plan: ExecutionPlan): Promise<string> =>
|
||||||
|
sealedPlanDigests.get(plan) ?? (await hexDigestAsync(plan.definition));
|
||||||
|
/** Transfers owned parsed JSON into immutable runtime metadata. Freeze before
|
||||||
|
* yielding to hashing so concurrent mutation cannot invalidate the proof. */
|
||||||
|
export async function sealExecutionPlan(plan: ExecutionPlan): Promise<boolean> {
|
||||||
|
if (sealedPlanDigests.has(plan)) return true;
|
||||||
|
if (!Object.hasOwn(plan, "definition") || !Object.hasOwn(plan, "digest")) return false;
|
||||||
|
const pending: object[] = [plan],
|
||||||
|
seen = new WeakSet<object>();
|
||||||
|
let sliceStarted = performance.now(),
|
||||||
|
count = 0;
|
||||||
|
while (pending.length) {
|
||||||
|
const value = pending.pop()!;
|
||||||
|
if (seen.has(value)) continue;
|
||||||
|
if (
|
||||||
|
!Array.isArray(value) &&
|
||||||
|
Object.getPrototypeOf(value) !== Object.prototype &&
|
||||||
|
Object.getPrototypeOf(value) !== null
|
||||||
|
)
|
||||||
|
throw new ExecutionPlanError("INVALID_PLAN_JSON", "plan", "Expected owned JSON records");
|
||||||
|
for (const key of Reflect.ownKeys(value)) {
|
||||||
|
const descriptor = Object.getOwnPropertyDescriptor(value, key)!;
|
||||||
|
if (typeof key !== "string" || !Object.hasOwn(descriptor, "value"))
|
||||||
|
throw new ExecutionPlanError(
|
||||||
|
"INVALID_PLAN_JSON",
|
||||||
|
"plan",
|
||||||
|
"Runtime metadata cannot contain accessors or symbols",
|
||||||
|
);
|
||||||
|
if (descriptor.value !== null && typeof descriptor.value === "object") pending.push(descriptor.value);
|
||||||
|
}
|
||||||
|
Object.freeze(value);
|
||||||
|
seen.add(value);
|
||||||
|
if (++count % 1024 === 0 && performance.now() - sliceStarted >= 8) {
|
||||||
|
await setImmediate();
|
||||||
|
sliceStarted = performance.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const digest = await hexDigestAsync(plan.definition);
|
||||||
|
if (digest !== plan.digest) return false;
|
||||||
|
sealedPlanDigests.set(plan, digest);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
const sorted = <T>(values: readonly T[], key: (value: T) => string): T[] =>
|
const sorted = <T>(values: readonly T[], key: (value: T) => string): T[] =>
|
||||||
[...values].sort((a, b) => (key(a) < key(b) ? -1 : key(a) > key(b) ? 1 : 0));
|
[...values].sort((a, b) => (key(a) < key(b) ? -1 : key(a) > key(b) ? 1 : 0));
|
||||||
export const publicOperationKey = (atom: string, iface: string, operation: string): string =>
|
export const publicOperationKey = (atom: string, iface: string, operation: string): string =>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ExecutionPlanError, hexDigest, type ExecutionPlan, type PlannedQuery } from "./plan.js";
|
import { executionPlanDigest, ExecutionPlanError, type ExecutionPlan, type PlannedQuery } from "./plan.js";
|
||||||
|
|
||||||
/** Contracts promised by a direct result. Interface dependencies are explicit; accidental
|
/** Contracts promised by a direct result. Interface dependencies are explicit; accidental
|
||||||
* overlap among today's implementations does not silently widen an exported interface contract. */
|
* overlap among today's implementations does not silently widen an exported interface contract. */
|
||||||
export function resultInterfaces(plan: ExecutionPlan, operationKey: string): string[] {
|
export function resultInterfaces(plan: ExecutionPlan, operationKey: string): string[] {
|
||||||
@@ -23,7 +24,7 @@ export function resultInterfaces(plan: ExecutionPlan, operationKey: string): str
|
|||||||
}
|
}
|
||||||
/** Compile-time observeResult compatibility. Observation does not grant read or mutation authority. */
|
/** Compile-time observeResult compatibility. Observation does not grant read or mutation authority. */
|
||||||
export function checkResultProjection(plan: ExecutionPlan, operationKey: string, queryId: string): PlannedQuery {
|
export function checkResultProjection(plan: ExecutionPlan, operationKey: string, queryId: string): PlannedQuery {
|
||||||
if (hexDigest(plan.definition) !== plan.digest)
|
if (executionPlanDigest(plan) !== plan.digest)
|
||||||
throw new ExecutionPlanError("INVALID_PLAN", "projection", "Plan digest mismatch");
|
throw new ExecutionPlanError("INVALID_PLAN", "projection", "Plan digest mismatch");
|
||||||
const query = plan.definition.queries.find((q) => q.id === queryId);
|
const query = plan.definition.queries.find((q) => q.id === queryId);
|
||||||
if (!query) throw new ExecutionPlanError("UNKNOWN_PROJECTION", queryId, "No installed query");
|
if (!query) throw new ExecutionPlanError("UNKNOWN_PROJECTION", queryId, "No installed query");
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import { executionPlanDigest, ExecutionPlanError, type ExecutionPlan } from "./plan.js";
|
||||||
import { compileKernelProgram } from "./kernel.js";
|
import { compileKernelProgram } from "./kernel.js";
|
||||||
import { compileInspectionTemplates } from "./inspection.js";
|
import { compileInspectionTemplates } from "./inspection.js";
|
||||||
import { portableValueSchema } from "../bindings/portable.js";
|
import { portableValueSchema } from "../bindings/portable.js";
|
||||||
import { ExecutionPlanError, hexDigest, type ExecutionPlan } from "./plan.js";
|
|
||||||
import type { ValueType } from "../capability-model/types.js";
|
import type { ValueType } from "../capability-model/types.js";
|
||||||
import { compileQueryComposition } from "../query/composition.js";
|
import { compileQueryComposition } from "../query/composition.js";
|
||||||
import { queryTreeBlueprint } from "./query-trees.js";
|
import { queryTreeBlueprint } from "./query-trees.js";
|
||||||
@@ -24,7 +25,7 @@ const lowerTypes = (value: unknown): unknown =>
|
|||||||
/** Checked query IR and installed getter identities, without GraphQL schemas/parsers or SQL adapters.
|
/** Checked query IR and installed getter identities, without GraphQL schemas/parsers or SQL adapters.
|
||||||
* Keep authored selection names only for output paths; field access uses resolved interface/member IDs. */
|
* Keep authored selection names only for output paths; field access uses resolved interface/member IDs. */
|
||||||
export function compileQueryProgram(plan: ExecutionPlan): unknown {
|
export function compileQueryProgram(plan: ExecutionPlan): unknown {
|
||||||
if (hexDigest(plan.definition) !== plan.digest)
|
if (executionPlanDigest(plan) !== plan.digest)
|
||||||
throw new ExecutionPlanError("INVALID_PLAN", "queries", "Plan digest mismatch");
|
throw new ExecutionPlanError("INVALID_PLAN", "queries", "Plan digest mismatch");
|
||||||
const allQueries = new Map(plan.definition.queries.map(({ id, query }) => [id, query.checked]));
|
const allQueries = new Map(plan.definition.queries.map(({ id, query }) => [id, query.checked]));
|
||||||
const compositions = plan.definition.workspace.packageImports.flatMap((pkg) =>
|
const compositions = plan.definition.workspace.packageImports.flatMap((pkg) =>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import { executionPlanDigest, hexDigest, type ExecutionPlan, type PlannedQuery } from "./plan.js";
|
||||||
import { compileQuery } from "../query/compile.js";
|
import { compileQuery } from "../query/compile.js";
|
||||||
import { linkQueries } from "../query/link.js";
|
import { linkQueries } from "../query/link.js";
|
||||||
import { capabilityId } from "../capability-model/types.js";
|
import { capabilityId } from "../capability-model/types.js";
|
||||||
import { portableValueSchema } from "../bindings/portable.js";
|
import { portableValueSchema } from "../bindings/portable.js";
|
||||||
import { hexDigest, type ExecutionPlan, type PlannedQuery } from "./plan.js";
|
|
||||||
import { compileKernelProgram } from "./kernel.js";
|
import { compileKernelProgram } from "./kernel.js";
|
||||||
import { queryProgramEntry } from "./queries.js";
|
import { queryProgramEntry } from "./queries.js";
|
||||||
import { unsupportedQueryOperator } from "./query-operators.js";
|
import { unsupportedQueryOperator } from "./query-operators.js";
|
||||||
@@ -17,7 +18,7 @@ export interface QueryDocument {
|
|||||||
/** Runtime query documents use the same language, nominal contracts, linking
|
/** Runtime query documents use the same language, nominal contracts, linking
|
||||||
* and interpreter as packaged queries. They introduce no resources or code. */
|
* and interpreter as packaged queries. They introduce no resources or code. */
|
||||||
export async function compilePublicQuery(plan: ExecutionPlan, request: QueryDocument) {
|
export async function compilePublicQuery(plan: ExecutionPlan, request: QueryDocument) {
|
||||||
if (hexDigest(plan.definition) !== plan.digest) throw Error("INVALID_PLAN");
|
if (executionPlanDigest(plan) !== plan.digest) throw Error("INVALID_PLAN");
|
||||||
if (
|
if (
|
||||||
typeof request.document !== "string" ||
|
typeof request.document !== "string" ||
|
||||||
Buffer.byteLength(request.document) > 32768 ||
|
Buffer.byteLength(request.document) > 32768 ||
|
||||||
|
|||||||
+13
-1
@@ -55,10 +55,22 @@ test("hashing is canonical and rejects values outside the persisted JSON contrac
|
|||||||
test("cooperative hashing preserves canonical identities and services the event loop", async () => {
|
test("cooperative hashing preserves canonical identities and services the event loop", async () => {
|
||||||
const sparse = [1, 2, 3];
|
const sparse = [1, 2, 3];
|
||||||
delete sparse[1];
|
delete sparse[1];
|
||||||
const values = [null, false, -0, "中\\n\ud800", sparse, { z: 1, a: { omitted: undefined, b: [false, 1e100] } }];
|
const values: unknown[] = [
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
-0,
|
||||||
|
"中\\n\ud800",
|
||||||
|
sparse,
|
||||||
|
{ z: 1, a: { omitted: undefined, b: [false, 1e100] } },
|
||||||
|
];
|
||||||
|
const shared = { text: "中😀".repeat(22000) };
|
||||||
|
values.push({ first: shared, second: shared });
|
||||||
for (const value of values) assert.equal(await contentDigestAsync(value), contentDigest(value));
|
for (const value of values) assert.equal(await contentDigestAsync(value), contentDigest(value));
|
||||||
for (const value of [undefined, NaN, Infinity, new Date(), [undefined]])
|
for (const value of [undefined, NaN, Infinity, new Date(), [undefined]])
|
||||||
await assert.rejects(contentDigestAsync(value));
|
await assert.rejects(contentDigestAsync(value));
|
||||||
|
const cyclic: { next?: unknown } = {};
|
||||||
|
cyclic.next = cyclic;
|
||||||
|
await assert.rejects(contentDigestAsync(cyclic), /cyclic JSON/);
|
||||||
const large = Array.from({ length: 50000 }, (_, index) => ({ z: "definition".repeat(8), a: index }));
|
const large = Array.from({ length: 50000 }, (_, index) => ({ z: "definition".repeat(8), a: index }));
|
||||||
const expected = contentDigest(large);
|
const expected = contentDigest(large);
|
||||||
let serviced = false;
|
let serviced = false;
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ import {
|
|||||||
implementationDigest,
|
implementationDigest,
|
||||||
clientContractCatalog,
|
clientContractCatalog,
|
||||||
hexDigest,
|
hexDigest,
|
||||||
|
executionPlanDigest,
|
||||||
|
executionPlanDigestAsync,
|
||||||
|
sealExecutionPlan,
|
||||||
parsePackageArtifacts,
|
parsePackageArtifacts,
|
||||||
artifactPath,
|
artifactPath,
|
||||||
type PackageArtifacts,
|
type PackageArtifacts,
|
||||||
@@ -21,6 +24,54 @@ import { compileKernelProgram } from "../src/execution-world/kernel.js";
|
|||||||
import { loadCompositeWorkspace } from "./composite-fixture.js";
|
import { loadCompositeWorkspace } from "./composite-fixture.js";
|
||||||
import { standardNativeBackends } from "../src/execution-world/native-backends.js";
|
import { standardNativeBackends } from "../src/execution-world/native-backends.js";
|
||||||
|
|
||||||
|
test("sealed runtime plans reuse verified integrity while mutable copies are rechecked", async () => {
|
||||||
|
const workspace = await loadReaderWorkspace();
|
||||||
|
const compiled = compileExecutionPlan(workspace);
|
||||||
|
const plan: ExecutionPlan = JSON.parse(JSON.stringify(compiled));
|
||||||
|
assert.equal(await sealExecutionPlan(plan), true);
|
||||||
|
assert.equal(executionPlanDigest(plan), compiled.digest);
|
||||||
|
assert.equal(await executionPlanDigestAsync(plan), compiled.digest);
|
||||||
|
assert.throws(() => {
|
||||||
|
plan.definition.workspace.sourceRootCommit = "f".repeat(40);
|
||||||
|
}, TypeError);
|
||||||
|
assert.throws(() => {
|
||||||
|
plan.digest = "0".repeat(64);
|
||||||
|
}, TypeError);
|
||||||
|
|
||||||
|
const stringify = JSON.stringify;
|
||||||
|
JSON.stringify = () => {
|
||||||
|
throw Error("Repeated serialization of sealed metadata");
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
assert.equal(executionPlanDigest(plan), compiled.digest);
|
||||||
|
assert.equal(await executionPlanDigestAsync(plan), compiled.digest);
|
||||||
|
} finally {
|
||||||
|
JSON.stringify = stringify;
|
||||||
|
}
|
||||||
|
|
||||||
|
const changed: ExecutionPlan = structuredClone(plan);
|
||||||
|
changed.definition.workspace.sourceRootCommit = "f".repeat(40);
|
||||||
|
Object.freeze(changed);
|
||||||
|
assert.notEqual(executionPlanDigest(changed), plan.digest);
|
||||||
|
assert.notEqual(await executionPlanDigestAsync(changed), plan.digest);
|
||||||
|
assert.equal(await sealExecutionPlan(changed), false, "shallow freezing is not an integrity proof");
|
||||||
|
|
||||||
|
const resigned: ExecutionPlan = structuredClone(plan);
|
||||||
|
resigned.definition.workspace.sourceRootCommit = "e".repeat(40);
|
||||||
|
resigned.digest = executionPlanDigest(resigned);
|
||||||
|
assert.equal(await sealExecutionPlan(resigned), true);
|
||||||
|
assert.notEqual(executionPlanDigest(resigned), plan.digest);
|
||||||
|
workspace.sourceRootCommit = "d".repeat(40);
|
||||||
|
assert.equal(workspace.sourceRootCommit, "d".repeat(40), "sealing owned JSON leaves compiler inputs editable");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("runtime plan sealing rejects accessor-backed metadata", async () => {
|
||||||
|
const plan: ExecutionPlan = compileExecutionPlan(await loadReaderWorkspace());
|
||||||
|
assert.equal(await sealExecutionPlan(Object.create(plan)), false, "inherited integrity fields cannot be sealed");
|
||||||
|
Object.defineProperty(plan.definition, "codec", { get: () => "camino-execution-plan-v1", enumerable: true });
|
||||||
|
await assert.rejects(sealExecutionPlan(plan), /INVALID_PLAN_JSON/);
|
||||||
|
});
|
||||||
|
|
||||||
test("optional targets close over calls into another package", async () => {
|
test("optional targets close over calls into another package", async () => {
|
||||||
const workspace = await loadCompositeWorkspace();
|
const workspace = await loadCompositeWorkspace();
|
||||||
const before = compileExecutionPlan(workspace).definition.operations.find(
|
const before = compileExecutionPlan(workspace).definition.operations.find(
|
||||||
|
|||||||
Reference in New Issue
Block a user