From 18aa3cb1e53b0bbb85587e4c280c000762224209 Mon Sep 17 00:00:00 2001 From: David Chen Date: Wed, 29 Jul 2026 01:36:25 -0700 Subject: [PATCH 1/5] feat(ts): add local untrusted js Podman runtime --- .changeset/bright-local-code-runtime.md | 21 + packages/ts/package.json | 30 + ...cal-untrusted-js-compute-node.live.test.ts | 98 + .../local-untrusted-js-compute-node.test.ts | 435 ++++ .../local-untrusted-js-compute.test.ts | 808 ++++++++ packages/ts/src/__tests__/subpaths.test.ts | 26 + .../executors/local-untrusted-js-compute.ts | 1788 +++++++++++++++++ .../local-untrusted-js-compute/node.ts | 945 +++++++++ packages/ts/tsup.config.ts | 2 + scripts/check-no-raw-async.ts | 4 + .../src/content/docs/integrations/matrix.md | 2 + 11 files changed, 4159 insertions(+) create mode 100644 .changeset/bright-local-code-runtime.md create mode 100644 packages/ts/src/__tests__/local-untrusted-js-compute-node.live.test.ts create mode 100644 packages/ts/src/__tests__/local-untrusted-js-compute-node.test.ts create mode 100644 packages/ts/src/__tests__/local-untrusted-js-compute.test.ts create mode 100644 packages/ts/src/executors/local-untrusted-js-compute.ts create mode 100644 packages/ts/src/executors/local-untrusted-js-compute/node.ts diff --git a/.changeset/bright-local-code-runtime.md b/.changeset/bright-local-code-runtime.md new file mode 100644 index 00000000..a013689f --- /dev/null +++ b/.changeset/bright-local-code-runtime.md @@ -0,0 +1,21 @@ +--- +"@graphrefly/ts": minor +--- + +Add the focused D667 local untrusted-JavaScript compute contract and Node +rootless-Podman Libpod API v0 broker subpaths. The contract binds immutable +bundle, runner image, compiler, fixed GraphReFly API, admitted input, sandbox, +network, filesystem, resource and output coordinates; accepts only current +independently certified readiness; and returns bounded answer, actual runtime +topology/describe/provenance and verified cleanup. Its Graph-visible runtime +projects admitted runs, lifecycle, status, outcome, usage, issues, audit and +exact-attempt cancellation while keeping bundle/input bytes and Podman handles +host-private. + +The Node broker uses one fixed runner entrypoint in a fresh digest-pinned +non-root container with a read-only root filesystem, all capabilities dropped, +no-new-privileges, no host bind or engine-socket mount, one bounded noexec +tmpfs, no network, bounded resources, exact stop/kill/remove and stable absence +verification. It is distinct +from the hosted E2B managed-compute backend and the fixed PostgreSQL Podman +recipe. The package root and aggregate executor surfaces remain unchanged. diff --git a/packages/ts/package.json b/packages/ts/package.json index 3782e1db..7d9bd95c 100644 --- a/packages/ts/package.json +++ b/packages/ts/package.json @@ -226,6 +226,36 @@ "default": "./dist/executors/local-container-postgresql-podman-libpod-api-v0-rootless/node.cjs" } }, + "./executors/local-untrusted-js-compute": { + "import": { + "types": "./dist/executors/local-untrusted-js-compute.d.ts", + "default": "./dist/executors/local-untrusted-js-compute.js" + }, + "require": { + "types": "./dist/executors/local-untrusted-js-compute.d.cts", + "default": "./dist/executors/local-untrusted-js-compute.cjs" + } + }, + "./executors/local-untrusted-js-compute/node": { + "node": { + "import": { + "types": "./dist/executors/local-untrusted-js-compute/node.d.ts", + "default": "./dist/executors/local-untrusted-js-compute/node.js" + }, + "require": { + "types": "./dist/executors/local-untrusted-js-compute/node.d.cts", + "default": "./dist/executors/local-untrusted-js-compute/node.cjs" + } + }, + "import": { + "types": "./dist/executors/local-untrusted-js-compute/node.d.ts", + "default": "./dist/executors/local-untrusted-js-compute/node.js" + }, + "require": { + "types": "./dist/executors/local-untrusted-js-compute/node.d.cts", + "default": "./dist/executors/local-untrusted-js-compute/node.cjs" + } + }, "./executors/managed-cloud-postgresql": { "import": { "types": "./dist/executors/managed-cloud-postgresql.d.ts", diff --git a/packages/ts/src/__tests__/local-untrusted-js-compute-node.live.test.ts b/packages/ts/src/__tests__/local-untrusted-js-compute-node.live.test.ts new file mode 100644 index 00000000..e79f7bd7 --- /dev/null +++ b/packages/ts/src/__tests__/local-untrusted-js-compute-node.live.test.ts @@ -0,0 +1,98 @@ +import { createHash } from "node:crypto"; +import { describe, expect, it } from "vitest"; +import { + nodeLocalUntrustedJsComputeDriver, + nodeLocalUntrustedJsComputeHostBindingAttestation, +} from "../executors/local-untrusted-js-compute/node.js"; +import { + LOCAL_UNTRUSTED_JS_COMPUTE_BACKEND, + LOCAL_UNTRUSTED_JS_COMPUTE_COMPATIBILITY, + LOCAL_UNTRUSTED_JS_COMPUTE_RUNNER_API, + type LocalUntrustedJsComputeArguments, + type LocalUntrustedJsComputeManifest, +} from "../executors/local-untrusted-js-compute.js"; +import { strictCanonicalJsonBytes } from "../json/codec.js"; + +const live = process.env.GRAPHREFLY_D667_LIVE_NEGATIVE === "1"; +const imageDigest = "sha256:d13105efe29040feb046f1c5fc9f0a98e58d8980c85300306a325c80df9a45c4"; +const imageRef = `docker.io/library/postgres@${imageDigest}`; +const bundle = new TextEncoder().encode("export default ({ input }) => input;"); +const bundleDigest = `sha256:${createHash("sha256").update(bundle).digest("hex")}`; +const input = { rows: [] }; +const inputDigest = `sha256:${createHash("sha256") + .update(strictCanonicalJsonBytes(input)) + .digest("hex")}`; + +describe.runIf(live)("D667 Node rootless-Podman broker live negative", () => { + it("creates the exact contained attempt, fails on a non-runner image, and still removes it", async () => { + const host = await nodeLocalUntrustedJsComputeHostBindingAttestation(); + const manifest: LocalUntrustedJsComputeManifest = { + kind: "local-untrusted-js-compute-manifest", + manifestId: "manifest:d667:live-negative", + revision: "manifest-revision:1", + fingerprint: "manifest-fingerprint:d667-live-negative", + compatibilityRevision: LOCAL_UNTRUSTED_JS_COMPUTE_COMPATIBILITY, + backend: LOCAL_UNTRUSTED_JS_COMPUTE_BACKEND, + runnerApiRevision: LOCAL_UNTRUSTED_JS_COMPUTE_RUNNER_API, + runnerImageDigest: imageDigest, + runnerRevision: "runner:missing-negative-control", + compilerRevision: "compiler:negative-control", + allowedApiRevision: "api:negative-control", + graphreflyPackageRevision: "graphrefly-ts:0.7.0", + sandboxPolicyRevision: "sandbox:d667-v0", + networkPolicyRevision: "deny-all-v1", + filesystemPolicyRevision: "read-only-input-bounded-tmp-v1", + resourcePolicyRevision: "resource:d667-v0", + outputPolicyRevision: "output:d667-v0", + executionTimeoutMs: 5_000, + killGraceMs: 1_000, + cleanupTimeoutMs: 5_000, + maxBundleBytes: 8_192, + maxInputBytes: 1_024, + maxOutputBytes: 16_384, + maxTopologyNodes: 8, + maxTopologyEdges: 8, + }; + const args: LocalUntrustedJsComputeArguments = { + contractVersion: "1", + runId: "run:d667-live-negative", + attempt: 1, + epoch: "epoch:1", + sourceRevision: "source:1", + sourceDigest: `sha256:${"2".repeat(64)}`, + bundleRevision: "bundle:1", + bundleDigest, + compilerRevision: manifest.compilerRevision, + allowedApiRevision: manifest.allowedApiRevision, + graphreflyPackageRevision: manifest.graphreflyPackageRevision, + runnerRevision: manifest.runnerRevision, + runnerImageDigest: manifest.runnerImageDigest, + sandboxPolicyRevision: manifest.sandboxPolicyRevision, + networkPolicyRevision: manifest.networkPolicyRevision, + filesystemPolicyRevision: manifest.filesystemPolicyRevision, + resourcePolicyRevision: manifest.resourcePolicyRevision, + outputPolicyRevision: manifest.outputPolicyRevision, + admittedInputRefs: ["input:negative-control"], + inputDigest, + }; + const outcome = await nodeLocalUntrustedJsComputeDriver({ imageRef }).execute( + { + runId: args.runId, + attempt: args.attempt, + epoch: args.epoch, + manifestFingerprint: manifest.fingerprint, + hostBindingDigest: host.hostBindingDigest, + runAdmissionId: "run-admission:d667-live-negative", + signal: new AbortController().signal, + }, + args, + { bundle, input }, + manifest, + ); + expect(outcome).toMatchObject({ + outcome: "failed", + code: "local-untrusted-js-compute-container-start-failed", + cleanup: "succeeded", + }); + }); +}); diff --git a/packages/ts/src/__tests__/local-untrusted-js-compute-node.test.ts b/packages/ts/src/__tests__/local-untrusted-js-compute-node.test.ts new file mode 100644 index 00000000..3c3c8b7b --- /dev/null +++ b/packages/ts/src/__tests__/local-untrusted-js-compute-node.test.ts @@ -0,0 +1,435 @@ +import { createHash } from "node:crypto"; +import { once } from "node:events"; +import { mkdir, mkdtemp, rm } from "node:fs/promises"; +import { createServer, type ServerResponse } from "node:http"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + nodeLocalUntrustedJsComputeDriver, + nodeLocalUntrustedJsComputeHostBindingAttestation, +} from "../executors/local-untrusted-js-compute/node.js"; +import { + LOCAL_UNTRUSTED_JS_COMPUTE_BACKEND, + LOCAL_UNTRUSTED_JS_COMPUTE_COMPATIBILITY, + LOCAL_UNTRUSTED_JS_COMPUTE_RUNNER_API, + type LocalUntrustedJsComputeArguments, + type LocalUntrustedJsComputeManifest, +} from "../executors/local-untrusted-js-compute.js"; +import { strictCanonicalJsonBytes } from "../json/codec.js"; + +const imageDigest = `sha256:${"1".repeat(64)}`; +const imageRef = `registry.example.test/graphrefly/local-js@${imageDigest}`; +const containerId = "a".repeat(64); +const bundle = new TextEncoder().encode("export default ({ input }) => input;"); +const input = { rows: [] }; +const digest = (bytes: Uint8Array): string => + `sha256:${createHash("sha256").update(bytes).digest("hex")}`; +const capabilities = [ + "CAP_CHOWN", + "CAP_DAC_OVERRIDE", + "CAP_FOWNER", + "CAP_FSETID", + "CAP_KILL", + "CAP_NET_BIND_SERVICE", + "CAP_SETFCAP", + "CAP_SETGID", + "CAP_SETPCAP", + "CAP_SETUID", + "CAP_SYS_CHROOT", +]; + +interface HarnessState { + mode: + | "ambiguous-create" + | "containment-mismatch" + | "timeout" + | "output-overflow" + | "residue" + | "strict-exit" + | "delayed-create" + | "unrelated-list"; + created: boolean; + deleted: boolean; + hangInfo: boolean; + containerName?: string; + labels?: Record; + readonly createdRunLabels: string[]; + readonly deletePaths: string[]; +} + +const cleanupTasks: Array<() => Promise> = []; + +afterEach(async () => { + for (const cleanup of cleanupTasks.splice(0).reverse()) await cleanup(); + vi.unstubAllEnvs(); +}); + +const manifest = (overrides: Partial = {}) => + ({ + kind: "local-untrusted-js-compute-manifest", + manifestId: "manifest:d667:node-harness", + revision: "manifest-revision:1", + fingerprint: "manifest-fingerprint:d667-node-harness", + compatibilityRevision: LOCAL_UNTRUSTED_JS_COMPUTE_COMPATIBILITY, + backend: LOCAL_UNTRUSTED_JS_COMPUTE_BACKEND, + runnerApiRevision: LOCAL_UNTRUSTED_JS_COMPUTE_RUNNER_API, + runnerImageDigest: imageDigest, + runnerRevision: "runner:harness", + compilerRevision: "compiler:harness", + allowedApiRevision: "api:harness", + graphreflyPackageRevision: "graphrefly-ts:0.7.0", + sandboxPolicyRevision: "sandbox:d667-v0", + networkPolicyRevision: "deny-all-v1", + filesystemPolicyRevision: "read-only-input-bounded-tmp-v1", + resourcePolicyRevision: "resource:d667-v0", + outputPolicyRevision: "output:d667-v0", + executionTimeoutMs: 1_000, + killGraceMs: 1_000, + cleanupTimeoutMs: 1_000, + maxBundleBytes: 8_192, + maxInputBytes: 1_024, + maxOutputBytes: 1_024, + maxTopologyNodes: 8, + maxTopologyEdges: 8, + ...overrides, + }) satisfies LocalUntrustedJsComputeManifest; + +const args = (): LocalUntrustedJsComputeArguments => ({ + contractVersion: "1", + runId: "run:node-harness", + attempt: 1, + epoch: "epoch:1", + sourceRevision: "source:1", + sourceDigest: `sha256:${"2".repeat(64)}`, + bundleRevision: "bundle:1", + bundleDigest: digest(bundle), + compilerRevision: "compiler:harness", + allowedApiRevision: "api:harness", + graphreflyPackageRevision: "graphrefly-ts:0.7.0", + runnerRevision: "runner:harness", + runnerImageDigest: imageDigest, + sandboxPolicyRevision: "sandbox:d667-v0", + networkPolicyRevision: "deny-all-v1", + filesystemPolicyRevision: "read-only-input-bounded-tmp-v1", + resourcePolicyRevision: "resource:d667-v0", + outputPolicyRevision: "output:d667-v0", + admittedInputRefs: ["input:harness"], + inputDigest: digest(strictCanonicalJsonBytes(input)), +}); + +function sendJson(response: ServerResponse, status: number, value: unknown): void { + response.writeHead(status, { "content-type": "application/json" }); + response.end(JSON.stringify(value)); +} + +async function harness(mode: HarnessState["mode"]): Promise<{ + state: HarnessState; + hostBindingDigest: string; +}> { + const directory = await mkdtemp(join(tmpdir(), "graphrefly-d667-node-test-")); + const podmanDirectory = join(directory, "podman"); + await mkdir(podmanDirectory); + const socketPath = join(podmanDirectory, "podman.sock"); + const state: HarnessState = { + mode, + created: false, + deleted: false, + hangInfo: false, + createdRunLabels: [], + deletePaths: [], + }; + const previousRuntimeDirectory = process.env.XDG_RUNTIME_DIR; + vi.stubEnv("XDG_RUNTIME_DIR", directory); + const server = createServer((request, response) => { + const path = request.url ?? ""; + if (path.endsWith("/libpod/info")) { + if (state.hangInfo) return; + sendJson(response, 200, { + host: { + arch: "arm64", + os: "linux", + security: { rootless: true, capabilities: capabilities.join(",") }, + }, + version: { APIVersion: "5.0.3", Version: "5.0.3", OsArch: "linux/arm64" }, + }); + return; + } + if (path.includes("/libpod/images/")) { + sendJson(response, 200, { Digest: imageDigest, RepoDigests: [imageRef] }); + return; + } + if (path.endsWith("/libpod/containers/create")) { + const chunks: Buffer[] = []; + request.on("data", (chunk: Buffer) => chunks.push(chunk)); + request.on("end", () => { + const body = JSON.parse(Buffer.concat(chunks).toString("utf8")) as { + name?: string; + labels?: Record; + }; + const finishCreate = () => { + state.containerName = body.name; + state.labels = body.labels; + state.createdRunLabels.push(body.labels?.["dev.graphrefly.run"] ?? "missing"); + state.created = true; + state.deleted = false; + if (mode === "ambiguous-create") sendJson(response, 500, { message: "response lost" }); + else sendJson(response, 201, { Id: containerId, Warnings: [] }); + }; + if (mode === "delayed-create") setTimeout(finishCreate, 60); + else finishCreate(); + }); + return; + } + if (path.includes("/libpod/containers/json?")) { + if (mode === "unrelated-list") { + sendJson(response, 200, [ + { + Id: "b".repeat(64), + Labels: state.labels, + Names: [state.containerName], + }, + ]); + return; + } + sendJson( + response, + 200, + state.created && !state.deleted + ? [ + { + Id: containerId, + Labels: state.labels, + Names: [state.containerName], + }, + ] + : [], + ); + return; + } + if (path.endsWith("/json")) { + if (state.deleted || !state.created) { + sendJson(response, 404, { message: "absent" }); + return; + } + sendJson( + response, + 200, + inspectBody(state.containerName ?? "", mode === "containment-mismatch", state.labels ?? {}), + ); + return; + } + if (path.includes("/archive?")) { + response.writeHead(200).end(); + return; + } + if (path.endsWith("/start")) { + if (mode === "residue") response.writeHead(500).end(); + else response.writeHead(204).end(); + return; + } + if (path.includes("/wait?")) { + if (mode === "timeout") return; + response.writeHead(200).end(mode === "strict-exit" ? "0junk" : "0"); + return; + } + if (path.includes("/logs?")) { + response.writeHead(200); + if (mode === "output-overflow") response.end(Buffer.alloc(20_000, 120)); + else response.end("{}"); + return; + } + if (path.includes("/stop?") || path.includes("/kill?")) { + response.writeHead(204).end(); + return; + } + if (request.method === "DELETE") { + state.deletePaths.push(path); + if (mode === "residue") response.writeHead(500).end(); + else { + if (!path.includes("b".repeat(64))) state.deleted = true; + response.writeHead(204).end(); + } + return; + } + sendJson(response, 404, { message: "unexpected harness path", path }); + }); + server.listen(socketPath); + await once(server, "listening"); + cleanupTasks.push(async () => { + server.closeAllConnections(); + await new Promise((resolve) => server.close(() => resolve())); + if (previousRuntimeDirectory === undefined) delete process.env.XDG_RUNTIME_DIR; + else process.env.XDG_RUNTIME_DIR = previousRuntimeDirectory; + await rm(directory, { recursive: true, force: true }); + }); + const attestation = await nodeLocalUntrustedJsComputeHostBindingAttestation(); + return { state, hostBindingDigest: attestation.hostBindingDigest }; +} + +function inspectBody( + name: string, + mismatch: boolean, + labels: Record, +): Record { + return { + Id: containerId, + Name: name, + Config: { + Image: imageRef, + User: "65532:65532", + Entrypoint: ["/usr/local/bin/node", "/opt/graphrefly/local-untrusted-js-runner.mjs"], + Cmd: ["/input/bundle.mjs", "/input/input.json", "/input/control.json"], + Env: mismatch ? ["SECRET=ambient"] : [], + Labels: labels, + }, + HostConfig: { + ReadonlyRootfs: true, + Privileged: false, + SecurityOpt: ["no-new-privileges"], + CapDrop: capabilities, + CapAdd: [], + Memory: 256 * 1024 * 1024, + CpuPeriod: 100_000, + CpuQuota: 100_000, + PidsLimit: 64, + NetworkMode: "none", + PublishAllPorts: false, + PortBindings: {}, + Tmpfs: { + "/tmp": "rw,nosuid,nodev,noexec,size=16777216,mode=0700,rprivate,tmpcopyup", + }, + Ulimits: [ + { Name: "RLIMIT_RLIMIT_FSIZE", Soft: 16 * 1024 * 1024, Hard: 16 * 1024 * 1024 }, + { Name: "RLIMIT_RLIMIT_NOFILE", Soft: 128, Hard: 128 }, + ], + }, + Mounts: [], + }; +} + +async function execute( + hostBindingDigest: string, + manifestValue: LocalUntrustedJsComputeManifest, + signal = new AbortController().signal, + attempt = 1, +) { + return nodeLocalUntrustedJsComputeDriver({ imageRef }).execute( + { + runId: "run:node-harness", + attempt, + epoch: "epoch:1", + manifestFingerprint: manifestValue.fingerprint, + hostBindingDigest, + runAdmissionId: "run-admission:node-harness", + signal, + }, + { ...args(), attempt }, + { bundle, input }, + manifestValue, + ); +} + +describe("D667 Node rootless-Podman broker lifecycle", () => { + it("reconciles an ambiguous create response by exact name and labels", async () => { + const { state, hostBindingDigest } = await harness("ambiguous-create"); + const result = await execute(hostBindingDigest, manifest()); + expect(result).toMatchObject({ + outcome: "failed", + code: "local-untrusted-js-compute-container-create-failed", + cleanup: "succeeded", + }); + expect(state).toMatchObject({ created: true, deleted: true }); + }); + + it("keeps an aborted create unverifiable while reconciling a delayed allocation", async () => { + const { state, hostBindingDigest } = await harness("delayed-create"); + await expect( + execute(hostBindingDigest, manifest({ executionTimeoutMs: 25, cleanupTimeoutMs: 300 })), + ).resolves.toMatchObject({ + outcome: "timeout", + code: "local-untrusted-js-compute-timeout", + cleanup: "unverifiable", + }); + expect(state).toMatchObject({ created: true, deleted: true }); + }); + + it("refuses an unrelated ID even when an ignored filter returns matching labels and name", async () => { + const { state, hostBindingDigest } = await harness("unrelated-list"); + await expect(execute(hostBindingDigest, manifest())).resolves.toMatchObject({ + outcome: "succeeded", + cleanup: "failed", + }); + expect(state.deletePaths.some((path) => path.includes("b".repeat(64)))).toBe(false); + }); + + it("isolates the ownership label across attempts that share one run id", async () => { + const { state, hostBindingDigest } = await harness("strict-exit"); + await execute(hostBindingDigest, manifest(), new AbortController().signal, 1); + await execute(hostBindingDigest, manifest(), new AbortController().signal, 2); + expect(state.createdRunLabels).toHaveLength(2); + expect(state.createdRunLabels[0]).not.toBe(state.createdRunLabels[1]); + }); + + it("fails closed and cleans up on containment mismatch", async () => { + const { state, hostBindingDigest } = await harness("containment-mismatch"); + await expect(execute(hostBindingDigest, manifest())).resolves.toMatchObject({ + outcome: "failed", + code: "local-untrusted-js-compute-containment-mismatch", + cleanup: "succeeded", + }); + expect(state.deleted).toBe(true); + }); + + it("classifies one attempt-wide deadline as timeout and still cleans up", async () => { + const { state, hostBindingDigest } = await harness("timeout"); + await expect( + execute(hostBindingDigest, manifest({ executionTimeoutMs: 40 })), + ).resolves.toMatchObject({ + outcome: "timeout", + code: "local-untrusted-js-compute-timeout", + cleanup: "succeeded", + }); + expect(state.deleted).toBe(true); + }); + + it("fails closed on bounded log overflow and strict exit-code parsing", async () => { + for (const mode of ["output-overflow", "strict-exit"] as const) { + const { state, hostBindingDigest } = await harness(mode); + await expect(execute(hostBindingDigest, manifest())).resolves.toMatchObject({ + outcome: "failed", + code: + mode === "output-overflow" + ? "local-untrusted-js-compute-output-overflow" + : "local-untrusted-js-compute-runner-failed", + cleanup: "succeeded", + }); + expect(state.deleted).toBe(true); + await cleanupTasks.pop()?.(); + } + }); + + it("reports residue separately when label-scoped cleanup cannot remove the attempt", async () => { + const { hostBindingDigest } = await harness("residue"); + await expect( + execute(hostBindingDigest, manifest({ cleanupTimeoutMs: 100 })), + ).resolves.toMatchObject({ + outcome: "failed", + code: "local-untrusted-js-compute-container-start-failed", + cleanup: "failed", + }); + }); + + it("preserves cancellation during actual-socket discovery", async () => { + const { state, hostBindingDigest } = await harness("timeout"); + state.hangInfo = true; + const controller = new AbortController(); + const running = execute(hostBindingDigest, manifest(), controller.signal); + setTimeout(() => controller.abort(), 10); + await expect(running).resolves.toMatchObject({ + outcome: "canceled", + code: "local-untrusted-js-compute-canceled", + cleanup: "unverifiable", + }); + }); +}); diff --git a/packages/ts/src/__tests__/local-untrusted-js-compute.test.ts b/packages/ts/src/__tests__/local-untrusted-js-compute.test.ts new file mode 100644 index 00000000..d0ab3472 --- /dev/null +++ b/packages/ts/src/__tests__/local-untrusted-js-compute.test.ts @@ -0,0 +1,808 @@ +import { createHash } from "node:crypto"; +import { describe, expect, it, vi } from "vitest"; +import { nodeLocalUntrustedJsComputeDriver } from "../executors/local-untrusted-js-compute/node.js"; +import { + LOCAL_UNTRUSTED_JS_COMPUTE_BACKEND, + LOCAL_UNTRUSTED_JS_COMPUTE_COMPATIBILITY, + LOCAL_UNTRUSTED_JS_COMPUTE_RUNNER_API, + type LocalUntrustedJsComputeArguments, + type LocalUntrustedJsComputeCancellationAdmission, + type LocalUntrustedJsComputeDriver, + type LocalUntrustedJsComputeManifest, + type LocalUntrustedJsComputeReadiness, + localUntrustedJsComputeRuntime, + runLocalUntrustedJsComputeAttempt, +} from "../executors/local-untrusted-js-compute.js"; +import { Graph } from "../graph/graph.js"; +import { strictCanonicalJsonBytes } from "../json/codec.js"; +import type { + AgentRuntimeAuditRecord, + ExecutorOutcome, + ToolProviderAdapterInput, + ToolProviderAdapterRunRequested, + ToolProviderAdapterRunStatus, + ToolProviderRunAdmission, +} from "../orchestration/index.js"; + +const imageDigest = `sha256:${"1".repeat(64)}`; +const sourceDigest = `sha256:${"2".repeat(64)}`; +const bundle = new Uint8Array([1, 2, 3]); +const input = { question: "count rows", rows: [{ id: 1 }, { id: 2 }] }; +const sha256 = (bytes: Uint8Array): string => + `sha256:${createHash("sha256").update(bytes).digest("hex")}`; +const bundleDigest = sha256(bundle); +const inputDigest = sha256(strictCanonicalJsonBytes(input)); + +const manifest = (): LocalUntrustedJsComputeManifest => ({ + kind: "local-untrusted-js-compute-manifest", + manifestId: "manifest:local-js:1", + revision: "manifest-revision:1", + fingerprint: "manifest-fingerprint:1", + compatibilityRevision: LOCAL_UNTRUSTED_JS_COMPUTE_COMPATIBILITY, + backend: LOCAL_UNTRUSTED_JS_COMPUTE_BACKEND, + runnerApiRevision: LOCAL_UNTRUSTED_JS_COMPUTE_RUNNER_API, + runnerImageDigest: imageDigest, + runnerRevision: "runner:1", + compilerRevision: "compiler:1", + allowedApiRevision: "api:1", + graphreflyPackageRevision: "graphrefly-ts:0.7.0", + sandboxPolicyRevision: "sandbox:1", + networkPolicyRevision: "deny-all-v1", + filesystemPolicyRevision: "read-only-input-bounded-tmp-v1", + resourcePolicyRevision: "resource:1", + outputPolicyRevision: "output:1", + executionTimeoutMs: 1_000, + killGraceMs: 1_000, + cleanupTimeoutMs: 1_000, + maxBundleBytes: 8_192, + maxInputBytes: 1_024, + maxOutputBytes: 16_384, + maxTopologyNodes: 8, + maxTopologyEdges: 8, +}); + +const readiness = (): LocalUntrustedJsComputeReadiness => ({ + kind: "local-untrusted-js-compute-readiness", + manifestFingerprint: "manifest-fingerprint:1", + state: "ready", + observedAtMs: 100, + expiresAtMs: 1_000, + rootlessVerified: true, + imageDigestVerified: true, + runnerVerified: true, + nonRootVerified: true, + readOnlyRootFilesystemVerified: true, + noNewPrivilegesVerified: true, + capabilitiesDroppedVerified: true, + noEngineSocketMountVerified: true, + noHostBindMountVerified: true, + denyNetworkVerified: true, + resourceBoundsVerified: true, + cancellationVerified: true, + cleanupVerified: true, + hostBindingDigest: `sha256:${"4".repeat(64)}`, + attestationRefs: ["attestation:d667:1"], +}); + +const args = (): LocalUntrustedJsComputeArguments => ({ + contractVersion: "1", + runId: "run:1", + attempt: 1, + epoch: "epoch:1", + sourceRevision: "source:1", + sourceDigest, + bundleRevision: "bundle:1", + bundleDigest, + compilerRevision: "compiler:1", + allowedApiRevision: "api:1", + graphreflyPackageRevision: "graphrefly-ts:0.7.0", + runnerRevision: "runner:1", + runnerImageDigest: imageDigest, + sandboxPolicyRevision: "sandbox:1", + networkPolicyRevision: "deny-all-v1", + filesystemPolicyRevision: "read-only-input-bounded-tmp-v1", + resourcePolicyRevision: "resource:1", + outputPolicyRevision: "output:1", + admittedInputRefs: ["input:postgresql-result:1"], + inputDigest, +}); + +const adapterInput = (): ToolProviderAdapterInput => ({ + kind: "tool-provider-adapter-input", + adapterInputId: "adapter-input:1", + status: "ready", + requestId: "request:1", + operationId: "operation:1", + routeId: "route:1", + providerId: "provider:local-untrusted-js", + executorId: "executor:d667", + profileId: "profile:local-v0", + toolName: "graphrefly.local-untrusted-js-compute", + operation: "run", + toolCall: { + kind: "tool-call", + toolName: "graphrefly.local-untrusted-js-compute", + operation: "run", + arguments: args(), + }, +}); + +const admittedRunRequest = (): ToolProviderAdapterRunRequested => ({ + kind: "tool-provider-adapter-run-requested", + runId: "run:1", + adapterInputId: "adapter-input:1", + requestId: "request:1", + operationId: "operation:1", + routeId: "route:1", + providerId: "provider:local-untrusted-js", + executorId: "executor:d667", + profileId: "profile:local-v0", + attempt: 1, + reason: "initial", + sourceRefs: [ + { kind: "tool-provider-adapter-run", id: "run:candidate:1" }, + { kind: "tool-provider-run-admission-proposal", id: "proposal:1" }, + { kind: "tool-provider-run-admission", id: "run-admission:1" }, + ], + metadata: { + approval: "granted", + approvalGranted: true, + admissionId: "run-admission:1", + proposalId: "proposal:1", + approvedFromRunId: "run:candidate:1", + }, +}); + +const runAdmission = (): ToolProviderRunAdmission => ({ + kind: "tool-provider-run-admission", + admissionId: "run-admission:1", + proposalId: "proposal:1", + runId: "run:candidate:1", + adapterInputId: "adapter-input:1", + requestId: "request:1", + operationId: "operation:1", + state: "admitted", + approvedRunId: "run:1", +}); + +const successfulDriver = (): LocalUntrustedJsComputeDriver => ({ + compatibility: LOCAL_UNTRUSTED_JS_COMPUTE_COMPATIBILITY, + execute: vi.fn((_context, request) => ({ + outcome: "succeeded", + cleanup: "succeeded", + result: { + contractVersion: "1", + answer: { rows: 2 }, + topology: { + name: "local-code-workgraph", + nodes: [ + { id: "question", name: "question", factory: "source", deps: [] }, + { id: "answer", name: "answer", factory: "map", deps: ["question"] }, + ], + edges: [{ from: "question", to: "answer" }], + }, + describe: { + name: "local-code-workgraph", + nodes: [ + { + id: "question", + name: "question", + factory: "source", + deps: [], + status: "resolved", + value: { text: "count rows" }, + }, + { + id: "answer", + name: "answer", + factory: "map", + deps: ["question"], + status: "resolved", + value: { rows: 2 }, + }, + ], + edges: [{ from: "question", to: "answer" }], + }, + provenance: { + sourceRevision: request.sourceRevision, + sourceDigest: request.sourceDigest, + bundleRevision: request.bundleRevision, + bundleDigest: request.bundleDigest, + compilerRevision: request.compilerRevision, + allowedApiRevision: request.allowedApiRevision, + graphreflyPackageRevision: request.graphreflyPackageRevision, + runnerRevision: request.runnerRevision, + runnerImageDigest: request.runnerImageDigest, + manifestFingerprint: "manifest-fingerprint:1", + runId: request.runId, + attempt: request.attempt, + graphName: "local-code-workgraph", + admittedInputRefs: request.admittedInputRefs, + inputDigest: request.inputDigest, + runAdmissionId: "run-admission:1", + }, + cleanup: { graphNodesAfterDispose: 0, graphEdgesAfterDispose: 0 }, + }, + })), +}); + +const attemptOptions = (driver: LocalUntrustedJsComputeDriver) => ({ + manifest: manifest(), + readiness: readiness(), + args: args(), + material: { bundle, input }, + adapterInput: adapterInput(), + admittedRunRequest: admittedRunRequest(), + runAdmission: runAdmission(), + driver, + signal: new AbortController().signal, + now: () => 200, +}); + +const eventually = async (predicate: () => boolean): Promise => { + for (let attempt = 0; attempt < 100; attempt += 1) { + if (predicate()) return; + await new Promise((resolve) => setTimeout(resolve, 0)); + } + throw new Error("Timed out waiting for local untrusted JS compute evidence."); +}; + +describe("D667 local untrusted JS compute contract", () => { + it("runs one exact admitted attempt and returns bounded actual topology/provenance only after cleanup", async () => { + const driver = successfulDriver(); + const outcome = await runLocalUntrustedJsComputeAttempt({ + manifest: manifest(), + readiness: readiness(), + args: args(), + material: { + bundle, + input, + }, + adapterInput: adapterInput(), + admittedRunRequest: admittedRunRequest(), + runAdmission: runAdmission(), + driver, + signal: new AbortController().signal, + now: () => 200, + }); + expect(outcome).toMatchObject({ + outcome: "succeeded", + cleanup: "succeeded", + result: { + answer: { rows: 2 }, + topology: { name: "local-code-workgraph" }, + provenance: { + runId: "run:1", + bundleDigest, + admittedInputRefs: ["input:postgresql-result:1"], + }, + }, + }); + expect(driver.execute).toHaveBeenCalledTimes(1); + }); + + it("fails closed before the driver on stale readiness or coordinate drift", async () => { + for (const changed of [ + { readiness: { ...readiness(), expiresAtMs: 150 } }, + { args: { ...args(), runnerRevision: "runner:stale" } }, + { args: { ...args(), admittedInputRefs: [] } }, + ]) { + const driver = successfulDriver(); + await expect( + runLocalUntrustedJsComputeAttempt({ + manifest: manifest(), + readiness: changed.readiness ?? readiness(), + args: changed.args ?? args(), + material: { bundle, input }, + adapterInput: adapterInput(), + admittedRunRequest: admittedRunRequest(), + runAdmission: runAdmission(), + driver, + signal: new AbortController().signal, + now: () => 200, + }), + ).rejects.toThrow(TypeError); + expect(driver.execute).not.toHaveBeenCalled(); + } + }); + + it("downgrades success when cleanup is not verified and rejects forged runtime provenance", async () => { + const cleanupDriver = successfulDriver(); + vi.mocked(cleanupDriver.execute).mockImplementationOnce(async (...call) => { + const good = await successfulDriver().execute(...call); + return { ...good, cleanup: "failed" }; + }); + await expect( + runLocalUntrustedJsComputeAttempt({ + manifest: manifest(), + readiness: readiness(), + args: args(), + material: { bundle, input }, + adapterInput: adapterInput(), + admittedRunRequest: admittedRunRequest(), + runAdmission: runAdmission(), + driver: cleanupDriver, + signal: new AbortController().signal, + now: () => 200, + }), + ).resolves.toMatchObject({ + outcome: "failed", + code: "local-untrusted-js-compute-cleanup-not-verified", + cleanup: "failed", + }); + + const forgedDriver = successfulDriver(); + vi.mocked(forgedDriver.execute).mockImplementationOnce(async (...call) => { + const good = await successfulDriver().execute(...call); + if (good.outcome !== "succeeded") return good; + return { + ...good, + result: { + ...good.result, + provenance: { ...good.result.provenance, bundleDigest: `sha256:${"f".repeat(64)}` }, + }, + }; + }); + await expect( + runLocalUntrustedJsComputeAttempt({ + manifest: manifest(), + readiness: readiness(), + args: args(), + material: { bundle, input }, + adapterInput: adapterInput(), + admittedRunRequest: admittedRunRequest(), + runAdmission: runAdmission(), + driver: forgedDriver, + signal: new AbortController().signal, + now: () => 200, + }), + ).rejects.toThrow("mismatched provenance"); + }); + + it("does not accept a mutable image tag for the Node broker", () => { + expect(() => + nodeLocalUntrustedJsComputeDriver({ imageRef: "docker.io/library/node:24" }), + ).toThrow("digest pinned"); + expect( + nodeLocalUntrustedJsComputeDriver({ + imageRef: `docker.io/graphrefly/local-untrusted-js@${imageDigest}`, + }).compatibility, + ).toBe(LOCAL_UNTRUSTED_JS_COMPUTE_COMPATIBILITY); + }); + + it("binds immutable bundle/input bytes and exact D419 admission before invoking a driver", async () => { + for (const changed of [ + { material: { bundle: new Uint8Array([9]), input } }, + { material: { bundle, input: { ...input, question: "different admitted bytes" } } }, + { + admittedRunRequest: { ...admittedRunRequest(), routeId: "route:different" }, + }, + { + runAdmission: { ...runAdmission(), state: "blocked" as const }, + }, + { + runAdmission: { ...runAdmission(), approvedRunId: "run:different" }, + }, + { + admittedRunRequest: { + ...admittedRunRequest(), + sourceRefs: [ + ...(admittedRunRequest().sourceRefs ?? []), + { kind: "tool-provider-run-admission", id: "run-admission:other" }, + ], + }, + }, + { + adapterInput: { + ...adapterInput(), + toolCall: { ...adapterInput().toolCall, arguments: { ...args(), epoch: "epoch:2" } }, + }, + }, + ]) { + const driver = successfulDriver(); + await expect( + runLocalUntrustedJsComputeAttempt({ ...attemptOptions(driver), ...changed }), + ).rejects.toThrow(TypeError); + expect(driver.execute).not.toHaveBeenCalled(); + } + }); + + it("rejects malformed, inconsistent, nested-overflow and private-field Graph evidence", async () => { + const invalidResults = [ + ( + good: Extract< + Awaited>, + { outcome: "succeeded" } + >, + ) => ({ + ...good.result, + topology: { nodes: [{ totally: "invalid" }], edges: [] }, + }), + ( + good: Extract< + Awaited>, + { outcome: "succeeded" } + >, + ) => ({ + ...good.result, + describe: { ...good.result.describe, name: "different-graph" }, + }), + ( + good: Extract< + Awaited>, + { outcome: "succeeded" } + >, + ) => ({ + ...good.result, + topology: { + ...good.result.topology, + subgraphs: Array.from({ length: 8 }, (_, index) => ({ + name: `nested-${index}`, + nodes: [{ id: `nested-${index}`, factory: "source", deps: [] }], + edges: [], + })), + }, + }), + ( + good: Extract< + Awaited>, + { outcome: "succeeded" } + >, + ) => ({ + ...good.result, + provenance: { ...good.result.provenance, secret: "must-not-cross" }, + }), + ( + good: Extract< + Awaited>, + { outcome: "succeeded" } + >, + ) => ({ + ...good.result, + topology: { + ...good.result.topology, + subgraphs: [ + { + mountId: "child:1", + name: "child-1", + nodes: [{ id: "question", factory: "source", deps: [] }], + edges: [], + }, + ], + }, + }), + ( + good: Extract< + Awaited>, + { outcome: "succeeded" } + >, + ) => ({ + ...good.result, + topology: { + ...good.result.topology, + subgraphs: [ + { + mountId: "child:same", + name: "child-1", + nodes: [{ id: "child-1", factory: "source", deps: [] }], + edges: [], + }, + { + mountId: "child:same", + name: "child-2", + nodes: [{ id: "child-2", factory: "source", deps: [] }], + edges: [], + }, + ], + }, + }), + ]; + for (const invalidResult of invalidResults) { + const driver = successfulDriver(); + vi.mocked(driver.execute).mockImplementationOnce(async (...call) => { + const good = await successfulDriver().execute(...call); + if (good.outcome !== "succeeded") return good; + return { ...good, result: invalidResult(good) }; + }); + await expect(runLocalUntrustedJsComputeAttempt(attemptOptions(driver))).rejects.toThrow( + TypeError, + ); + } + }); + + it("fails closed on output overflow and preserves exact canceled-before-allocation posture", async () => { + const overflowDriver = successfulDriver(); + vi.mocked(overflowDriver.execute).mockImplementationOnce(async (...call) => { + const good = await successfulDriver().execute(...call); + if (good.outcome !== "succeeded") return good; + return { ...good, result: { ...good.result, answer: "x".repeat(20_000) } }; + }); + await expect(runLocalUntrustedJsComputeAttempt(attemptOptions(overflowDriver))).rejects.toThrow( + "output byte bound", + ); + + const canceledDriver = successfulDriver(); + const controller = new AbortController(); + controller.abort(); + await expect( + runLocalUntrustedJsComputeAttempt({ + ...attemptOptions(canceledDriver), + signal: controller.signal, + }), + ).resolves.toEqual({ + outcome: "canceled", + code: "local-untrusted-js-compute-canceled-before-allocation", + cleanup: "succeeded", + }); + expect(canceledDriver.execute).not.toHaveBeenCalled(); + }); +}); + +describe("D667 local untrusted JS compute Graph-visible runtime", () => { + const setup = (driver: LocalUntrustedJsComputeDriver) => { + const graph = new Graph(); + const sources = graph.topologyGroup({ name: "local-untrusted-js-compute-fixtures" }); + const inputNode = sources.node>(); + const requestNode = sources.node(); + const admissionNode = sources.node(); + const manifestNode = sources.node(); + const readinessNode = sources.node(); + const cancellationNode = sources.node(); + const runtime = localUntrustedJsComputeRuntime(graph, { + name: "testLocalUntrustedJsCompute", + inputs: inputNode, + admittedRunRequests: [requestNode], + runAdmissions: [admissionNode], + manifests: [manifestNode], + readiness: [readinessNode], + cancellationAdmissions: [cancellationNode], + driver, + loadMaterial: vi.fn(async () => ({ bundle, input })), + now: () => 200, + }); + const request = { + ...admittedRunRequest(), + metadata: { + ...admittedRunRequest().metadata, + manifestFingerprint: manifest().fingerprint, + }, + }; + const publishAuthority = () => { + inputNode.down([["DATA", adapterInput()]]); + admissionNode.down([["DATA", runAdmission()]]); + manifestNode.down([["DATA", manifest()]]); + readinessNode.down([["DATA", readiness()]]); + }; + return { + graph, + sources, + runtime, + requestNode, + cancellationNode, + request, + publishAuthority, + }; + }; + + it("projects an exact admitted run through status, lifecycle, outcome, usage, audit and cleanup", async () => { + const driver = successfulDriver(); + const fixture = setup(driver); + const statuses: ToolProviderAdapterRunStatus[] = []; + const outcomesSeen: ExecutorOutcome[] = []; + const lifecycleStates: string[] = []; + const usageSeen: unknown[] = []; + const cleanupSeen: unknown[] = []; + const audits: AgentRuntimeAuditRecord[] = []; + const unsubscribes = [ + fixture.runtime.runStatus.subscribe((message) => { + if (message[0] === "DATA") statuses.push(message[1]); + }), + fixture.runtime.outcomes.subscribe((message) => { + if (message[0] === "DATA") outcomesSeen.push(message[1]); + }), + fixture.runtime.lifecycle.subscribe((message) => { + if (message[0] === "DATA") lifecycleStates.push(message[1].state); + }), + fixture.runtime.usage.subscribe((message) => { + if (message[0] === "DATA") usageSeen.push(message[1]); + }), + fixture.runtime.cleanup.subscribe((message) => { + if (message[0] === "DATA") cleanupSeen.push(message[1]); + }), + fixture.runtime.audit.subscribe((message) => { + if (message[0] === "DATA") audits.push(message[1]); + }), + ]; + fixture.publishAuthority(); + fixture.requestNode.down([["DATA", fixture.request]]); + await eventually(() => outcomesSeen.length === 1); + expect(statuses.map((entry) => entry.status)).toEqual(["requested", "started", "result"]); + expect(lifecycleStates).toEqual([ + "admitted", + "loading-material", + "running", + "cleaning", + "settled", + ]); + expect(outcomesSeen[0]).toMatchObject({ + kind: "result", + requestId: "request:1", + usage: { latencyMs: 0 }, + result: { + kind: "local-untrusted-js-compute-result", + value: { + answer: { rows: 2 }, + topology: { name: "local-code-workgraph" }, + provenance: { runAdmissionId: "run-admission:1" }, + }, + }, + }); + expect(usageSeen).toEqual([ + { + kind: "local-untrusted-js-compute-usage", + runId: "run:1", + attempt: 1, + latencyMs: 0, + }, + ]); + expect(cleanupSeen).toEqual([ + { + kind: "local-untrusted-js-compute-cleanup-status", + runId: "run:1", + attempt: 1, + epoch: "epoch:1", + state: "succeeded", + }, + ]); + expect(audits.map((entry) => entry.kind)).toContain("local-untrusted-js-compute-settled"); + + fixture.requestNode.down([["DATA", fixture.request]]); + await eventually(() => statuses.some((entry) => entry.status === "stale-request")); + expect(driver.execute).toHaveBeenCalledTimes(1); + + for (const unsubscribe of unsubscribes) unsubscribe(); + await fixture.runtime.dispose(); + expect(fixture.graph.topology().nodes).toHaveLength(6); + fixture.sources.release({ reason: "test-complete" }); + expect(fixture.graph.topology()).toEqual({ nodes: [], edges: [] }); + }); + + it("rejects coordinate drift and delivers only an exact admitted cancellation", async () => { + const driver: LocalUntrustedJsComputeDriver = { + compatibility: LOCAL_UNTRUSTED_JS_COMPUTE_COMPATIBILITY, + execute: vi.fn( + (context) => + new Promise((resolve) => { + context.signal.addEventListener( + "abort", + () => + resolve({ + outcome: "canceled", + code: "local-untrusted-js-compute-canceled", + cleanup: "succeeded", + }), + { once: true }, + ); + }), + ), + }; + const fixture = setup(driver); + const statuses: ToolProviderAdapterRunStatus[] = []; + const outcomesSeen: ExecutorOutcome[] = []; + const cancellationsSeen: unknown[] = []; + const issuesSeen: string[] = []; + const unsubscribes = [ + fixture.runtime.runStatus.subscribe((message) => { + if (message[0] === "DATA") statuses.push(message[1]); + }), + fixture.runtime.outcomes.subscribe((message) => { + if (message[0] === "DATA") outcomesSeen.push(message[1]); + }), + fixture.runtime.cancellations.subscribe((message) => { + if (message[0] === "DATA") cancellationsSeen.push(message[1]); + }), + fixture.runtime.issues.subscribe((message) => { + if (message[0] === "DATA") issuesSeen.push(message[1].code); + }), + ]; + fixture.publishAuthority(); + fixture.requestNode.down([["DATA", fixture.request]]); + await eventually(() => statuses.some((entry) => entry.status === "started")); + const exactCancellation: LocalUntrustedJsComputeCancellationAdmission = { + kind: "local-untrusted-js-compute-cancellation-admission", + admissionId: "cancel-admission:1", + cancellationId: "cancellation:1", + state: "admitted", + runId: "run:1", + adapterInputId: "adapter-input:1", + requestId: "request:1", + operationId: "operation:1", + routeId: "route:1", + providerId: "provider:local-untrusted-js", + executorId: "executor:d667", + profileId: "profile:local-v0", + runAdmissionId: "run-admission:1", + attempt: 1, + epoch: "epoch:1", + manifestFingerprint: "manifest-fingerprint:1", + hostBindingDigest: readiness().hostBindingDigest, + sourceRefs: [{ kind: "local-cancellation-decision", id: "cancel-decision:1" }], + }; + fixture.cancellationNode.down([["DATA", { ...exactCancellation, epoch: "epoch:stale" }]]); + await eventually(() => cancellationsSeen.length === 1); + expect(cancellationsSeen[0]).toMatchObject({ state: "rejected" }); + expect(outcomesSeen).toHaveLength(0); + + fixture.cancellationNode.down([["DATA", exactCancellation]]); + await eventually(() => outcomesSeen.length === 1); + expect(cancellationsSeen[1]).toMatchObject({ + state: "accepted", + admissionId: "cancel-admission:1", + }); + expect(outcomesSeen[0]).toMatchObject({ + kind: "canceled", + metadata: { cleanup: "succeeded" }, + }); + expect(issuesSeen).toContain("local-untrusted-js-compute-cancellation-coordinate-mismatch"); + + for (const unsubscribe of unsubscribes) unsubscribe(); + await fixture.runtime.dispose(); + fixture.sources.release({ reason: "test-complete" }); + expect(fixture.graph.topology()).toEqual({ nodes: [], edges: [] }); + }); + + it("does not resolve disposal or release topology before active private cleanup settles", async () => { + let privateCleanupSettled = false; + let markDriverEntered = () => {}; + const driverEntered = new Promise((resolve) => { + markDriverEntered = resolve; + }); + const driver: LocalUntrustedJsComputeDriver = { + compatibility: LOCAL_UNTRUSTED_JS_COMPUTE_COMPATIBILITY, + execute: vi.fn((context) => { + markDriverEntered(); + return new Promise((resolve) => { + context.signal.addEventListener( + "abort", + () => { + setTimeout(() => { + privateCleanupSettled = true; + resolve({ + outcome: "canceled", + code: "local-untrusted-js-compute-canceled", + cleanup: "succeeded", + }); + }, 40); + }, + { once: true }, + ); + }); + }), + }; + const fixture = setup(driver); + const statuses: ToolProviderAdapterRunStatus[] = []; + const unsubscribe = fixture.runtime.runStatus.subscribe((message) => { + if (message[0] === "DATA") statuses.push(message[1]); + }); + fixture.publishAuthority(); + fixture.requestNode.down([["DATA", fixture.request]]); + await eventually(() => statuses.some((entry) => entry.status === "started")); + await driverEntered; + unsubscribe(); + + let disposeResolved = false; + const disposing = fixture.runtime.dispose().then(() => { + disposeResolved = true; + }); + await new Promise((resolve) => setTimeout(resolve, 10)); + expect(privateCleanupSettled).toBe(false); + expect(disposeResolved).toBe(false); + expect(fixture.graph.topology().nodes).toHaveLength(15); + + await disposing; + expect(privateCleanupSettled).toBe(true); + expect(fixture.graph.topology().nodes).toHaveLength(6); + fixture.sources.release({ reason: "test-complete" }); + expect(fixture.graph.topology()).toEqual({ nodes: [], edges: [] }); + }); +}); diff --git a/packages/ts/src/__tests__/subpaths.test.ts b/packages/ts/src/__tests__/subpaths.test.ts index be68b799..d03eb478 100644 --- a/packages/ts/src/__tests__/subpaths.test.ts +++ b/packages/ts/src/__tests__/subpaths.test.ts @@ -33,6 +33,8 @@ import * as executorLocalContainerPostgresqlDockerEngineApiV0Node from "../execu import * as executorLocalContainerPostgresqlDockerEngineApiV0 from "../executors/local-container-postgresql-docker-engine-api-v0.js"; import * as executorLocalContainerPostgresqlPodmanLibpodApiV0RootlessNode from "../executors/local-container-postgresql-podman-libpod-api-v0-rootless/node.js"; import * as executorLocalContainerPostgresqlPodmanLibpodApiV0Rootless from "../executors/local-container-postgresql-podman-libpod-api-v0-rootless.js"; +import * as executorLocalUntrustedJsComputeNode from "../executors/local-untrusted-js-compute/node.js"; +import * as executorLocalUntrustedJsCompute from "../executors/local-untrusted-js-compute.js"; import * as executorManagedCloudPostgresql from "../executors/managed-cloud-postgresql.js"; import * as executorManagedUntrustedJsCompute from "../executors/managed-untrusted-js-compute.js"; import * as executorPostgresqlRunOperations from "../executors/postgresql-run-operations.js"; @@ -258,6 +260,8 @@ describe("package subpath barrels (D40/D41 intent parity)", () => { "./executors/local-container-postgresql-docker-engine-api-v0/node", "./executors/local-container-postgresql-podman-libpod-api-v0-rootless", "./executors/local-container-postgresql-podman-libpod-api-v0-rootless/node", + "./executors/local-untrusted-js-compute", + "./executors/local-untrusted-js-compute/node", "./executors/managed-cloud-postgresql", "./executors/managed-untrusted-js-compute", "./executors/postgresql-run-operations", @@ -888,6 +892,21 @@ describe("package subpath barrels (D40/D41 intent parity)", () => { expect( typeof executorLocalContainerPostgresqlPodmanLibpodApiV0RootlessNode.certifyPodmanLibpodApiV0RootlessLocalContainerPostgresqlWithNode, ).toBe("function"); + expect(typeof executorLocalUntrustedJsCompute.localUntrustedJsComputeManifest).toBe("function"); + expect(typeof executorLocalUntrustedJsCompute.localUntrustedJsComputeReadiness).toBe( + "function", + ); + expect(typeof executorLocalUntrustedJsCompute.localUntrustedJsComputeArguments).toBe( + "function", + ); + expect(typeof executorLocalUntrustedJsCompute.localUntrustedJsComputeRuntime).toBe("function"); + expect(typeof executorLocalUntrustedJsComputeNode.nodeLocalUntrustedJsComputeDriver).toBe( + "function", + ); + expect( + typeof executorLocalUntrustedJsComputeNode.nodeLocalUntrustedJsComputeHostBindingAttestation, + ).toBe("function"); + expect(Object.hasOwn(rootPackage, "localUntrustedJsComputeRuntime")).toBe(false); expect(Object.hasOwn(rootPackage, "certifyDockerEngineApiV0LocalContainerPostgresql")).toBe( false, ); @@ -910,6 +929,13 @@ describe("package subpath barrels (D40/D41 intent parity)", () => { "certifyPodmanLibpodApiV0RootlessLocalContainerPostgresqlWithNode", ), ).toBe(false); + expect(Object.hasOwn(rootPackage, "localUntrustedJsComputeManifest")).toBe(false); + expect(Object.hasOwn(rootPackage, "localUntrustedJsComputeReadiness")).toBe(false); + expect(Object.hasOwn(rootPackage, "localUntrustedJsComputeArguments")).toBe(false); + expect(Object.hasOwn(rootPackage, "nodeLocalUntrustedJsComputeDriver")).toBe(false); + expect(Object.hasOwn(rootPackage, "nodeLocalUntrustedJsComputeHostBindingAttestation")).toBe( + false, + ); expect(Object.hasOwn(rootPackage, "LocalSandboxDriver")).toBe(false); expect(typeof executorManagedCloudPostgresql.managedCloudPostgresqlRuntime).toBe("function"); expect(typeof executorManagedUntrustedJsCompute.managedUntrustedJsComputeRuntime).toBe( diff --git a/packages/ts/src/executors/local-untrusted-js-compute.ts b/packages/ts/src/executors/local-untrusted-js-compute.ts new file mode 100644 index 00000000..c334ac49 --- /dev/null +++ b/packages/ts/src/executors/local-untrusted-js-compute.ts @@ -0,0 +1,1788 @@ +/** D667 focused local untrusted-JavaScript compute contract. */ +import type { DataIssue } from "../data/index.js"; +import { + type DescribeSnapshot, + type GraphTopologySnapshot, + topologyFromDescribe, +} from "../graph/describe.js"; +import type { Graph } from "../graph/graph.js"; +import { compoundTupleKey } from "../identity.js"; +import { strictCanonicalJsonBytes } from "../json/codec.js"; +import type { Node } from "../node/node.js"; +import type { + AgentRuntimeAuditRecord, + ExecutorOutcome, + ExecutorUsage, + SourceRef, + ToolProviderAdapterInput, + ToolProviderAdapterRunRequested, + ToolProviderAdapterRunStatus, + ToolProviderRunAdmission, +} from "../orchestration/index.js"; +import { buildToolProviderExecutorOutcome } from "../orchestration/index.js"; + +export const LOCAL_UNTRUSTED_JS_COMPUTE_COMPATIBILITY = + "graphrefly-local-untrusted-js-compute-v1" as const; +export const LOCAL_UNTRUSTED_JS_COMPUTE_BACKEND = + "local-untrusted-js-compute-podman-libpod-api-v0-rootless" as const; +export const LOCAL_UNTRUSTED_JS_COMPUTE_RUNNER_API = "graphrefly-runner-api-v1" as const; +const MAX_EXECUTION_TIMEOUT_MS = 5 * 60_000; +const MAX_KILL_GRACE_MS = 60_000; +const MAX_CLEANUP_TIMEOUT_MS = 5 * 60_000; +const MAX_MATERIAL_BYTES = 16 * 1024 * 1024; +const MAX_TOPOLOGY_NODES = 100_000; +const MAX_TOPOLOGY_EDGES = 200_000; + +export type LocalUntrustedJsJson = + | null + | boolean + | number + | string + | readonly LocalUntrustedJsJson[] + | { readonly [key: string]: LocalUntrustedJsJson }; + +export interface LocalUntrustedJsComputeManifest { + readonly kind: "local-untrusted-js-compute-manifest"; + readonly manifestId: string; + readonly revision: string; + readonly fingerprint: string; + readonly compatibilityRevision: typeof LOCAL_UNTRUSTED_JS_COMPUTE_COMPATIBILITY; + readonly backend: typeof LOCAL_UNTRUSTED_JS_COMPUTE_BACKEND; + readonly runnerApiRevision: typeof LOCAL_UNTRUSTED_JS_COMPUTE_RUNNER_API; + readonly runnerImageDigest: string; + readonly runnerRevision: string; + readonly compilerRevision: string; + readonly allowedApiRevision: string; + readonly graphreflyPackageRevision: string; + readonly sandboxPolicyRevision: string; + readonly networkPolicyRevision: "deny-all-v1"; + readonly filesystemPolicyRevision: "read-only-input-bounded-tmp-v1"; + readonly resourcePolicyRevision: string; + readonly outputPolicyRevision: string; + readonly executionTimeoutMs: number; + readonly killGraceMs: number; + readonly cleanupTimeoutMs: number; + readonly maxBundleBytes: number; + readonly maxInputBytes: number; + readonly maxOutputBytes: number; + readonly maxTopologyNodes: number; + readonly maxTopologyEdges: number; +} + +export interface LocalUntrustedJsComputeReadiness { + readonly kind: "local-untrusted-js-compute-readiness"; + readonly manifestFingerprint: string; + readonly state: "ready" | "stale" | "unavailable"; + readonly observedAtMs: number; + readonly expiresAtMs: number; + readonly rootlessVerified: boolean; + readonly imageDigestVerified: boolean; + readonly runnerVerified: boolean; + readonly nonRootVerified: boolean; + readonly readOnlyRootFilesystemVerified: boolean; + readonly noNewPrivilegesVerified: boolean; + readonly capabilitiesDroppedVerified: boolean; + readonly noEngineSocketMountVerified: boolean; + readonly noHostBindMountVerified: boolean; + readonly denyNetworkVerified: boolean; + readonly resourceBoundsVerified: boolean; + readonly cancellationVerified: boolean; + readonly cleanupVerified: boolean; + readonly hostBindingDigest: string; + readonly attestationRefs: readonly string[]; +} + +export interface LocalUntrustedJsComputeArguments { + readonly contractVersion: "1"; + readonly runId: string; + readonly attempt: number; + readonly epoch: string; + readonly sourceRevision: string; + readonly sourceDigest: string; + readonly bundleRevision: string; + readonly bundleDigest: string; + readonly compilerRevision: string; + readonly allowedApiRevision: string; + readonly graphreflyPackageRevision: string; + readonly runnerRevision: string; + readonly runnerImageDigest: string; + readonly sandboxPolicyRevision: string; + readonly networkPolicyRevision: "deny-all-v1"; + readonly filesystemPolicyRevision: "read-only-input-bounded-tmp-v1"; + readonly resourcePolicyRevision: string; + readonly outputPolicyRevision: string; + readonly admittedInputRefs: readonly string[]; + readonly inputDigest: string; +} + +export interface LocalUntrustedJsComputeMaterial { + readonly bundle: Uint8Array; + readonly input: LocalUntrustedJsJson; +} + +export interface LocalUntrustedJsComputeProvenance { + readonly sourceRevision: string; + readonly sourceDigest: string; + readonly bundleRevision: string; + readonly bundleDigest: string; + readonly compilerRevision: string; + readonly allowedApiRevision: string; + readonly graphreflyPackageRevision: string; + readonly runnerRevision: string; + readonly runnerImageDigest: string; + readonly manifestFingerprint: string; + readonly runId: string; + readonly attempt: number; + readonly graphName: string; + readonly admittedInputRefs: readonly string[]; + readonly inputDigest: string; + readonly runAdmissionId: string; +} + +export interface LocalUntrustedJsComputeRunnerResult { + readonly contractVersion: "1"; + readonly answer: LocalUntrustedJsJson; + readonly topology: GraphTopologySnapshot; + readonly describe: DescribeSnapshot; + readonly provenance: LocalUntrustedJsComputeProvenance; + readonly cleanup: { + readonly graphNodesAfterDispose: 0; + readonly graphEdgesAfterDispose: 0; + }; +} + +export interface LocalUntrustedJsComputeRunnerControl { + readonly contractVersion: "1"; + readonly compatibilityRevision: typeof LOCAL_UNTRUSTED_JS_COMPUTE_COMPATIBILITY; + readonly runnerApiRevision: typeof LOCAL_UNTRUSTED_JS_COMPUTE_RUNNER_API; + readonly manifestFingerprint: string; + readonly args: LocalUntrustedJsComputeArguments; + readonly runAdmissionId: string; +} + +export type LocalUntrustedJsComputeCleanupState = "succeeded" | "failed" | "unverifiable"; + +export type LocalUntrustedJsComputeDriverOutcome = + | { + readonly outcome: "succeeded"; + readonly result: LocalUntrustedJsComputeRunnerResult; + readonly cleanup: LocalUntrustedJsComputeCleanupState; + } + | { + readonly outcome: "failed" | "timeout" | "canceled"; + readonly code: string; + readonly cleanup: LocalUntrustedJsComputeCleanupState; + }; + +export interface LocalUntrustedJsComputeDriverContext { + readonly runId: string; + readonly attempt: number; + readonly epoch: string; + readonly manifestFingerprint: string; + readonly hostBindingDigest: string; + readonly runAdmissionId: string; + readonly signal: AbortSignal; +} + +/** + * Runtime sockets, endpoints, container ids, paths and private handles stay behind this driver. + * The driver owns fresh allocation, bounded transfer, execution, cancellation and exact cleanup. + */ +export interface LocalUntrustedJsComputeDriver { + readonly compatibility: typeof LOCAL_UNTRUSTED_JS_COMPUTE_COMPATIBILITY; + execute( + context: LocalUntrustedJsComputeDriverContext, + args: LocalUntrustedJsComputeArguments, + material: LocalUntrustedJsComputeMaterial, + manifest: LocalUntrustedJsComputeManifest, + ): LocalUntrustedJsComputeDriverOutcome | PromiseLike; +} + +export interface RunLocalUntrustedJsComputeAttemptOptions { + readonly manifest: LocalUntrustedJsComputeManifest; + readonly readiness: LocalUntrustedJsComputeReadiness; + readonly args: LocalUntrustedJsComputeArguments; + readonly material: LocalUntrustedJsComputeMaterial; + readonly adapterInput: ToolProviderAdapterInput; + readonly admittedRunRequest: ToolProviderAdapterRunRequested; + readonly runAdmission: ToolProviderRunAdmission; + readonly driver: LocalUntrustedJsComputeDriver; + readonly signal: AbortSignal; + readonly now?: () => number; +} + +export type LocalUntrustedJsComputeLifecycleState = + | "admitted" + | "loading-material" + | "running" + | "cleaning" + | "settled"; + +export interface LocalUntrustedJsComputeLifecycleFact { + readonly kind: "local-untrusted-js-compute-lifecycle-fact"; + readonly state: LocalUntrustedJsComputeLifecycleState; + readonly runId: string; + readonly attempt: number; + readonly epoch: string; + readonly manifestFingerprint: string; + readonly runAdmissionId: string; + readonly occurredAtMs: number; + readonly sourceRefs?: readonly SourceRef[]; +} + +export interface LocalUntrustedJsComputeCleanupStatus { + readonly kind: "local-untrusted-js-compute-cleanup-status"; + readonly runId: string; + readonly attempt: number; + readonly epoch: string; + readonly state: LocalUntrustedJsComputeCleanupState; + readonly issue?: DataIssue; +} + +export interface LocalUntrustedJsComputeUsageFact extends ExecutorUsage { + readonly kind: "local-untrusted-js-compute-usage"; + readonly runId: string; + readonly attempt: number; +} + +/** + * Graph-visible cancellation authority for one exact D667 attempt. Producing a request alone + * never aborts compute; only an independently admitted fact reaches the host-private driver. + */ +export interface LocalUntrustedJsComputeCancellationAdmission { + readonly kind: "local-untrusted-js-compute-cancellation-admission"; + readonly admissionId: string; + readonly cancellationId: string; + readonly state: "admitted" | "blocked"; + readonly runId: string; + readonly adapterInputId: string; + readonly requestId: string; + readonly operationId: string; + readonly routeId: string; + readonly providerId: string; + readonly executorId: string; + readonly profileId: string; + readonly runAdmissionId: string; + readonly attempt: number; + readonly epoch: string; + readonly manifestFingerprint: string; + readonly hostBindingDigest: string; + readonly sourceRefs: readonly SourceRef[]; +} + +export interface LocalUntrustedJsComputeCancellationAcknowledgement { + readonly kind: "local-untrusted-js-compute-cancellation-acknowledgement"; + readonly admissionId: string; + readonly cancellationId: string; + readonly runId: string; + readonly attempt: number; + readonly state: "accepted" | "rejected"; + readonly code?: string; +} + +export interface LocalUntrustedJsComputeMaterialLoadContext { + readonly input: ToolProviderAdapterInput; + readonly request: ToolProviderAdapterRunRequested; + readonly admission: ToolProviderRunAdmission; + readonly manifest: LocalUntrustedJsComputeManifest; + readonly readiness: LocalUntrustedJsComputeReadiness; + readonly args: LocalUntrustedJsComputeArguments; + readonly signal: AbortSignal; +} + +export interface LocalUntrustedJsComputeRuntimeOptions { + readonly name?: string; + readonly inputs: Node>; + readonly admittedRunRequests: readonly Node[]; + readonly runAdmissions: readonly Node[]; + readonly manifests: readonly Node[]; + readonly readiness: readonly Node[]; + readonly cancellationAdmissions?: readonly Node[]; + readonly driver: LocalUntrustedJsComputeDriver; + /** + * Host-private material authority. It must honor `context.signal` and must not settle + * until any material acquisition it owns has been released. + */ + readonly loadMaterial: ( + context: LocalUntrustedJsComputeMaterialLoadContext, + ) => LocalUntrustedJsComputeMaterial | PromiseLike; + readonly now?: () => number; +} + +export interface LocalUntrustedJsComputeRuntimeBundle { + readonly admittedRunRequests: Node; + readonly runStatus: Node; + readonly lifecycle: Node; + readonly cleanup: Node; + readonly outcomes: Node; + readonly usage: Node; + readonly cancellations: Node; + readonly issues: Node; + readonly audit: Node; + dispose(): Promise; +} + +interface LocalUntrustedJsComputeActiveAttempt { + readonly input: ToolProviderAdapterInput; + readonly request: ToolProviderAdapterRunRequested; + readonly admission: ToolProviderRunAdmission; + readonly manifest: LocalUntrustedJsComputeManifest; + readonly readiness: LocalUntrustedJsComputeReadiness; + readonly args: LocalUntrustedJsComputeArguments; + readonly abortController: AbortController; + readonly startedAtMs: number; + cancelled: boolean; + settled: boolean; +} + +/** + * D667 Graph-visible runtime. Graph DATA carries immutable authority coordinates and bounded + * results only; bundle/input bytes and the Podman socket remain behind loadMaterial/driver. + */ +export function localUntrustedJsComputeRuntime( + graph: Graph, + opts: LocalUntrustedJsComputeRuntimeOptions, +): LocalUntrustedJsComputeRuntimeBundle { + if (opts.driver.compatibility !== LOCAL_UNTRUSTED_JS_COMPUTE_COMPATIBILITY) + throw new TypeError("Local untrusted JS compute driver compatibility mismatch."); + const name = opts.name ?? "localUntrustedJsCompute"; + const group = graph.topologyGroup({ name }); + const outputNode = (suffix: string, factory: string) => + group.node([], null, { + name: `${name}/${suffix}`, + factory, + completeWhenDepsComplete: false, + errorWhenDepsError: false, + }); + const admittedRunRequests = outputNode( + "admittedRunRequests", + "localUntrustedJsComputeAdmittedRunRequests", + ); + const runStatus = outputNode( + "runStatus", + "localUntrustedJsComputeRunStatus", + ); + const lifecycle = outputNode( + "lifecycle", + "localUntrustedJsComputeLifecycle", + ); + const cleanup = outputNode( + "cleanup", + "localUntrustedJsComputeCleanup", + ); + const outcomes = outputNode("outcomes", "localUntrustedJsComputeOutcomes"); + const usage = outputNode( + "usage", + "localUntrustedJsComputeUsage", + ); + const cancellations = outputNode( + "cancellations", + "localUntrustedJsComputeCancellations", + ); + const issues = outputNode("issues", "localUntrustedJsComputeIssues"); + const audit = outputNode("audit", "localUntrustedJsComputeAudit"); + const inputs = new Map>(); + const admissions = new Map(); + const manifests = new Map(); + const readiness = new Map(); + const active = new Map(); + const terminal = new Set(); + const consumedCancellationAdmissions = new Set(); + const pending = new Set>(); + const now = opts.now ?? Date.now; + let disposed = false; + let disposePromise: Promise | undefined; + const emit = (node: Node, value: T) => node.down([["DATA", value]]); + const emitIssue = ( + code: string, + message: string, + subjectId?: string, + sourceRefs: readonly SourceRef[] = [], + ) => { + const issue: DataIssue = Object.freeze({ + kind: "issue", + code, + message, + severity: "error", + ...(subjectId === undefined ? {} : { subjectId }), + ...(sourceRefs.length === 0 + ? {} + : { + refs: Object.freeze( + sourceRefs.map((ref) => compoundTupleKey("source-ref", [ref.kind, ref.id])), + ), + }), + }); + emit(issues, issue); + emit(audit, { + id: compoundTupleKey("local-untrusted-js-compute-audit", [code, subjectId ?? "runtime"]), + kind: code, + ...(subjectId === undefined ? {} : { subjectId }), + issueCode: code, + ...(sourceRefs.length === 0 ? {} : { sourceRefs }), + }); + return issue; + }; + const emitStatus = ( + request: ToolProviderAdapterRunRequested, + statusValue: ToolProviderAdapterRunStatus["status"], + issue?: DataIssue, + outcomeId?: string, + ) => + emit(runStatus, { + kind: "tool-provider-adapter-run-status", + runId: request.runId, + adapterInputId: request.adapterInputId, + requestId: request.requestId, + operationId: request.operationId, + attempt: request.attempt, + status: statusValue, + ...(outcomeId === undefined ? {} : { outcomeId }), + ...(issue === undefined ? {} : { issues: [issue] }), + sourceRefs: request.sourceRefs, + metadata: { + ...(request.routeId === undefined ? {} : { routeId: request.routeId }), + ...(request.profileId === undefined ? {} : { profileId: request.profileId }), + }, + }); + const emitLifecycle = ( + record: LocalUntrustedJsComputeActiveAttempt, + state: LocalUntrustedJsComputeLifecycleState, + sourceRefs: readonly SourceRef[] = record.request.sourceRefs ?? [], + ) => { + const occurredAtMs = now(); + const fact: LocalUntrustedJsComputeLifecycleFact = Object.freeze({ + kind: "local-untrusted-js-compute-lifecycle-fact", + state, + runId: record.request.runId, + attempt: record.request.attempt, + epoch: record.args.epoch, + manifestFingerprint: record.manifest.fingerprint, + runAdmissionId: record.admission.admissionId, + occurredAtMs, + ...(sourceRefs.length === 0 ? {} : { sourceRefs: Object.freeze([...sourceRefs]) }), + }); + emit(lifecycle, fact); + emit(audit, { + id: compoundTupleKey("local-untrusted-js-compute-lifecycle-audit", [ + fact.runId, + String(fact.attempt), + state, + String(occurredAtMs), + ]), + kind: `local-untrusted-js-compute-${state}`, + subjectId: fact.runId, + sourceRefs: fact.sourceRefs, + metadata: { + attempt: fact.attempt, + epoch: fact.epoch, + manifestFingerprint: fact.manifestFingerprint, + runAdmissionId: fact.runAdmissionId, + }, + }); + }; + const track = (work: Promise) => { + pending.add(work); + void work.finally(() => pending.delete(work)); + }; + const failRequest = ( + request: ToolProviderAdapterRunRequested, + code: string, + statusValue: ToolProviderAdapterRunStatus["status"], + ) => { + const issue = emitIssue( + code, + "Local untrusted JS compute admission failed closed.", + request.runId, + request.sourceRefs, + ); + emitStatus(request, statusValue, issue); + }; + const execute = async (record: LocalUntrustedJsComputeActiveAttempt) => { + let driverOutcome: LocalUntrustedJsComputeDriverOutcome; + try { + emitLifecycle(record, "loading-material"); + const material = await opts.loadMaterial( + Object.freeze({ + input: record.input, + request: record.request, + admission: record.admission, + manifest: record.manifest, + readiness: record.readiness, + args: record.args, + signal: record.abortController.signal, + }), + ); + if (disposed) return; + emitLifecycle(record, "running"); + emitStatus(record.request, "started"); + driverOutcome = await runLocalUntrustedJsComputeAttempt({ + manifest: record.manifest, + readiness: record.readiness, + args: record.args, + material, + adapterInput: record.input, + admittedRunRequest: record.request, + runAdmission: record.admission, + driver: opts.driver, + signal: record.abortController.signal, + now, + }); + } catch { + driverOutcome = Object.freeze({ + outcome: record.cancelled ? "canceled" : "failed", + code: record.cancelled + ? "local-untrusted-js-compute-canceled" + : "local-untrusted-js-compute-runtime-failed", + cleanup: record.cancelled ? "unverifiable" : "failed", + }); + } + if (disposed) return; + record.settled = true; + emitLifecycle(record, "cleaning"); + const latencyMs = Math.max(0, now() - record.startedAtMs); + const executorUsage: ExecutorUsage = Object.freeze({ latencyMs }); + const usageFact: LocalUntrustedJsComputeUsageFact = Object.freeze({ + kind: "local-untrusted-js-compute-usage", + runId: record.request.runId, + attempt: record.request.attempt, + latencyMs, + }); + emit(usage, usageFact); + const sourceRefs: readonly SourceRef[] = Object.freeze([ + { kind: "tool-provider-run-admission", id: record.admission.admissionId }, + { kind: "local-untrusted-js-compute-manifest", id: record.manifest.manifestId }, + ...(record.request.sourceRefs ?? []), + ]); + let outcome: ExecutorOutcome; + if (driverOutcome.outcome === "succeeded") { + outcome = buildToolProviderExecutorOutcome( + record.input, + { + kind: "result", + result: { + kind: "local-untrusted-js-compute-result", + value: driverOutcome.result, + refs: sourceRefs, + summary: + "Local GraphReFly JavaScript execution completed with topology and provenance.", + }, + evidenceRefs: sourceRefs, + usage: executorUsage, + occurredAtMs: now(), + metadata: { + manifestFingerprint: record.manifest.fingerprint, + epoch: record.args.epoch, + runAdmissionId: record.admission.admissionId, + cleanup: driverOutcome.cleanup, + }, + }, + { runId: record.request.runId, attempt: record.request.attempt }, + ); + } else { + const problem = emitIssue( + driverOutcome.code, + "Local untrusted JS compute attempt did not produce a successful result.", + record.request.runId, + sourceRefs, + ); + const common = { + evidenceRefs: sourceRefs, + issues: [problem], + usage: executorUsage, + occurredAtMs: now(), + metadata: { + manifestFingerprint: record.manifest.fingerprint, + epoch: record.args.epoch, + runAdmissionId: record.admission.admissionId, + cleanup: driverOutcome.cleanup, + }, + }; + outcome = + driverOutcome.outcome === "timeout" + ? buildToolProviderExecutorOutcome( + record.input, + { + kind: "timeout", + timeoutMs: record.manifest.executionTimeoutMs, + retryable: false, + ...common, + }, + { runId: record.request.runId, attempt: record.request.attempt }, + ) + : driverOutcome.outcome === "canceled" + ? buildToolProviderExecutorOutcome( + record.input, + { kind: "canceled", reason: driverOutcome.code, ...common }, + { runId: record.request.runId, attempt: record.request.attempt }, + ) + : buildToolProviderExecutorOutcome( + record.input, + { kind: "failure", error: problem, retryable: false, ...common }, + { runId: record.request.runId, attempt: record.request.attempt }, + ); + } + emit(outcomes, outcome); + emitStatus(record.request, outcome.kind, undefined, outcome.outcomeId); + const cleanupIssue = + driverOutcome.cleanup === "succeeded" + ? undefined + : emitIssue( + `local-untrusted-js-compute-cleanup-${driverOutcome.cleanup}`, + "Local untrusted JS compute cleanup was not proven.", + record.request.runId, + sourceRefs, + ); + emit(cleanup, { + kind: "local-untrusted-js-compute-cleanup-status", + runId: record.request.runId, + attempt: record.request.attempt, + epoch: record.args.epoch, + state: driverOutcome.cleanup, + ...(cleanupIssue === undefined ? {} : { issue: cleanupIssue }), + }); + emitLifecycle(record, "settled", sourceRefs); + active.delete(runtimeAttemptKey(record.request)); + terminal.add(runtimeAttemptKey(record.request)); + }; + const unsubscribes = [ + opts.inputs.subscribe((message) => { + if (message[0] !== "DATA" || disposed) return; + const value = message[1] as ToolProviderAdapterInput; + if (!plain(value) || value.kind !== "tool-provider-adapter-input") { + emitIssue( + "local-untrusted-js-compute-input-invalid", + "Local untrusted JS compute input is invalid.", + ); + return; + } + inputs.set(value.adapterInputId, value); + }), + ...opts.runAdmissions.map((node) => + node.subscribe((message) => { + if (message[0] !== "DATA" || disposed) return; + const value = message[1] as ToolProviderRunAdmission; + if (!plain(value) || value.kind !== "tool-provider-run-admission") { + emitIssue( + "local-untrusted-js-compute-run-admission-invalid", + "Local untrusted JS compute run admission is invalid.", + ); + return; + } + admissions.set(value.admissionId, value); + }), + ), + ...opts.manifests.map((node) => + node.subscribe((message) => { + if (message[0] !== "DATA" || disposed) return; + try { + const value = localUntrustedJsComputeManifest( + message[1] as LocalUntrustedJsComputeManifest, + ); + manifests.set(value.fingerprint, value); + } catch { + emitIssue( + "local-untrusted-js-compute-manifest-invalid", + "Local untrusted JS compute manifest is invalid.", + ); + } + }), + ), + ...opts.readiness.map((node) => + node.subscribe((message) => { + if (message[0] !== "DATA" || disposed) return; + const raw = message[1] as LocalUntrustedJsComputeReadiness; + if (!plain(raw) || typeof raw.manifestFingerprint !== "string") { + emitIssue( + "local-untrusted-js-compute-readiness-invalid", + "Local untrusted JS compute readiness is invalid.", + ); + return; + } + readiness.set(raw.manifestFingerprint, raw); + }), + ), + ...opts.admittedRunRequests.map((node) => + node.subscribe((message) => { + if (message[0] !== "DATA" || disposed) return; + const request = message[1] as ToolProviderAdapterRunRequested; + const work = Promise.resolve().then(async () => { + if (!plain(request) || request.kind !== "tool-provider-adapter-run-requested") { + emitIssue( + "local-untrusted-js-compute-run-request-invalid", + "Local untrusted JS compute run request is invalid.", + ); + return; + } + const key = runtimeAttemptKey(request); + if (active.has(key) || terminal.has(key)) { + failRequest(request, "local-untrusted-js-compute-duplicate-run", "stale-request"); + return; + } + const input = inputs.get(request.adapterInputId); + const admissionId = request.metadata?.admissionId; + const manifestFingerprint = request.metadata?.manifestFingerprint; + if ( + input === undefined || + typeof admissionId !== "string" || + typeof manifestFingerprint !== "string" + ) { + failRequest( + request, + "local-untrusted-js-compute-admission-context-missing", + input === undefined ? "missing-input" : "missing-request", + ); + return; + } + const admission = admissions.get(admissionId); + const manifest = manifests.get(manifestFingerprint); + const posture = readiness.get(manifestFingerprint); + if (admission === undefined || manifest === undefined || posture === undefined) { + failRequest( + request, + "local-untrusted-js-compute-admission-context-stale", + "missing-request", + ); + return; + } + try { + const safeManifest = localUntrustedJsComputeManifest(manifest); + const safeReadiness = localUntrustedJsComputeReadiness(posture, safeManifest, now()); + const safeArgs = localUntrustedJsComputeArguments( + input.toolCall?.arguments as LocalUntrustedJsComputeArguments, + safeManifest, + ); + validateAdmission(input, request, admission, safeArgs); + const record: LocalUntrustedJsComputeActiveAttempt = { + input, + request, + admission, + manifest: safeManifest, + readiness: safeReadiness, + args: safeArgs, + abortController: new AbortController(), + startedAtMs: now(), + cancelled: false, + settled: false, + }; + active.set(key, record); + emitLifecycle(record, "admitted"); + emitStatus(request, "requested"); + emit(admittedRunRequests, request); + await execute(record); + } catch { + failRequest( + request, + "local-untrusted-js-compute-admission-mismatch", + "mismatched-request", + ); + } + }); + track(work); + }), + ), + ...(opts.cancellationAdmissions ?? []).map((node) => + node.subscribe((message) => { + if (message[0] !== "DATA" || disposed) return; + const admission = message[1] as LocalUntrustedJsComputeCancellationAdmission; + const reject = (code: string) => { + const cancellationId = + plain(admission) && typeof admission.cancellationId === "string" + ? admission.cancellationId + : "invalid"; + const admissionId = + plain(admission) && typeof admission.admissionId === "string" + ? admission.admissionId + : "invalid"; + const runId = + plain(admission) && typeof admission.runId === "string" ? admission.runId : "invalid"; + const attempt = + plain(admission) && Number.isSafeInteger(admission.attempt) + ? Number(admission.attempt) + : 1; + emitIssue( + code, + "Cancellation did not exactly match an active local untrusted JS attempt.", + runId, + ); + emit(cancellations, { + kind: "local-untrusted-js-compute-cancellation-acknowledgement", + admissionId, + cancellationId, + runId, + attempt, + state: "rejected", + code, + }); + }; + if ( + !plain(admission) || + admission.kind !== "local-untrusted-js-compute-cancellation-admission" + ) { + reject("local-untrusted-js-compute-cancellation-invalid"); + return; + } + const record = active.get(runtimeAttemptKey(admission)); + if ( + admission.state !== "admitted" || + record === undefined || + record.settled || + record.cancelled || + consumedCancellationAdmissions.has(admission.admissionId) || + !cancellationMatchesLocalAttempt(admission, record) + ) { + reject("local-untrusted-js-compute-cancellation-coordinate-mismatch"); + return; + } + record.cancelled = true; + consumedCancellationAdmissions.add(admission.admissionId); + record.abortController.abort(); + emit(cancellations, { + kind: "local-untrusted-js-compute-cancellation-acknowledgement", + admissionId: admission.admissionId, + cancellationId: admission.cancellationId, + runId: admission.runId, + attempt: admission.attempt, + state: "accepted", + }); + emit(audit, { + id: compoundTupleKey("local-untrusted-js-compute-cancellation-audit", [ + admission.admissionId, + admission.cancellationId, + ]), + kind: "local-untrusted-js-compute-cancellation-accepted", + subjectId: admission.runId, + sourceRefs: admission.sourceRefs, + metadata: { + attempt: admission.attempt, + epoch: admission.epoch, + runAdmissionId: admission.runAdmissionId, + }, + }); + }), + ), + ]; + return { + admittedRunRequests, + runStatus, + lifecycle, + cleanup, + outcomes, + usage, + cancellations, + issues, + audit, + dispose() { + if (disposePromise === undefined) { + disposed = true; + for (const unsubscribe of unsubscribes) unsubscribe(); + for (const record of active.values()) { + record.cancelled = true; + record.abortController.abort(); + } + disposePromise = Promise.allSettled([...pending]).then(() => { + active.clear(); + terminal.clear(); + consumedCancellationAdmissions.clear(); + inputs.clear(); + admissions.clear(); + manifests.clear(); + readiness.clear(); + group.release({ reason: `${name}:dispose` }); + }); + } + return disposePromise; + }, + }; +} + +export async function runLocalUntrustedJsComputeAttempt( + opts: RunLocalUntrustedJsComputeAttemptOptions, +): Promise { + const manifest = localUntrustedJsComputeManifest(opts.manifest); + const readiness = localUntrustedJsComputeReadiness( + opts.readiness, + manifest, + opts.now?.() ?? Date.now(), + ); + const args = localUntrustedJsComputeArguments(opts.args, manifest); + validateAdmission(opts.adapterInput, opts.admittedRunRequest, opts.runAdmission, args); + if (opts.driver.compatibility !== LOCAL_UNTRUSTED_JS_COMPUTE_COMPATIBILITY) + throw new TypeError("Local untrusted JS compute driver compatibility mismatch."); + if (readiness.state !== "ready") + throw new TypeError("Local untrusted JS compute readiness is not current and ready."); + if ( + opts.material.bundle.byteLength === 0 || + opts.material.bundle.byteLength > manifest.maxBundleBytes + ) + throw new TypeError("Local untrusted JS compute bundle exceeds the admitted byte bound."); + const inputBytes = jsonBytes(opts.material.input, "local untrusted JS compute input"); + if (inputBytes > manifest.maxInputBytes) + throw new TypeError("Local untrusted JS compute input exceeds the admitted byte bound."); + if ((await sha256(opts.material.bundle)) !== args.bundleDigest) + throw new TypeError( + "Local untrusted JS compute bundle bytes do not match the admitted digest.", + ); + if ((await sha256(strictCanonicalJsonBytes(opts.material.input))) !== args.inputDigest) + throw new TypeError("Local untrusted JS compute input bytes do not match the admitted digest."); + if (opts.signal.aborted) + return Object.freeze({ + outcome: "canceled", + code: "local-untrusted-js-compute-canceled-before-allocation", + cleanup: "succeeded", + }); + const outcome = await opts.driver.execute( + Object.freeze({ + runId: args.runId, + attempt: args.attempt, + epoch: args.epoch, + manifestFingerprint: manifest.fingerprint, + hostBindingDigest: readiness.hostBindingDigest, + runAdmissionId: opts.runAdmission.admissionId, + signal: opts.signal, + }), + args, + Object.freeze({ + bundle: new Uint8Array(opts.material.bundle), + input: cloneJson(opts.material.input, "local untrusted JS compute input"), + }), + manifest, + ); + return localUntrustedJsComputeDriverOutcome( + outcome, + args, + manifest, + opts.runAdmission.admissionId, + ); +} + +export function localUntrustedJsComputeManifest( + value: LocalUntrustedJsComputeManifest, +): LocalUntrustedJsComputeManifest { + exactKeys(value, [ + "kind", + "manifestId", + "revision", + "fingerprint", + "compatibilityRevision", + "backend", + "runnerApiRevision", + "runnerImageDigest", + "runnerRevision", + "compilerRevision", + "allowedApiRevision", + "graphreflyPackageRevision", + "sandboxPolicyRevision", + "networkPolicyRevision", + "filesystemPolicyRevision", + "resourcePolicyRevision", + "outputPolicyRevision", + "executionTimeoutMs", + "killGraceMs", + "cleanupTimeoutMs", + "maxBundleBytes", + "maxInputBytes", + "maxOutputBytes", + "maxTopologyNodes", + "maxTopologyEdges", + ]); + if ( + value.kind !== "local-untrusted-js-compute-manifest" || + value.compatibilityRevision !== LOCAL_UNTRUSTED_JS_COMPUTE_COMPATIBILITY || + value.backend !== LOCAL_UNTRUSTED_JS_COMPUTE_BACKEND || + value.runnerApiRevision !== LOCAL_UNTRUSTED_JS_COMPUTE_RUNNER_API || + value.networkPolicyRevision !== "deny-all-v1" || + value.filesystemPolicyRevision !== "read-only-input-bounded-tmp-v1" + ) + throw new TypeError("Invalid local untrusted JS compute manifest identity."); + for (const entry of [ + value.manifestId, + value.revision, + value.fingerprint, + value.runnerRevision, + value.compilerRevision, + value.allowedApiRevision, + value.graphreflyPackageRevision, + value.sandboxPolicyRevision, + value.resourcePolicyRevision, + value.outputPolicyRevision, + ]) + safe(entry, "local untrusted JS compute manifest coordinate"); + digest(value.runnerImageDigest, "runnerImageDigest"); + for (const entry of [ + value.executionTimeoutMs, + value.killGraceMs, + value.cleanupTimeoutMs, + value.maxBundleBytes, + value.maxInputBytes, + value.maxOutputBytes, + value.maxTopologyNodes, + value.maxTopologyEdges, + ]) + positive(entry, "local untrusted JS compute manifest bound"); + if ( + value.executionTimeoutMs > MAX_EXECUTION_TIMEOUT_MS || + value.killGraceMs > MAX_KILL_GRACE_MS || + value.cleanupTimeoutMs > MAX_CLEANUP_TIMEOUT_MS || + value.maxBundleBytes > MAX_MATERIAL_BYTES || + value.maxInputBytes > MAX_MATERIAL_BYTES || + value.maxOutputBytes > MAX_MATERIAL_BYTES || + value.maxTopologyNodes > MAX_TOPOLOGY_NODES || + value.maxTopologyEdges > MAX_TOPOLOGY_EDGES + ) + throw new TypeError("Local untrusted JS compute manifest bound exceeds the v0 ceiling."); + if (value.killGraceMs % 1_000 !== 0) + throw new TypeError("Local untrusted JS compute kill grace must use whole seconds."); + return Object.freeze({ ...value }); +} + +export function localUntrustedJsComputeReadiness( + value: LocalUntrustedJsComputeReadiness, + manifest: LocalUntrustedJsComputeManifest, + now: number, +): LocalUntrustedJsComputeReadiness { + exactKeys(value, [ + "kind", + "manifestFingerprint", + "state", + "observedAtMs", + "expiresAtMs", + "rootlessVerified", + "imageDigestVerified", + "runnerVerified", + "nonRootVerified", + "readOnlyRootFilesystemVerified", + "noNewPrivilegesVerified", + "capabilitiesDroppedVerified", + "noEngineSocketMountVerified", + "noHostBindMountVerified", + "denyNetworkVerified", + "resourceBoundsVerified", + "cancellationVerified", + "cleanupVerified", + "hostBindingDigest", + "attestationRefs", + ]); + if ( + value.kind !== "local-untrusted-js-compute-readiness" || + value.manifestFingerprint !== manifest.fingerprint || + value.state !== "ready" || + !Number.isSafeInteger(value.observedAtMs) || + !Number.isSafeInteger(value.expiresAtMs) || + value.observedAtMs > now || + value.expiresAtMs <= now + ) + throw new TypeError("Invalid or stale local untrusted JS compute readiness."); + const required = [ + value.rootlessVerified, + value.imageDigestVerified, + value.runnerVerified, + value.nonRootVerified, + value.readOnlyRootFilesystemVerified, + value.noNewPrivilegesVerified, + value.capabilitiesDroppedVerified, + value.noEngineSocketMountVerified, + value.noHostBindMountVerified, + value.denyNetworkVerified, + value.resourceBoundsVerified, + value.cancellationVerified, + value.cleanupVerified, + ]; + if (required.some((entry) => entry !== true)) + throw new TypeError("Local untrusted JS compute readiness lacks required certification."); + digest(value.hostBindingDigest, "readiness hostBindingDigest"); + const attestationRefs = stringList(value.attestationRefs, "readiness attestationRefs"); + if (attestationRefs.length === 0) + throw new TypeError("Local untrusted JS compute readiness needs attestation refs."); + return Object.freeze({ ...value, attestationRefs }); +} + +export function localUntrustedJsComputeArguments( + value: LocalUntrustedJsComputeArguments, + manifest: LocalUntrustedJsComputeManifest, +): LocalUntrustedJsComputeArguments { + exactKeys(value, [ + "contractVersion", + "runId", + "attempt", + "epoch", + "sourceRevision", + "sourceDigest", + "bundleRevision", + "bundleDigest", + "compilerRevision", + "allowedApiRevision", + "graphreflyPackageRevision", + "runnerRevision", + "runnerImageDigest", + "sandboxPolicyRevision", + "networkPolicyRevision", + "filesystemPolicyRevision", + "resourcePolicyRevision", + "outputPolicyRevision", + "admittedInputRefs", + "inputDigest", + ]); + if ( + value.contractVersion !== "1" || + value.attempt < 1 || + !Number.isSafeInteger(value.attempt) || + value.compilerRevision !== manifest.compilerRevision || + value.allowedApiRevision !== manifest.allowedApiRevision || + value.graphreflyPackageRevision !== manifest.graphreflyPackageRevision || + value.runnerRevision !== manifest.runnerRevision || + value.runnerImageDigest !== manifest.runnerImageDigest || + value.sandboxPolicyRevision !== manifest.sandboxPolicyRevision || + value.networkPolicyRevision !== manifest.networkPolicyRevision || + value.filesystemPolicyRevision !== manifest.filesystemPolicyRevision || + value.resourcePolicyRevision !== manifest.resourcePolicyRevision || + value.outputPolicyRevision !== manifest.outputPolicyRevision + ) + throw new TypeError("Local untrusted JS compute arguments do not match the manifest."); + for (const entry of [ + value.runId, + value.epoch, + value.sourceRevision, + value.bundleRevision, + value.compilerRevision, + value.allowedApiRevision, + value.graphreflyPackageRevision, + value.runnerRevision, + value.sandboxPolicyRevision, + value.resourcePolicyRevision, + value.outputPolicyRevision, + ]) + safe(entry, "local untrusted JS compute argument coordinate"); + digest(value.sourceDigest, "sourceDigest"); + digest(value.bundleDigest, "bundleDigest"); + digest(value.inputDigest, "inputDigest"); + const admittedInputRefs = stringList(value.admittedInputRefs, "admittedInputRefs"); + if (admittedInputRefs.length === 0) + throw new TypeError("Local untrusted JS compute requires admitted input refs."); + return Object.freeze({ ...value, admittedInputRefs }); +} + +function localUntrustedJsComputeDriverOutcome( + value: LocalUntrustedJsComputeDriverOutcome, + args: LocalUntrustedJsComputeArguments, + manifest: LocalUntrustedJsComputeManifest, + runAdmissionId: string, +): LocalUntrustedJsComputeDriverOutcome { + if ( + !plain(value) || + !["succeeded", "failed", "timeout", "canceled"].includes(String(value.outcome)) + ) + throw new TypeError("Invalid local untrusted JS compute driver outcome."); + if (!["succeeded", "failed", "unverifiable"].includes(String(value.cleanup))) + throw new TypeError("Invalid local untrusted JS compute cleanup state."); + if (value.outcome !== "succeeded") { + safe(value.code, "local untrusted JS compute outcome code"); + return Object.freeze({ outcome: value.outcome, code: value.code, cleanup: value.cleanup }); + } + if (value.cleanup !== "succeeded") + return Object.freeze({ + outcome: "failed", + code: "local-untrusted-js-compute-cleanup-not-verified", + cleanup: value.cleanup, + }); + const result = runnerResult(value.result, args, manifest, runAdmissionId); + if (jsonBytes(result, "local untrusted JS compute result") > manifest.maxOutputBytes) + throw new TypeError("Local untrusted JS compute result exceeds the output byte bound."); + return Object.freeze({ outcome: "succeeded", result, cleanup: "succeeded" }); +} + +function runnerResult( + value: LocalUntrustedJsComputeRunnerResult, + args: LocalUntrustedJsComputeArguments, + manifest: LocalUntrustedJsComputeManifest, + runAdmissionId: string, +): LocalUntrustedJsComputeRunnerResult { + exactKeys(value, ["contractVersion", "answer", "topology", "describe", "provenance", "cleanup"]); + exactKeys(value.provenance, [ + "sourceRevision", + "sourceDigest", + "bundleRevision", + "bundleDigest", + "compilerRevision", + "allowedApiRevision", + "graphreflyPackageRevision", + "runnerRevision", + "runnerImageDigest", + "manifestFingerprint", + "runId", + "attempt", + "graphName", + "admittedInputRefs", + "inputDigest", + "runAdmissionId", + ]); + exactKeys(value.cleanup, ["graphNodesAfterDispose", "graphEdgesAfterDispose"]); + if ( + value.contractVersion !== "1" || + value.provenance.runId !== args.runId || + value.provenance.attempt !== args.attempt || + value.provenance.sourceRevision !== args.sourceRevision || + value.provenance.sourceDigest !== args.sourceDigest || + value.provenance.bundleRevision !== args.bundleRevision || + value.provenance.bundleDigest !== args.bundleDigest || + value.provenance.compilerRevision !== args.compilerRevision || + value.provenance.allowedApiRevision !== args.allowedApiRevision || + value.provenance.graphreflyPackageRevision !== args.graphreflyPackageRevision || + value.provenance.runnerRevision !== args.runnerRevision || + value.provenance.runnerImageDigest !== args.runnerImageDigest || + value.provenance.manifestFingerprint !== manifest.fingerprint || + value.provenance.inputDigest !== args.inputDigest || + value.provenance.runAdmissionId !== runAdmissionId || + !sameStrings(value.provenance.admittedInputRefs, args.admittedInputRefs) || + !plain(value.cleanup) || + value.cleanup.graphNodesAfterDispose !== 0 || + value.cleanup.graphEdgesAfterDispose !== 0 + ) + throw new TypeError("Local untrusted JS compute runner result has mismatched provenance."); + const answer = cloneJson(value.answer, "local untrusted JS compute answer"); + const topology = validateTopologySnapshot(value.topology, manifest); + const describe = validateDescribeSnapshot(value.describe, manifest); + if ( + JSON.stringify(topology) !== JSON.stringify(topologyFromDescribe(describe)) || + topology.name !== value.provenance.graphName || + describe.name !== value.provenance.graphName + ) + throw new TypeError("Local untrusted JS compute topology and describe snapshots disagree."); + const provenance = Object.freeze({ + sourceRevision: value.provenance.sourceRevision, + sourceDigest: value.provenance.sourceDigest, + bundleRevision: value.provenance.bundleRevision, + bundleDigest: value.provenance.bundleDigest, + compilerRevision: value.provenance.compilerRevision, + allowedApiRevision: value.provenance.allowedApiRevision, + graphreflyPackageRevision: value.provenance.graphreflyPackageRevision, + runnerRevision: value.provenance.runnerRevision, + runnerImageDigest: value.provenance.runnerImageDigest, + manifestFingerprint: value.provenance.manifestFingerprint, + runId: value.provenance.runId, + attempt: value.provenance.attempt, + graphName: value.provenance.graphName, + admittedInputRefs: Object.freeze([...value.provenance.admittedInputRefs]), + inputDigest: value.provenance.inputDigest, + runAdmissionId: value.provenance.runAdmissionId, + }); + safe(provenance.graphName, "local untrusted JS compute graph name"); + return Object.freeze({ + contractVersion: "1", + answer, + topology, + describe, + provenance: Object.freeze(provenance), + cleanup: Object.freeze({ graphNodesAfterDispose: 0, graphEdgesAfterDispose: 0 }), + }); +} + +function validateAdmission( + input: ToolProviderAdapterInput, + request: ToolProviderAdapterRunRequested, + admission: ToolProviderRunAdmission, + args: LocalUntrustedJsComputeArguments, +): void { + safe(admission.admissionId, "local untrusted JS compute run admission id"); + const admissionRefs = + request.sourceRefs?.filter((ref) => ref.kind === "tool-provider-run-admission") ?? []; + const admissionRefPresent = admissionRefs.some( + (ref) => ref.kind === "tool-provider-run-admission" && ref.id === admission.admissionId, + ); + if ( + !plain(input) || + input.kind !== "tool-provider-adapter-input" || + input.status !== "ready" || + !plain(request) || + request.kind !== "tool-provider-adapter-run-requested" || + !plain(admission) || + admission.kind !== "tool-provider-run-admission" || + admission.state !== "admitted" || + admission.approvedRunId !== request.runId || + admission.adapterInputId !== input.adapterInputId || + admission.requestId !== input.requestId || + admission.operationId !== input.operationId || + request.runId !== args.runId || + request.attempt !== args.attempt || + request.adapterInputId !== input.adapterInputId || + request.requestId !== input.requestId || + request.operationId !== input.operationId || + request.routeId !== input.routeId || + request.providerId !== input.providerId || + request.executorId !== input.executorId || + request.profileId !== input.profileId || + input.toolName !== "graphrefly.local-untrusted-js-compute" || + input.operation !== "run" || + input.toolCall?.kind !== "tool-call" || + input.toolCall.toolName !== input.toolName || + input.toolCall.operation !== input.operation || + input.toolCall.arguments === undefined || + request.metadata?.approval !== "granted" || + request.metadata.approvalGranted !== true || + request.metadata.admissionId !== admission.admissionId || + request.metadata.proposalId !== admission.proposalId || + request.metadata.approvedFromRunId !== admission.runId || + admissionRefs.length !== 1 || + admissionRefPresent !== true || + !sameBytes(strictCanonicalJsonBytes(input.toolCall.arguments), strictCanonicalJsonBytes(args)) + ) + throw new TypeError("Local untrusted JS compute D419 admission coordinates do not match."); + for (const coordinate of [ + input.adapterInputId, + input.requestId, + input.operationId, + input.routeId, + input.providerId, + input.executorId, + input.profileId, + request.reason, + ]) + safe(coordinate, "local untrusted JS compute D419 coordinate"); +} + +interface TopologyBudget { + nodes: number; + edges: number; + readonly nodeIds: Set; + readonly mountIds: Set; +} + +function validateTopologySnapshot( + value: unknown, + manifest: LocalUntrustedJsComputeManifest, +): GraphTopologySnapshot { + const budget: TopologyBudget = { + nodes: 0, + edges: 0, + nodeIds: new Set(), + mountIds: new Set(), + }; + return topologySnapshot(value, manifest, budget, 0); +} + +function topologySnapshot( + value: unknown, + manifest: LocalUntrustedJsComputeManifest, + budget: TopologyBudget, + depth: number, +): GraphTopologySnapshot { + if (depth > 32) throw new TypeError("Local untrusted JS compute topology is too deeply nested."); + exactOptionalKeys(value, ["nodes", "edges"], ["mountId", "name", "subgraphs"]); + if (!Array.isArray(value.nodes) || !Array.isArray(value.edges)) + throw new TypeError("Local untrusted JS compute topology has an invalid shape."); + budget.nodes += value.nodes.length; + budget.edges += value.edges.length; + if (budget.nodes > manifest.maxTopologyNodes || budget.edges > manifest.maxTopologyEdges) + throw new TypeError("Local untrusted JS compute topology exceeds its bounds."); + const nodes = value.nodes.map((node) => topologyNode(node)); + const ids = new Set(nodes.map((node) => node.id)); + if (ids.size !== nodes.length) + throw new TypeError("Local untrusted JS compute topology has duplicate node ids."); + for (const node of nodes) { + if (budget.nodeIds.has(node.id)) + throw new TypeError("Local untrusted JS compute topology has duplicate global node ids."); + budget.nodeIds.add(node.id); + } + const expectedEdges = new Set(); + for (const node of nodes) { + for (const dep of node.deps) { + if (!ids.has(dep)) + throw new TypeError("Local untrusted JS compute topology has an unknown dependency."); + expectedEdges.add(`${dep}\n${node.id}`); + } + } + const edges = value.edges.map((edge) => topologyEdge(edge)); + const observedEdges = new Set(edges.map((edge) => `${edge.from}\n${edge.to}`)); + if ( + observedEdges.size !== edges.length || + observedEdges.size !== expectedEdges.size || + [...observedEdges].some((edge) => !expectedEdges.has(edge)) + ) + throw new TypeError("Local untrusted JS compute topology edges disagree with node deps."); + const out: GraphTopologySnapshot = { nodes, edges }; + if (depth === 0 && value.mountId !== undefined) + throw new TypeError("Local untrusted JS compute root topology must not have a mount id."); + if (depth > 0 && value.mountId === undefined) + throw new TypeError("Local untrusted JS compute child topology requires a mount id."); + if (value.mountId !== undefined) { + safe(value.mountId, "local untrusted JS compute topology mount id"); + if (budget.mountIds.has(value.mountId)) + throw new TypeError("Local untrusted JS compute topology has duplicate mount ids."); + budget.mountIds.add(value.mountId); + out.mountId = value.mountId; + } + if (value.name !== undefined) { + safe(value.name, "local untrusted JS compute topology name"); + out.name = value.name; + } + if (value.subgraphs !== undefined) { + if (!Array.isArray(value.subgraphs)) + throw new TypeError("Local untrusted JS compute topology subgraphs must be an array."); + out.subgraphs = value.subgraphs.map((entry) => + topologySnapshot(entry, manifest, budget, depth + 1), + ); + } + return Object.freeze(out); +} + +function topologyNode(value: unknown): GraphTopologySnapshot["nodes"][number] { + exactOptionalKeys(value, ["id", "factory", "deps"], ["name", "meta"]); + safe(value.id, "local untrusted JS compute topology node id"); + safe(value.factory, "local untrusted JS compute topology node factory"); + if (!Array.isArray(value.deps)) + throw new TypeError("Local untrusted JS compute topology node deps must be an array."); + const deps = stringList(value.deps, "local untrusted JS compute topology node deps"); + const out: GraphTopologySnapshot["nodes"][number] = { + id: value.id, + factory: value.factory, + deps: [...deps], + }; + if (value.name !== undefined) { + safe(value.name, "local untrusted JS compute topology node name"); + out.name = value.name; + } + if (value.meta !== undefined) { + const meta = cloneJson(value.meta, "local untrusted JS compute topology node meta"); + if (!plain(meta)) + throw new TypeError("Local untrusted JS compute topology node meta must be an object."); + out.meta = meta; + } + return Object.freeze(out); +} + +function topologyEdge(value: unknown): GraphTopologySnapshot["edges"][number] { + exactKeys(value, ["from", "to"]); + safe(value.from, "local untrusted JS compute topology edge source"); + safe(value.to, "local untrusted JS compute topology edge target"); + return Object.freeze({ from: value.from, to: value.to }); +} + +function validateDescribeSnapshot( + value: unknown, + manifest: LocalUntrustedJsComputeManifest, +): DescribeSnapshot { + const budget: TopologyBudget = { + nodes: 0, + edges: 0, + nodeIds: new Set(), + mountIds: new Set(), + }; + return describeSnapshot(value, manifest, budget, 0); +} + +function describeSnapshot( + value: unknown, + manifest: LocalUntrustedJsComputeManifest, + budget: TopologyBudget, + depth: number, +): DescribeSnapshot { + if (depth > 32) throw new TypeError("Local untrusted JS compute describe is too deeply nested."); + exactOptionalKeys(value, ["nodes", "edges"], ["mountId", "name", "subgraphs"]); + if (!Array.isArray(value.nodes) || !Array.isArray(value.edges)) + throw new TypeError("Local untrusted JS compute describe has an invalid shape."); + budget.nodes += value.nodes.length; + budget.edges += value.edges.length; + if (budget.nodes > manifest.maxTopologyNodes || budget.edges > manifest.maxTopologyEdges) + throw new TypeError("Local untrusted JS compute describe exceeds its bounds."); + const nodes = value.nodes.map((node) => describeNode(node)); + const ids = new Set(nodes.map((node) => node.id)); + if (ids.size !== nodes.length) + throw new TypeError("Local untrusted JS compute describe has duplicate node ids."); + for (const node of nodes) { + if (budget.nodeIds.has(node.id)) + throw new TypeError("Local untrusted JS compute describe has duplicate global node ids."); + budget.nodeIds.add(node.id); + } + const expectedEdges = new Set(); + for (const node of nodes) { + for (const dep of node.deps) { + if (!ids.has(dep)) + throw new TypeError("Local untrusted JS compute describe has an unknown dependency."); + expectedEdges.add(`${dep}\n${node.id}`); + } + } + const edges = value.edges.map((edge) => topologyEdge(edge)); + const observedEdges = new Set(edges.map((edge) => `${edge.from}\n${edge.to}`)); + if ( + observedEdges.size !== edges.length || + observedEdges.size !== expectedEdges.size || + [...observedEdges].some((edge) => !expectedEdges.has(edge)) + ) + throw new TypeError("Local untrusted JS compute describe edges disagree with node deps."); + const out: DescribeSnapshot = { nodes, edges }; + if (depth === 0 && value.mountId !== undefined) + throw new TypeError("Local untrusted JS compute root describe must not have a mount id."); + if (depth > 0 && value.mountId === undefined) + throw new TypeError("Local untrusted JS compute child describe requires a mount id."); + if (value.mountId !== undefined) { + safe(value.mountId, "local untrusted JS compute describe mount id"); + if (budget.mountIds.has(value.mountId)) + throw new TypeError("Local untrusted JS compute describe has duplicate mount ids."); + budget.mountIds.add(value.mountId); + out.mountId = value.mountId; + } + if (value.name !== undefined) { + safe(value.name, "local untrusted JS compute describe name"); + out.name = value.name; + } + if (value.subgraphs !== undefined) { + if (!Array.isArray(value.subgraphs)) + throw new TypeError("Local untrusted JS compute describe subgraphs must be an array."); + out.subgraphs = value.subgraphs.map((entry) => + describeSnapshot(entry, manifest, budget, depth + 1), + ); + } + return Object.freeze(out); +} + +function describeNode(value: unknown): DescribeSnapshot["nodes"][number] { + exactOptionalKeys( + value, + ["id", "factory", "deps", "status"], + ["name", "meta", "value", "version"], + ); + const topology = topologyNode({ + id: value.id, + factory: value.factory, + deps: value.deps, + ...(value.name === undefined ? {} : { name: value.name }), + ...(value.meta === undefined ? {} : { meta: value.meta }), + }); + if ( + !["sentinel", "pending", "dirty", "settled", "resolved", "completed", "errored"].includes( + String(value.status), + ) + ) + throw new TypeError("Local untrusted JS compute describe node status is invalid."); + const out: DescribeSnapshot["nodes"][number] = { + ...topology, + status: value.status as DescribeSnapshot["nodes"][number]["status"], + }; + if (Object.hasOwn(value, "value")) + out.value = cloneJson(value.value, "local untrusted JS compute describe node value"); + if (value.version !== undefined) out.version = nodeVersion(value.version); + return Object.freeze(out); +} + +function nodeVersion(value: unknown): NonNullable { + if (!plain(value)) + throw new TypeError("Local untrusted JS compute describe node version is invalid."); + if (value.level === 0) { + exactKeys(value, ["level", "counter"]); + if (!Number.isSafeInteger(value.counter) || Number(value.counter) < 0) + throw new TypeError("Local untrusted JS compute describe node version is invalid."); + return Object.freeze({ level: 0, counter: Number(value.counter) }); + } + exactKeys(value, ["level", "counter", "cid", "prev"]); + if ( + value.level !== 1 || + !Number.isSafeInteger(value.counter) || + Number(value.counter) < 0 || + typeof value.cid !== "string" || + (value.prev !== null && typeof value.prev !== "string") + ) + throw new TypeError("Local untrusted JS compute describe node version is invalid."); + safe(value.cid, "local untrusted JS compute describe node version cid"); + if (value.prev !== null) + safe(value.prev, "local untrusted JS compute describe node previous version cid"); + return Object.freeze({ + level: 1, + counter: Number(value.counter), + cid: value.cid, + prev: value.prev, + }); +} + +function exactKeys( + value: unknown, + keys: readonly string[], +): asserts value is Record { + if (!plain(value) || Object.keys(value).sort().join("\n") !== [...keys].sort().join("\n")) + throw new TypeError("Local untrusted JS compute contract has an unexpected shape."); +} + +function runtimeAttemptKey(value: { readonly runId: string; readonly attempt: number }): string { + return compoundTupleKey("local-untrusted-js-compute-attempt", [ + value.runId, + String(value.attempt), + ]); +} + +function cancellationMatchesLocalAttempt( + value: LocalUntrustedJsComputeCancellationAdmission, + record: LocalUntrustedJsComputeActiveAttempt, +): boolean { + try { + exactKeys(value, [ + "kind", + "admissionId", + "cancellationId", + "state", + "runId", + "adapterInputId", + "requestId", + "operationId", + "routeId", + "providerId", + "executorId", + "profileId", + "runAdmissionId", + "attempt", + "epoch", + "manifestFingerprint", + "hostBindingDigest", + "sourceRefs", + ]); + for (const coordinate of [ + value.admissionId, + value.cancellationId, + value.runId, + value.adapterInputId, + value.requestId, + value.operationId, + value.routeId, + value.providerId, + value.executorId, + value.profileId, + value.runAdmissionId, + value.epoch, + value.manifestFingerprint, + ]) + safe(coordinate, "local untrusted JS compute cancellation coordinate"); + digest(value.hostBindingDigest, "local untrusted JS compute cancellation host binding"); + if ( + !Number.isSafeInteger(value.attempt) || + value.attempt < 1 || + !Array.isArray(value.sourceRefs) || + value.sourceRefs.length > 32 || + value.sourceRefs.some( + (ref) => + !plain(ref) || + Object.keys(ref).sort().join("\n") !== ["id", "kind"].join("\n") || + typeof ref.kind !== "string" || + typeof ref.id !== "string", + ) + ) + return false; + for (const ref of value.sourceRefs) { + safe(ref.kind, "local untrusted JS compute cancellation source ref kind"); + safe(ref.id, "local untrusted JS compute cancellation source ref id"); + } + } catch { + return false; + } + return ( + value.state === "admitted" && + value.runId === record.request.runId && + value.adapterInputId === record.input.adapterInputId && + value.requestId === record.input.requestId && + value.operationId === record.input.operationId && + value.routeId === record.input.routeId && + value.providerId === record.input.providerId && + value.executorId === record.input.executorId && + value.profileId === record.input.profileId && + value.runAdmissionId === record.admission.admissionId && + value.attempt === record.request.attempt && + value.epoch === record.args.epoch && + value.manifestFingerprint === record.manifest.fingerprint && + value.hostBindingDigest === record.readiness.hostBindingDigest + ); +} + +function exactOptionalKeys( + value: unknown, + required: readonly string[], + optional: readonly string[], +): asserts value is Record { + if ( + !plain(value) || + required.some((key) => !Object.hasOwn(value, key)) || + Object.keys(value).some((key) => !required.includes(key) && !optional.includes(key)) + ) + throw new TypeError("Local untrusted JS compute contract has an unexpected shape."); +} + +function plain(value: unknown): value is Record { + if (value === null || typeof value !== "object" || Array.isArray(value)) return false; + const proto = Object.getPrototypeOf(value); + return proto === Object.prototype || proto === null; +} + +function safe(value: unknown, label: string): asserts value is string { + if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:/@+-]{0,255}$/.test(value)) + throw new TypeError(`Invalid ${label}.`); +} + +function digest(value: unknown, label: string): asserts value is string { + if (typeof value !== "string" || !/^sha256:[a-f0-9]{64}$/.test(value)) + throw new TypeError(`Invalid ${label}.`); +} + +function positive(value: unknown, label: string): asserts value is number { + if (!Number.isSafeInteger(value) || Number(value) < 1) throw new TypeError(`Invalid ${label}.`); +} + +function stringList(value: readonly string[], label: string): readonly string[] { + if (!Array.isArray(value) || value.length > 64) throw new TypeError(`Invalid ${label}.`); + const out = value.map((entry) => { + safe(entry, label); + return entry; + }); + if (new Set(out).size !== out.length) throw new TypeError(`Duplicate ${label}.`); + return Object.freeze(out); +} + +function sameStrings(left: unknown, right: readonly string[]): boolean { + return ( + Array.isArray(left) && + left.length === right.length && + left.every((item, index) => item === right[index]) + ); +} + +function jsonBytes(value: unknown, label: string): number { + return new TextEncoder().encode(JSON.stringify(cloneJson(value, label))).byteLength; +} + +async function sha256(bytes: Uint8Array): Promise { + const crypto = globalThis.crypto; + if (crypto?.subtle === undefined) + throw new TypeError("Local untrusted JS compute SHA-256 authority is unavailable."); + const digestBytes = new Uint8Array( + await crypto.subtle.digest("SHA-256", new Uint8Array(bytes).buffer), + ); + return `sha256:${[...digestBytes].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`; +} + +function sameBytes(left: Uint8Array, right: Uint8Array): boolean { + return left.byteLength === right.byteLength && left.every((byte, index) => byte === right[index]); +} + +function cloneJson( + value: unknown, + label: string, + seen = new WeakSet(), +): LocalUntrustedJsJson { + if (value === null || typeof value === "string" || typeof value === "boolean") return value; + if (typeof value === "number") { + if (!Number.isFinite(value)) throw new TypeError(`${label} must contain finite JSON values.`); + return value; + } + if (typeof value !== "object") throw new TypeError(`${label} must be JSON data.`); + if (seen.has(value)) throw new TypeError(`${label} must not contain cycles.`); + seen.add(value); + if (Array.isArray(value)) { + const out = value.map((entry) => cloneJson(entry, label, seen)); + seen.delete(value); + return Object.freeze(out); + } + if (!plain(value)) throw new TypeError(`${label} must contain only plain records.`); + const out: Record = {}; + for (const [key, entry] of Object.entries(value)) { + if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(key)) + throw new TypeError(`${label} contains an invalid key.`); + out[key] = cloneJson(entry, label, seen); + } + seen.delete(value); + return Object.freeze(out); +} diff --git a/packages/ts/src/executors/local-untrusted-js-compute/node.ts b/packages/ts/src/executors/local-untrusted-js-compute/node.ts new file mode 100644 index 00000000..4250486e --- /dev/null +++ b/packages/ts/src/executors/local-untrusted-js-compute/node.ts @@ -0,0 +1,945 @@ +/** D667 Node rootless-Podman Libpod API v0 broker. */ +import { execFile } from "node:child_process"; +import { createHash, randomUUID } from "node:crypto"; +import { lstat, realpath } from "node:fs/promises"; +import { request as httpRequest } from "node:http"; +import { promisify } from "node:util"; +import { strictCanonicalJsonBytes } from "../../json/codec.js"; +import type { + LocalUntrustedJsComputeArguments, + LocalUntrustedJsComputeDriver, + LocalUntrustedJsComputeDriverContext, + LocalUntrustedJsComputeDriverOutcome, + LocalUntrustedJsComputeManifest, + LocalUntrustedJsComputeMaterial, + LocalUntrustedJsComputeRunnerControl, + LocalUntrustedJsComputeRunnerResult, +} from "../local-untrusted-js-compute.js"; +import { LOCAL_UNTRUSTED_JS_COMPUTE_COMPATIBILITY } from "../local-untrusted-js-compute.js"; + +const execFileAsync = promisify(execFile); +const API_REVISION = "5.0.3"; +const RUNNER_ENTRYPOINT = ["/usr/local/bin/node", "/opt/graphrefly/local-untrusted-js-runner.mjs"]; +const BOUNDARY_LABEL = "d667-local-untrusted-js-compute"; +const DEFAULT_REQUEST_TIMEOUT_MS = 10_000; +const MAX_ENGINE_RESPONSE_BYTES = 512 * 1024; +const MAX_LOG_BYTES = 1024 * 1024; +const MAX_CONTROL_BYTES = 32 * 1024; +const MEMORY_LIMIT_BYTES = 256 * 1024 * 1024; +const TMPFS_LIMIT_BYTES = 16 * 1024 * 1024; +const PIDS_LIMIT = 64; +const CPU_PERIOD = 100_000; +const CPU_QUOTA = 100_000; +const FILE_SIZE_LIMIT_BYTES = 16 * 1024 * 1024; +const NOFILE_LIMIT = 128; +const ID = /^[a-f0-9]{64}$/; +const SAFE_IMAGE = /^[A-Za-z0-9][A-Za-z0-9._:/@+-]{0,254}$/; +const NAMED_DIGEST_IMAGE = /^[A-Za-z0-9][A-Za-z0-9._:/+-]{0,190}@sha256:[a-f0-9]{64}$/; + +interface PodmanBinding { + readonly socketPath: string; + readonly containerId?: string; + readonly containerName: string; + readonly runLabel: string; + readonly manifestFingerprint: string; + readonly capabilityDrop: readonly string[]; +} + +interface PodmanHostBinding { + readonly socketPath: string; + readonly hostBindingDigest: string; + readonly capabilityDrop: readonly string[]; +} + +interface RawResponse { + readonly status: number; + readonly body: Uint8Array; +} + +export interface NodeLocalUntrustedJsComputeDriverOptions { + readonly imageRef: string; +} + +export interface NodeLocalUntrustedJsComputeHostBindingAttestation { + readonly kind: "node-local-untrusted-js-compute-host-binding"; + readonly hostBindingDigest: string; + readonly apiRevision: typeof API_REVISION; + readonly rootless: true; +} + +/** + * Returns only a digest-bound host attestation. Socket paths and engine handles + * remain private; the broker rechecks the same digest before every allocation. + */ +export async function nodeLocalUntrustedJsComputeHostBindingAttestation( + signal?: AbortSignal, +): Promise { + const binding = await discoverRootlessPodmanBinding(signal); + if (binding === undefined) + throw new TypeError("D667 rootless Podman host binding is unavailable."); + return Object.freeze({ + kind: "node-local-untrusted-js-compute-host-binding", + hostBindingDigest: binding.hostBindingDigest, + apiRevision: API_REVISION, + rootless: true, + }); +} + +/** + * Creates the exact D667 Node broker. The caller selects no endpoint, command, + * entrypoint, network, mount, user or containment option. + */ +export function nodeLocalUntrustedJsComputeDriver( + opts: NodeLocalUntrustedJsComputeDriverOptions, +): LocalUntrustedJsComputeDriver { + if (!SAFE_IMAGE.test(opts.imageRef) || !NAMED_DIGEST_IMAGE.test(opts.imageRef)) + throw new TypeError("D667 runner image must be digest pinned."); + return Object.freeze({ + compatibility: LOCAL_UNTRUSTED_JS_COMPUTE_COMPATIBILITY, + execute: async ( + context: LocalUntrustedJsComputeDriverContext, + args: LocalUntrustedJsComputeArguments, + material: LocalUntrustedJsComputeMaterial, + manifest: LocalUntrustedJsComputeManifest, + ): Promise => + executeAttempt(opts.imageRef, context, args, material, manifest), + }); +} + +async function executeAttempt( + imageRef: string, + context: LocalUntrustedJsComputeDriverContext, + args: LocalUntrustedJsComputeArguments, + material: LocalUntrustedJsComputeMaterial, + manifest: LocalUntrustedJsComputeManifest, +): Promise { + let binding: PodmanBinding | undefined; + let outcome: LocalUntrustedJsComputeDriverOutcome | undefined; + let createRequestSettled = true; + const attemptDeadline = Date.now() + manifest.executionTimeoutMs; + const timeoutSignal = AbortSignal.timeout(manifest.executionTimeoutMs); + const attemptSignal = AbortSignal.any([context.signal, timeoutSignal]); + try { + if (!imageRef.endsWith(`@${manifest.runnerImageDigest}`)) + throw new TypeError("D667 runner image does not match the admitted manifest digest."); + const observedBundleDigest = `sha256:${createHash("sha256").update(material.bundle).digest("hex")}`; + if (observedBundleDigest !== args.bundleDigest) + throw new TypeError("D667 bundle bytes do not match the admitted digest."); + const host = await discoverRootlessPodmanBinding(attemptSignal); + if (host === undefined) + return failed("local-untrusted-js-compute-podman-unavailable", "unverifiable"); + if (host.hostBindingDigest !== context.hostBindingDigest) + return failed("local-untrusted-js-compute-host-binding-mismatch", "unverifiable"); + const socketPath = host.socketPath; + const image = await jsonRequest( + socketPath, + `/v${API_REVISION}/libpod/images/${encodeURIComponent(imageRef)}/json`, + attemptSignal, + "GET", + undefined, + remainingMs(attemptDeadline), + ); + if ( + image.status !== 200 || + !record(image.body) || + image.body.Digest !== manifest.runnerImageDigest || + !Array.isArray(image.body.RepoDigests) || + !image.body.RepoDigests.includes(imageRef) + ) + return failed("local-untrusted-js-compute-runner-image-unverified", "unverifiable"); + const suffix = randomUUID(); + const containerName = `graphrefly-d667-${suffix}`; + const runLabel = labelValue({ + runId: context.runId, + attempt: context.attempt, + epoch: context.epoch, + manifestFingerprint: context.manifestFingerprint, + runAdmissionId: context.runAdmissionId, + }); + binding = { + socketPath, + containerName, + runLabel, + manifestFingerprint: context.manifestFingerprint, + capabilityDrop: host.capabilityDrop, + }; + createRequestSettled = false; + const created = await jsonRequest( + socketPath, + `/v${API_REVISION}/libpod/containers/create`, + attemptSignal, + "POST", + containerRequest(containerName, imageRef, manifest, runLabel), + remainingMs(attemptDeadline), + ); + createRequestSettled = true; + if ( + created.status !== 201 || + !record(created.body) || + typeof created.body.Id !== "string" || + !ID.test(created.body.Id) || + !Array.isArray(created.body.Warnings) || + created.body.Warnings.length !== 0 + ) { + outcome = failed("local-untrusted-js-compute-container-create-failed", "unverifiable"); + return outcome; + } + binding = { ...binding, containerId: created.body.Id }; + const inspected = await jsonRequest( + socketPath, + `/v${API_REVISION}/libpod/containers/${binding.containerId}/json`, + attemptSignal, + "GET", + undefined, + remainingMs(attemptDeadline), + ); + if (!inspectMatches(inspected, binding, imageRef, manifest)) { + outcome = failed("local-untrusted-js-compute-containment-mismatch", "unverifiable"); + return outcome; + } + const input = strictCanonicalJsonBytes(material.input); + const control: LocalUntrustedJsComputeRunnerControl = { + contractVersion: "1", + compatibilityRevision: LOCAL_UNTRUSTED_JS_COMPUTE_COMPATIBILITY, + runnerApiRevision: "graphrefly-runner-api-v1", + manifestFingerprint: manifest.fingerprint, + args, + runAdmissionId: context.runAdmissionId, + }; + const controlBytes = strictCanonicalJsonBytes(control); + if (controlBytes.byteLength > MAX_CONTROL_BYTES) + throw new TypeError("D667 runner control exceeds its byte bound."); + const archive = tarArchive([ + { path: "input/bundle.mjs", bytes: material.bundle, mode: 0o444 }, + { path: "input/input.json", bytes: input, mode: 0o444 }, + { path: "input/control.json", bytes: controlBytes, mode: 0o444 }, + ]); + const uploaded = await rawRequest( + socketPath, + `/v${API_REVISION}/libpod/containers/${binding.containerId}/archive?path=/`, + attemptSignal, + "PUT", + archive, + "application/x-tar", + remainingMs(attemptDeadline), + ); + if (uploaded.status !== 200) { + outcome = failed("local-untrusted-js-compute-input-upload-failed", "unverifiable"); + return outcome; + } + const started = await rawRequest( + socketPath, + `/v${API_REVISION}/libpod/containers/${binding.containerId}/start`, + attemptSignal, + "POST", + undefined, + undefined, + remainingMs(attemptDeadline), + ); + if (started.status !== 204) { + outcome = failed("local-untrusted-js-compute-container-start-failed", "unverifiable"); + return outcome; + } + let waited: RawResponse; + try { + waited = await rawRequest( + socketPath, + `/v${API_REVISION}/libpod/containers/${binding.containerId}/wait?condition=exited`, + attemptSignal, + "POST", + undefined, + undefined, + remainingMs(attemptDeadline), + ); + } catch (error) { + if (context.signal.aborted) { + outcome = failed("local-untrusted-js-compute-canceled", "unverifiable", "canceled"); + } else if (timeoutSignal.aborted || Date.now() >= attemptDeadline) { + outcome = failed("local-untrusted-js-compute-timeout", "unverifiable", "timeout"); + } else { + throw error; + } + return outcome; + } + const exitCode = parseExitCode(waited); + let logs: RawResponse; + try { + logs = await rawRequest( + socketPath, + `/v${API_REVISION}/libpod/containers/${binding.containerId}/logs?stdout=true&stderr=false`, + attemptSignal, + "GET", + undefined, + undefined, + remainingMs(attemptDeadline), + Math.min(MAX_LOG_BYTES, manifest.maxOutputBytes + 8 * 1024), + ); + } catch (error) { + if (error instanceof ResponseBoundError) { + outcome = failed("local-untrusted-js-compute-output-overflow", "unverifiable"); + return outcome; + } + throw error; + } + if (logs.status !== 200 || exitCode !== 0) { + outcome = failed("local-untrusted-js-compute-runner-failed", "unverifiable"); + return outcome; + } + const result = parseRunnerResult(logs.body); + outcome = { outcome: "succeeded", result, cleanup: "unverifiable" }; + return outcome; + } catch (error) { + if (context.signal.aborted) + outcome = failed("local-untrusted-js-compute-canceled", "unverifiable", "canceled"); + else if (timeoutSignal.aborted || Date.now() >= attemptDeadline) + outcome = failed("local-untrusted-js-compute-timeout", "unverifiable", "timeout"); + else + outcome = failed( + error instanceof TypeError + ? "local-untrusted-js-compute-invalid-private-material" + : "local-untrusted-js-compute-driver-failed", + "unverifiable", + ); + return outcome; + } finally { + if (binding !== undefined) { + const cleanup: "succeeded" | "unverifiable" | false = await removeAndVerify( + binding, + manifest.killGraceMs, + manifest.cleanupTimeoutMs, + createRequestSettled, + ).catch((): false => false); + if (outcome !== undefined) + (outcome as { cleanup: "succeeded" | "failed" | "unverifiable" }).cleanup = + cleanup === false ? "failed" : cleanup; + } + } +} + +function failed( + code: string, + cleanup: "failed" | "unverifiable", + outcome: "failed" | "timeout" | "canceled" = "failed", +): LocalUntrustedJsComputeDriverOutcome { + return { outcome, code, cleanup }; +} + +function containerRequest( + name: string, + image: string, + manifest: LocalUntrustedJsComputeManifest, + runLabel: string, +): Record { + return { + name, + image, + entrypoint: [...RUNNER_ENTRYPOINT], + command: ["/input/bundle.mjs", "/input/input.json", "/input/control.json"], + user: "65532:65532", + env: {}, + unsetenvall: true, + env_host: false, + httpproxy: false, + image_volume_mode: "ignore", + read_only_filesystem: true, + read_write_tmpfs: false, + mounts: [ + { + destination: "/tmp", + type: "tmpfs", + source: "tmpfs", + options: ["rw", "nosuid", "nodev", "noexec", `size=${TMPFS_LIMIT_BYTES}`, "mode=0700"], + }, + ], + r_limits: [ + { type: "RLIMIT_FSIZE", hard: FILE_SIZE_LIMIT_BYTES, soft: FILE_SIZE_LIMIT_BYTES }, + { type: "RLIMIT_NOFILE", hard: NOFILE_LIMIT, soft: NOFILE_LIMIT }, + ], + privileged: false, + cap_drop: ["all"], + no_new_privileges: true, + terminal: false, + stdin: false, + remove: false, + stop_signal: 15, + publish_image_ports: false, + netns: { nsmode: "none" }, + labels: { + "dev.graphrefly.boundary": BOUNDARY_LABEL, + "dev.graphrefly.manifest": manifest.fingerprint, + "dev.graphrefly.run": runLabel, + }, + resource_limits: { + memory: { limit: MEMORY_LIMIT_BYTES }, + cpu: { period: CPU_PERIOD, quota: CPU_QUOTA }, + pids: { limit: PIDS_LIMIT }, + }, + }; +} + +function inspectMatches( + response: { readonly status: number; readonly body: unknown }, + binding: PodmanBinding, + imageRef: string, + manifest: LocalUntrustedJsComputeManifest, +): boolean { + if (response.status !== 200 || !record(response.body)) return false; + const config = record(response.body.Config) ? response.body.Config : undefined; + const host = record(response.body.HostConfig) ? response.body.HostConfig : undefined; + const labels = config && record(config.Labels) ? config.Labels : undefined; + const mounts = Array.isArray(response.body.Mounts) ? response.body.Mounts : undefined; + return ( + response.body.Id === binding.containerId && + response.body.Name === binding.containerName && + config?.Image === imageRef && + config?.User === "65532:65532" && + exactStrings(config?.Entrypoint, RUNNER_ENTRYPOINT) && + exactStrings(config?.Cmd, ["/input/bundle.mjs", "/input/input.json", "/input/control.json"]) && + labels?.["dev.graphrefly.boundary"] === BOUNDARY_LABEL && + labels?.["dev.graphrefly.manifest"] === manifest.fingerprint && + labels?.["dev.graphrefly.run"] === binding.runLabel && + exactStrings(config?.Env, []) && + host?.ReadonlyRootfs === true && + host?.Privileged === false && + exactUnorderedStrings(host?.SecurityOpt, ["no-new-privileges"]) && + Array.isArray(host?.CapDrop) && + exactUnorderedStrings(host.CapDrop, binding.capabilityDrop) && + Array.isArray(host?.CapAdd) && + host.CapAdd.length === 0 && + host.Memory === MEMORY_LIMIT_BYTES && + host.CpuPeriod === CPU_PERIOD && + host.CpuQuota === CPU_QUOTA && + host.PidsLimit === PIDS_LIMIT && + host.NetworkMode === "none" && + host.PublishAllPorts === false && + record(host.PortBindings) && + Object.keys(host.PortBindings).length === 0 && + tmpfsMatches(host.Tmpfs) && + rlimitsMatch(host.Ulimits) && + mountsMatch(mounts) + ); +} + +async function stopAndKill( + binding: PodmanBinding, + graceMs: number, + signal: AbortSignal, + deadline: number, +): Promise { + const seconds = graceMs / 1_000; + const identifier = binding.containerId ?? binding.containerName; + const stopped = await rawRequest( + binding.socketPath, + `/v${API_REVISION}/libpod/containers/${identifier}/stop?t=${seconds}`, + signal, + "POST", + undefined, + undefined, + remainingMs(deadline), + ).catch(() => undefined); + if (stopped?.status === 204 || stopped?.status === 304 || stopped?.status === 404) return; + await rawRequest( + binding.socketPath, + `/v${API_REVISION}/libpod/containers/${identifier}/kill?signal=KILL`, + signal, + "POST", + undefined, + undefined, + remainingMs(deadline), + ).catch(() => undefined); +} + +async function removeAndVerify( + binding: PodmanBinding, + graceMs: number, + cleanupTimeoutMs: number, + createRequestSettled: boolean, +): Promise<"succeeded" | "unverifiable" | false> { + const deadline = Date.now() + cleanupTimeoutMs; + const signal = AbortSignal.timeout(cleanupTimeoutMs); + await stopAndKill(binding, graceMs, signal, deadline); + const identifier = binding.containerId ?? binding.containerName; + const removed = await rawRequest( + binding.socketPath, + `/v${API_REVISION}/libpod/containers/${identifier}?force=true&v=true`, + signal, + "DELETE", + undefined, + undefined, + remainingMs(deadline), + ).catch(() => undefined); + if (removed?.status !== 200 && removed?.status !== 204 && removed?.status !== 404) return false; + let absentSince: number | undefined; + while (Date.now() < deadline) { + const residues = await labeledResidues(binding, signal, deadline); + if (residues === undefined) return false; + for (const id of residues) { + await rawRequest( + binding.socketPath, + `/v${API_REVISION}/libpod/containers/${id}?force=true&v=true`, + signal, + "DELETE", + undefined, + undefined, + remainingMs(deadline), + ).catch(() => undefined); + } + if (residues.length > 0) { + absentSince = undefined; + continue; + } + const absent = await rawRequest( + binding.socketPath, + `/v${API_REVISION}/libpod/containers/${identifier}/json`, + signal, + "GET", + undefined, + undefined, + remainingMs(deadline), + ).catch(() => undefined); + if (absent?.status !== 404) return false; + absentSince ??= Date.now(); + if (Date.now() - absentSince >= 100) return createRequestSettled ? "succeeded" : "unverifiable"; + await delay(20, signal).catch(() => undefined); + } + return createRequestSettled ? false : "unverifiable"; +} + +async function labeledResidues( + binding: PodmanBinding, + signal: AbortSignal, + deadline: number, +): Promise { + const filters = encodeURIComponent( + JSON.stringify({ + label: [ + `dev.graphrefly.boundary=${BOUNDARY_LABEL}`, + `dev.graphrefly.run=${binding.runLabel}`, + `dev.graphrefly.manifest=${binding.manifestFingerprint}`, + ], + }), + ); + const response = await jsonRequest( + binding.socketPath, + `/v${API_REVISION}/libpod/containers/json?all=true&filters=${filters}`, + signal, + "GET", + undefined, + remainingMs(deadline), + ).catch(() => undefined); + if (response?.status !== 200 || !Array.isArray(response.body)) return undefined; + const ids: string[] = []; + for (const entry of response.body) { + if ( + !record(entry) || + typeof entry.Id !== "string" || + !ID.test(entry.Id) || + (binding.containerId !== undefined && entry.Id !== binding.containerId) || + !record(entry.Labels) || + entry.Labels["dev.graphrefly.boundary"] !== BOUNDARY_LABEL || + entry.Labels["dev.graphrefly.run"] !== binding.runLabel || + entry.Labels["dev.graphrefly.manifest"] !== binding.manifestFingerprint || + !Array.isArray(entry.Names) || + !entry.Names.includes(binding.containerName) + ) + return undefined; + ids.push(entry.Id); + } + return ids; +} + +async function discoverRootlessPodmanBinding( + signal?: AbortSignal, +): Promise { + if (signal?.aborted) throw signal.reason; + const runtimeDirectoryValue = Reflect.get(process.env, "XDG_RUNTIME_DIR"); + const runtimeDirectory = + typeof runtimeDirectoryValue === "string" && runtimeDirectoryValue.length > 0 + ? runtimeDirectoryValue + : undefined; + const homeDirectoryValue = Reflect.get(process.env, "HOME"); + const candidates = ( + runtimeDirectory !== undefined + ? [`${runtimeDirectory}/podman/podman.sock`] + : [ + process.platform === "darwin" + ? `${typeof homeDirectoryValue === "string" ? homeDirectoryValue : ""}/.local/share/containers/podman/machine/podman.sock` + : undefined, + ] + ).filter((entry): entry is string => typeof entry === "string" && entry.length > 0); + for (const candidate of candidates) { + const socketPath = await verifiedSocketPath(candidate); + if (socketPath !== undefined) { + const binding = await attestSocket(socketPath, signal); + if (binding !== undefined) return binding; + } + } + let connectionStdout: string; + try { + ({ stdout: connectionStdout } = await execFileAsync( + "podman", + ["system", "connection", "list", "--format", "json"], + { encoding: "utf8", maxBuffer: MAX_ENGINE_RESPONSE_BYTES, signal }, + )); + } catch (error) { + if (signal?.aborted) throw error; + return undefined; + } + const connections = parseJson(new TextEncoder().encode(connectionStdout)); + if (!Array.isArray(connections)) return undefined; + for (const connection of connections) { + if ( + record(connection) && + connection.Default === true && + typeof connection.URI === "string" && + connection.URI.startsWith("unix://") + ) { + const candidate = connection.URI.slice("unix://".length); + const socketPath = await verifiedSocketPath(candidate); + if (socketPath !== undefined) { + const binding = await attestSocket(socketPath, signal); + if (binding !== undefined) return binding; + } + } + } + return undefined; +} + +async function verifiedSocketPath(candidate: string): Promise { + if (!candidate.startsWith("/")) return undefined; + const direct = await lstat(candidate).catch(() => undefined); + const resolved = direct?.isSocket() + ? candidate + : await realpath(candidate).catch(() => undefined); + if (resolved === undefined || !resolved.startsWith("/")) return undefined; + const stat = direct?.isSocket() ? direct : await lstat(resolved).catch(() => undefined); + const currentUid = typeof process.getuid === "function" ? process.getuid() : undefined; + return stat?.isSocket() && (currentUid === undefined || stat.uid === currentUid) + ? resolved + : undefined; +} + +async function attestSocket( + socketPath: string, + signal?: AbortSignal, +): Promise { + const response = await jsonRequest(socketPath, `/v${API_REVISION}/libpod/info`, signal).catch( + (error) => { + if (signal?.aborted) throw error; + return undefined; + }, + ); + if ( + response?.status !== 200 || + !record(response.body) || + !record(response.body.host) || + !record(response.body.host.security) || + response.body.host.security.rootless !== true || + typeof response.body.host.security.capabilities !== "string" || + !record(response.body.version) || + response.body.version.APIVersion !== API_REVISION + ) + return undefined; + const capabilityDrop = response.body.host.security.capabilities + .split(",") + .filter((entry): entry is string => /^CAP_[A-Z0-9_]+$/.test(entry)); + if (capabilityDrop.length === 0 || new Set(capabilityDrop).size !== capabilityDrop.length) + return undefined; + const stat = await lstat(socketPath); + const material = JSON.stringify({ + socket: { path: socketPath, device: String(stat.dev), inode: String(stat.ino), uid: stat.uid }, + apiRevision: response.body.version.APIVersion, + version: response.body.version.Version, + osArch: response.body.version.OsArch, + hostArch: response.body.host.arch, + hostOs: response.body.host.os, + capabilities: [...capabilityDrop].sort(), + }); + return Object.freeze({ + socketPath, + hostBindingDigest: `sha256:${createHash("sha256").update(material).digest("hex")}`, + capabilityDrop: Object.freeze(capabilityDrop), + }); +} + +async function jsonRequest( + socketPath: string, + path: string, + signal?: AbortSignal, + method = "GET", + body?: unknown, + timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, +): Promise<{ readonly status: number; readonly body: unknown }> { + const response = await rawRequest( + socketPath, + path, + signal, + method, + body === undefined ? undefined : new TextEncoder().encode(JSON.stringify(body)), + body === undefined ? undefined : "application/json", + timeoutMs, + ); + return { status: response.status, body: parseJson(response.body) }; +} + +function rawRequest( + socketPath: string, + path: string, + signal?: AbortSignal, + method = "GET", + body?: Uint8Array, + contentType?: string, + timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, + maxResponseBytes = MAX_ENGINE_RESPONSE_BYTES, +): Promise { + return new Promise((resolve, reject) => { + const request = httpRequest( + { + socketPath, + path, + method, + signal, + headers: + body === undefined + ? undefined + : { + "content-length": String(body.byteLength), + ...(contentType === undefined ? {} : { "content-type": contentType }), + }, + }, + (response) => { + const chunks: Uint8Array[] = []; + let total = 0; + response.on("data", (chunk: Buffer) => { + total += chunk.byteLength; + if (total > maxResponseBytes) { + request.destroy(new ResponseBoundError()); + return; + } + chunks.push(chunk); + }); + response.on("end", () => + resolve({ + status: response.statusCode ?? 0, + body: new Uint8Array(Buffer.concat(chunks)), + }), + ); + response.on("error", reject); + }, + ); + request.setTimeout(timeoutMs, () => request.destroy(new Error("Podman request timed out."))); + request.on("error", reject); + if (body !== undefined) request.write(body); + request.end(); + }); +} + +class ResponseBoundError extends Error { + constructor() { + super("Podman response exceeded its byte bound."); + this.name = "ResponseBoundError"; + } +} + +function parseRunnerResult(bytes: Uint8Array): LocalUntrustedJsComputeRunnerResult { + const raw = demultiplex(bytes); + const value = parseJson(raw); + if (!record(value)) throw new TypeError("D667 runner returned an invalid result."); + return value as unknown as LocalUntrustedJsComputeRunnerResult; +} + +function demultiplex(bytes: Uint8Array): Uint8Array { + if (bytes.byteLength < 8 || (bytes[0] !== 1 && bytes[0] !== 2)) return bytes; + const chunks: Uint8Array[] = []; + let offset = 0; + while (offset + 8 <= bytes.byteLength) { + if ( + (bytes[offset] !== 1 && bytes[offset] !== 2) || + bytes[offset + 1] !== 0 || + bytes[offset + 2] !== 0 || + bytes[offset + 3] !== 0 + ) + throw new TypeError("D667 runner log framing is invalid."); + const length = + ((bytes[offset + 4] ?? 0) << 24) | + ((bytes[offset + 5] ?? 0) << 16) | + ((bytes[offset + 6] ?? 0) << 8) | + (bytes[offset + 7] ?? 0); + if (length < 0 || offset + 8 + length > bytes.byteLength) + throw new TypeError("D667 runner log framing is invalid."); + if (bytes[offset] === 1) chunks.push(bytes.slice(offset + 8, offset + 8 + length)); + offset += 8 + length; + } + if (offset !== bytes.byteLength) throw new TypeError("D667 runner log framing is incomplete."); + return new Uint8Array(Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)))); +} + +function parseExitCode(response: RawResponse): number | undefined { + if (response.status !== 200) return undefined; + const text = new TextDecoder().decode(response.body).trim(); + if (!/^(?:0|[1-9][0-9]*)$/.test(text)) return undefined; + const value = Number(text); + return Number.isSafeInteger(value) && value >= 0 ? value : undefined; +} + +function tarArchive( + entries: readonly { path: string; bytes: Uint8Array; mode: number }[], +): Uint8Array { + const blocks: Uint8Array[] = []; + for (const entry of entries) { + if (!/^[A-Za-z0-9][A-Za-z0-9._/-]{0,99}$/.test(entry.path) || entry.path.includes("..")) + throw new TypeError("D667 archive path is invalid."); + const header = new Uint8Array(512); + writeAscii(header, 0, 100, entry.path); + writeOctal(header, 100, 8, entry.mode); + writeOctal(header, 108, 8, 0); + writeOctal(header, 116, 8, 0); + writeOctal(header, 124, 12, entry.bytes.byteLength); + writeOctal(header, 136, 12, 0); + header.fill(0x20, 148, 156); + header[156] = "0".charCodeAt(0); + writeAscii(header, 257, 6, "ustar"); + writeAscii(header, 263, 2, "00"); + let sum = 0; + for (const byte of header) sum += byte; + writeOctal(header, 148, 8, sum); + blocks.push(header, entry.bytes); + const padding = (512 - (entry.bytes.byteLength % 512)) % 512; + if (padding > 0) blocks.push(new Uint8Array(padding)); + } + blocks.push(new Uint8Array(1024)); + return new Uint8Array(Buffer.concat(blocks.map((block) => Buffer.from(block)))); +} + +function writeAscii(target: Uint8Array, offset: number, length: number, value: string): void { + const bytes = new TextEncoder().encode(value); + if (bytes.byteLength > length) throw new TypeError("D667 tar field is too long."); + target.set(bytes, offset); +} + +function writeOctal(target: Uint8Array, offset: number, length: number, value: number): void { + const text = value.toString(8).padStart(length - 2, "0"); + writeAscii(target, offset, length, `${text}\0`); +} + +function parseJson(bytes: Uint8Array): unknown { + try { + return JSON.parse(new TextDecoder().decode(bytes)); + } catch { + return undefined; + } +} + +function record(value: unknown): value is Record { + if (value === null || typeof value !== "object" || Array.isArray(value)) return false; + const proto = Object.getPrototypeOf(value); + return proto === Object.prototype || proto === null; +} + +function exactStrings(value: unknown, expected: readonly string[]): boolean { + return ( + Array.isArray(value) && + value.length === expected.length && + value.every((entry, index) => entry === expected[index]) + ); +} + +function exactUnorderedStrings(value: unknown, expected: readonly string[]): boolean { + return ( + Array.isArray(value) && + value.every((entry) => typeof entry === "string") && + exactStrings([...value].sort(), [...expected].sort()) + ); +} + +function tmpfsMatches(value: unknown): boolean { + if (!record(value) || Object.keys(value).length !== 1) return false; + const options = value["/tmp"]; + if (typeof options !== "string") return false; + const parts = options.split(","); + const fields = new Set(parts); + if (fields.size !== parts.length) return false; + const size = [...fields].filter((field) => field.startsWith("size=")); + const mode = [...fields].filter((field) => field.startsWith("mode=")); + const allowed = new Set([ + "rw", + "nosuid", + "nodev", + "noexec", + `size=${TMPFS_LIMIT_BYTES}`, + `size=${TMPFS_LIMIT_BYTES}b`, + "mode=0700", + "mode=700", + "rprivate", + "tmpcopyup", + ]); + return ( + parts.every((field) => allowed.has(field)) && + fields.has("rw") && + fields.has("nosuid") && + fields.has("nodev") && + fields.has("noexec") && + size.length === 1 && + (fields.has(`size=${TMPFS_LIMIT_BYTES}`) || fields.has(`size=${TMPFS_LIMIT_BYTES}b`)) && + mode.length === 1 && + (fields.has("mode=0700") || fields.has("mode=700")) + ); +} + +function mountsMatch(value: unknown): boolean { + return Array.isArray(value) && value.length === 0; +} + +function rlimitsMatch(value: unknown): boolean { + if (!Array.isArray(value) || value.length !== 2) return false; + const normalized = value + .map((entry) => + record(entry) ? { name: entry.Name, hard: entry.Hard, soft: entry.Soft } : undefined, + ) + .filter( + (entry): entry is { name: unknown; hard: unknown; soft: unknown } => entry !== undefined, + ); + return ( + normalized.length === 2 && + normalized.some( + (entry) => + (entry.name === "RLIMIT_FSIZE" || entry.name === "RLIMIT_RLIMIT_FSIZE") && + entry.hard === FILE_SIZE_LIMIT_BYTES && + entry.soft === FILE_SIZE_LIMIT_BYTES, + ) && + normalized.some( + (entry) => + (entry.name === "RLIMIT_NOFILE" || entry.name === "RLIMIT_RLIMIT_NOFILE") && + entry.hard === NOFILE_LIMIT && + entry.soft === NOFILE_LIMIT, + ) + ); +} + +function remainingMs(deadline: number): number { + return Math.max(1, Math.min(DEFAULT_REQUEST_TIMEOUT_MS, deadline - Date.now())); +} + +function labelValue(value: { + readonly runId: string; + readonly attempt: number; + readonly epoch: string; + readonly manifestFingerprint: string; + readonly runAdmissionId: string; +}): string { + return `sha256:${createHash("sha256").update(JSON.stringify(value)).digest("hex")}`; +} + +function delay(ms: number, signal: AbortSignal): Promise { + return new Promise((resolve, reject) => { + const timeout = setTimeout(resolve, ms); + signal.addEventListener( + "abort", + () => { + clearTimeout(timeout); + reject(signal.reason); + }, + { once: true }, + ); + }); +} diff --git a/packages/ts/tsup.config.ts b/packages/ts/tsup.config.ts index 3b1f6ed4..6e0d3e2c 100644 --- a/packages/ts/tsup.config.ts +++ b/packages/ts/tsup.config.ts @@ -21,6 +21,8 @@ export default defineConfig({ "src/executors/local-container-postgresql-docker-engine-api-v0/node.ts", "src/executors/local-container-postgresql-podman-libpod-api-v0-rootless.ts", "src/executors/local-container-postgresql-podman-libpod-api-v0-rootless/node.ts", + "src/executors/local-untrusted-js-compute.ts", + "src/executors/local-untrusted-js-compute/node.ts", "src/executors/local-container-postgresql.ts", "src/executors/managed-cloud-postgresql.ts", "src/executors/managed-untrusted-js-compute.ts", diff --git a/scripts/check-no-raw-async.ts b/scripts/check-no-raw-async.ts index f4afda19..6ae4a7f9 100644 --- a/scripts/check-no-raw-async.ts +++ b/scripts/check-no-raw-async.ts @@ -46,6 +46,10 @@ const ALLOW_ALL = new Set([ "packages/ts/src/executors/local-container-postgresql-podman-libpod-api-v0-rootless.ts", // D645 Node-local Podman transport entry; sockets/IDs stay implementation-private. "packages/ts/src/executors/local-container-postgresql-podman-libpod-api-v0-rootless/node.ts", + // D667 independently certified local untrusted-JS driver/runtime boundary. + "packages/ts/src/executors/local-untrusted-js-compute.ts", + // D667 Node-local rootless Podman broker; sockets/IDs stay implementation-private. + "packages/ts/src/executors/local-untrusted-js-compute/node.ts", // D605 concrete managed control-store, WSS transport, and worker runtime boundary. "packages/ts/src/executors/managed-cloud-postgresql.ts", // D606 concrete customer endpoint, CAS store, outbound transport, and worker boundary. diff --git a/website/src/content/docs/integrations/matrix.md b/website/src/content/docs/integrations/matrix.md index 0e31a9a2..a1ab26a9 100644 --- a/website/src/content/docs/integrations/matrix.md +++ b/website/src/content/docs/integrations/matrix.md @@ -55,6 +55,8 @@ See [Adapters](/integrations/adapters/) for usage guidance and naming convention | Node-local Docker Engine API v0 certifier | D624 Node-only Docker Engine API certifier entry for the existing local-container PostgreSQL family; private Docker socket/resource handles stay inside the host process and caller-supplied proof adapters provide containment/network/secret evidence | `@graphrefly/ts/executors/local-container-postgresql-docker-engine-api-v0/node` | | Rootless Podman native Libpod API v0 PostgreSQL contract | D645 independent Podman backend family and app-private runtime host contract; it does not use the Docker-compatible API or expose a provider registry | `@graphrefly/ts/executors/local-container-postgresql-podman-libpod-api-v0-rootless` | | Node-local rootless Podman Libpod API v0 certifier | D645 Node-only candidate certifier with package-owned host coordinates, bounded CLI socket discovery, native Libpod requests, and private resource handles; it remains unavailable until every required live effect probe is certified | `@graphrefly/ts/executors/local-container-postgresql-podman-libpod-api-v0-rootless/node` | +| Local untrusted JavaScript compute contract | D667 host-neutral, exact-coordinate attempt contract for immutable JavaScript bundles, admitted JSON inputs, bounded GraphReFly runner results, runtime topology, provenance, cancellation, and verified cleanup | `@graphrefly/ts/executors/local-untrusted-js-compute` | +| Node-local untrusted JavaScript Podman broker | D667 Node-only rootless-Podman broker with package-owned socket discovery, fixed runner entrypoint, digest-pinned image, non-root read-only containment, no host bind or engine-socket mount, one bounded noexec tmpfs, no network, bounded resources, and private resource handles | `@graphrefly/ts/executors/local-untrusted-js-compute/node` | | Managed-cloud PostgreSQL binding | PostgreSQL-16 atomic control-store and worker-initiated WSS session lifecycle with exact fenced leases, cancellation, and settlement | `@graphrefly/ts/executors/managed-cloud-postgresql` | | Managed untrusted JS compute | E2B Cloud v0 untrusted JavaScript compute lifecycle with deny-all network, bounded movement evidence, exact cancellation, and independently visible cleanup | `@graphrefly/ts/executors/managed-untrusted-js-compute` | | Customer-hosted PostgreSQL binding | Signed digest-pinned endpoint agent, outbound authenticated WSS, customer-resident credentials, exact cross-domain fences, and encrypted evidence-only offline outbox | `@graphrefly/ts/executors/customer-hosted-postgresql` | From 12599f2be26f61d9903c4577e8ab507a000b531d Mon Sep 17 00:00:00 2001 From: David Chen Date: Wed, 29 Jul 2026 11:07:56 -0700 Subject: [PATCH 2/5] feat(ts): certify graph-owned local JS runner --- .changeset/quiet-local-runner.md | 5 + biome.json | 1 + package.json | 2 + packages/ts/package.json | 4 +- .../runners/local-untrusted-js/Containerfile | 6 + .../local-untrusted-js-runner.mjs | 4316 +++++++++++++++++ .../ts/runners/local-untrusted-js/runner.ts | 554 +++ .../runners/local-untrusted-js/tsconfig.json | 10 + ...cal-untrusted-js-compute-node.live.test.ts | 187 + .../local-untrusted-js-compute-node.test.ts | 53 +- .../local-untrusted-js-compute.test.ts | 37 +- packages/ts/src/__tests__/subpaths.test.ts | 7 + .../executors/local-untrusted-js-compute.ts | 24 +- .../local-untrusted-js-compute/node.ts | 366 +- .../build-local-untrusted-js-runner-image.mjs | 36 + scripts/build-local-untrusted-js-runner.mjs | 31 + .../src/content/docs/integrations/matrix.md | 2 +- 17 files changed, 5606 insertions(+), 35 deletions(-) create mode 100644 .changeset/quiet-local-runner.md create mode 100644 packages/ts/runners/local-untrusted-js/Containerfile create mode 100644 packages/ts/runners/local-untrusted-js/local-untrusted-js-runner.mjs create mode 100644 packages/ts/runners/local-untrusted-js/runner.ts create mode 100644 packages/ts/runners/local-untrusted-js/tsconfig.json create mode 100644 scripts/build-local-untrusted-js-runner-image.mjs create mode 100644 scripts/build-local-untrusted-js-runner.mjs diff --git a/.changeset/quiet-local-runner.md b/.changeset/quiet-local-runner.md new file mode 100644 index 00000000..4a565bd6 --- /dev/null +++ b/.changeset/quiet-local-runner.md @@ -0,0 +1,5 @@ +--- +"@graphrefly/ts": minor +--- + +Add the fixed distroless D667 local untrusted-JavaScript runner artifact and independent rootless-Podman live certifier. diff --git a/biome.json b/biome.json index 43bde7d9..fc81b5b9 100644 --- a/biome.json +++ b/biome.json @@ -22,6 +22,7 @@ "!archive/packages", "!archive/website-prototypes", "!benchmarks", + "!packages/ts/runners/local-untrusted-js/local-untrusted-js-runner.mjs", "!website", "!examples/nestjs" ] diff --git a/package.json b/package.json index 507b3f91..524d5a30 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,8 @@ "dashboard": "node ../graphrefly/dashboard/build.mjs", "dashboard:check": "node ../graphrefly/dashboard/build.mjs --check", "build": "pnpm --filter @graphrefly/ts build", + "build:local-untrusted-js-runner": "node scripts/build-local-untrusted-js-runner.mjs", + "build:local-untrusted-js-runner-image": "node scripts/build-local-untrusted-js-runner-image.mjs", "test": "pnpm --filter @graphrefly/ts test", "test:ts": "pnpm --filter @graphrefly/ts test", "probe:b49:ts": "tsx packages/ts/src/__bench__/b49-probe.ts", diff --git a/packages/ts/package.json b/packages/ts/package.json index 7d9bd95c..6570628c 100644 --- a/packages/ts/package.json +++ b/packages/ts/package.json @@ -799,13 +799,15 @@ }, "files": [ "dist", + "runners/local-untrusted-js/Containerfile", + "runners/local-untrusted-js/local-untrusted-js-runner.mjs", "LICENSE" ], "publishConfig": { "access": "public" }, "scripts": { - "build": "tsup && node ../../scripts/check-ts-package-exports.mjs", + "build": "node ../../scripts/build-local-untrusted-js-runner.mjs && tsup && node ../../scripts/check-ts-package-exports.mjs", "test": "vitest run", "test:typecheck": "tsc --noEmit -p tsconfig.tests.json", "test:browser:agentic-memory": "node ../../scripts/run-agentic-memory-indexeddb-smoke.mjs", diff --git a/packages/ts/runners/local-untrusted-js/Containerfile b/packages/ts/runners/local-untrusted-js/Containerfile new file mode 100644 index 00000000..5b99d52d --- /dev/null +++ b/packages/ts/runners/local-untrusted-js/Containerfile @@ -0,0 +1,6 @@ +FROM gcr.io/distroless/nodejs22-debian12@sha256:13593b7570658e8477de39e2f4a1dd25db2f836d68a0ba771251572d23bb4f8e + +COPY --chown=65532:65532 local-untrusted-js-runner.mjs /opt/graphrefly/local-untrusted-js-runner.mjs + +USER 65532:65532 +ENTRYPOINT ["/nodejs/bin/node", "--experimental-vm-modules", "--no-addons", "--disable-proto=throw", "/opt/graphrefly/local-untrusted-js-runner.mjs"] diff --git a/packages/ts/runners/local-untrusted-js/local-untrusted-js-runner.mjs b/packages/ts/runners/local-untrusted-js/local-untrusted-js-runner.mjs new file mode 100644 index 00000000..639b61a6 --- /dev/null +++ b/packages/ts/runners/local-untrusted-js/local-untrusted-js-runner.mjs @@ -0,0 +1,4316 @@ +// runners/local-untrusted-js/runner.ts +import { createHash } from "node:crypto"; +import { readFile } from "node:fs/promises"; +import { createContext, Script, SourceTextModule } from "node:vm"; + +// src/batch/boundary.ts +var depth = 0; +var pendingCores = []; +var pendingHead = 0; +function enterWave() { + depth++; +} +function exitWave() { + depth--; + if (depth === 0 && pendingHead < pendingCores.length) drain(); +} +function deferRewire(core, apply, options = {}) { + core.enqueueBoundaryTask({ apply, batchToken: options.batchToken, isReady: options.isReady }); + pendingCores.push(core); +} +function scheduleBoundaryDrain(core) { + for (let i = 0; i < core.boundaryTaskCount(); i++) pendingCores.push(core); + if (depth === 0 && pendingHead < pendingCores.length) drain(); +} +function dropBoundaryTasksForBatch(batchToken) { + const seen = /* @__PURE__ */ new Set(); + for (let i = pendingHead; i < pendingCores.length; i++) { + const core = pendingCores[i]; + if (seen.has(core)) continue; + seen.add(core); + core.dropBoundaryTasksForBatch(batchToken); + } +} +function drain() { + let escaped = null; + while (pendingHead < pendingCores.length) { + const core = pendingCores[pendingHead++]; + const task = core.shiftBoundaryTask(); + if (task === void 0) continue; + if (task.batchToken !== void 0) { + const committed = task.batchToken.committed === true; + if (!committed) continue; + } + if (task.isReady !== void 0 && !task.isReady()) { + core.unshiftBoundaryTask(task); + continue; + } + depth++; + try { + task.apply(); + } catch (e) { + if (escaped === null) escaped = { e }; + } finally { + depth--; + } + } + pendingCores.length = 0; + pendingHead = 0; + if (escaped !== null) throw escaped.e; +} + +// src/batch/batch.ts +var active = null; +var boundaryOwner = null; +function currentBatch() { + return active !== null; +} +function currentBoundaryBatchToken() { + return active ?? boundaryOwner ?? void 0; +} +function deferToBatch(target, tier3Wave) { + if (active === null) return false; + if (!active.deferred.has(target)) active.order.push(target); + active.deferred.set(target, tier3Wave); + return true; +} +function deferAfterBatchForTarget(target, fn) { + if (active === null || !active.deferred.has(target)) return false; + const owner = active; + target.__deferBoundary(() => { + if (owner.committed) fn(); + }, owner); + return true; +} +function commit(b) { + const prev = boundaryOwner; + boundaryOwner = b; + try { + for (const target of b.order) { + const wave = b.deferred.get(target); + if (wave) target.__commitBatchedWave(wave); + } + b.committed = true; + } catch (e) { + dropBoundaryTasksForBatch(b); + throw e; + } finally { + boundaryOwner = prev; + } +} +function rollback(b) { + dropBoundaryTasksForBatch(b); + for (const target of b.order) target.__rollbackBatched(); +} +function batch(fn) { + enterWave(); + try { + if (active !== null) { + const outer = active; + return fn({ + rollback: () => { + outer.rolledBack = true; + } + }); + } + const b = { order: [], deferred: /* @__PURE__ */ new Map(), committed: false, rolledBack: false }; + active = b; + const bctx = { + rollback: () => { + b.rolledBack = true; + } + }; + let result; + try { + result = fn(bctx); + } catch (e) { + active = null; + rollback(b); + throw e; + } + active = null; + if (b.rolledBack) rollback(b); + else { + commit(b); + } + return result; + } finally { + exitWave(); + } +} + +// src/protocol/messages.ts +var SENTINEL = void 0; +function isInvalidErrorPayload(v) { + return v === SENTINEL || typeof v === "boolean"; +} +function errorPayload(reason, fallback = "error without a valid payload") { + return isInvalidErrorPayload(reason) ? new Error(fallback) : reason; +} +var TIER_START = 0; +var TIER_CONTROL = 1; +var TIER_NOTIFICATION = 2; +var TIER_VALUE = 3; +var TIER_SETTLE = 4; +var TIER_TERMINAL = 5; +var TIER_TEARDOWN = 6; +var TIER = { + START: TIER_START, + PAUSE: TIER_CONTROL, + RESUME: TIER_CONTROL, + PULL: TIER_CONTROL, + DIRTY: TIER_NOTIFICATION, + DATA: TIER_VALUE, + RESOLVED: TIER_VALUE, + INVALIDATE: TIER_SETTLE, + COMPLETE: TIER_TERMINAL, + ERROR: TIER_TERMINAL, + TEARDOWN: TIER_TEARDOWN +}; +function messageTier(t) { + return TIER[t]; +} +function isDeferredTier(t) { + return TIER[t] >= TIER_VALUE; +} +function isValueTier(t) { + return TIER[t] === TIER_VALUE; +} +function isPauseBufferedTier(t) { + const tier = TIER[t]; + return tier === TIER_VALUE || tier === TIER_SETTLE; +} +function isTerminal(t) { + return TIER[t] === TIER_TERMINAL; +} +function isUpAllowed(t) { + const tier = TIER[t]; + return tier !== void 0 && tier !== TIER_START && tier !== TIER_VALUE && tier !== TIER_TERMINAL; +} + +// src/ctx/types.ts +var CTX_DEP_CACHE = /* @__PURE__ */ Symbol.for("graphrefly.ctx.depCache"); +var ctxDepWaveOrigins = /* @__PURE__ */ new WeakMap(); +function setCtxDepWaveOrigin(ctx, origin) { + ctxDepWaveOrigins.set(ctx, origin); +} +var CTX_NODE_BINDING = /* @__PURE__ */ Symbol("graphrefly.ctx.nodeBinding"); +function depCount(ctx) { + return ctx.waveData.length; +} +function depLatest(ctx, depIndex) { + return ctx[CTX_DEP_CACHE]?.latest[depIndex]; +} + +// src/dispatcher/index.ts +var PoolTable = class { + constructor(kind) { + this.kind = kind; + } + kind; + fns = []; + free = []; + register(fn) { + const reused = this.free.pop(); + if (reused !== void 0) { + this.fns[reused] = fn; + return reused; + } + const id = this.fns.length; + this.fns.push(fn); + return id; + } + unregister(handleId) { + if (this.fns[handleId] === void 0) return; + this.fns[handleId] = void 0; + this.free.push(handleId); + } + invoke(handleId, ctx) { + this.fns[handleId](ctx); + } +}; +var dispatcherHandleStatKey = (h) => JSON.stringify([String(h.poolId), String(h.handleId)]); +var Dispatcher = class { + pools = []; + syncPoolId; + asyncPoolId; + // opt-in profile recorder (default OFF → zero overhead, F-PERF). + _recording = false; + _stats = /* @__PURE__ */ new Map(); + _totalInvokes = 0; + constructor() { + this.syncPoolId = this.addPool(new PoolTable("sync")); + this.asyncPoolId = this.addPool(new PoolTable("async")); + } + /** Turn the profile recorder on/off (D39). Off = zero overhead on invoke. */ + setRecording(on) { + this._recording = on; + } + /** Reset accumulated profiling counters. */ + clearStats() { + this._stats.clear(); + this._totalInvokes = 0; + } + /** Read a handle's accumulated counters (undefined if it never ran while recording). */ + statFor(handle) { + return this._stats.get(dispatcherHandleStatKey(handle)); + } + /** Total fn invocations recorded across the dispatcher. */ + get totalInvokes() { + return this._totalInvokes; + } + addPool(pool) { + const id = this.pools.length; + this.pools.push(pool); + return id; + } + /** Register a fn in a pool, returning its Handle. Default pool = sync (R-sync-core). */ + register(fn, pool = "sync") { + const poolId = pool === "sync" ? this.syncPoolId : pool === "async" ? this.asyncPoolId : pool; + const handleId = this.pools[poolId].register(fn); + return { poolId, handleId }; + } + /** + * Release a handle (B15): frees the pool slot (closure GC'd, id reusable) and drops any + * accumulated profile stat so a reused id never inherits the previous tenant's counters. + * Called on rewire fn-swap (node._rewire) — the old handle is dropped before the node + * adopts the new one. Idempotent. NOT called on deactivate (a node's handle survives + * activate↔deactivate and is reused on reactivation; only a rewire swaps it). + */ + unregister(handle) { + this.pools[handle.poolId].unregister(handle.handleId); + this._stats.delete(dispatcherHandleStatKey(handle)); + } + /** Uniform sync-void invoke (R-sync-core / R-dispatch-all). */ + invoke(handle, ctx) { + if (!this._recording) { + this.pools[handle.poolId].invoke(handle.handleId, ctx); + return; + } + this._totalInvokes++; + const t0 = performance.now(); + try { + this.pools[handle.poolId].invoke(handle.handleId, ctx); + } finally { + const dur = (performance.now() - t0) * 1e6; + const key = dispatcherHandleStatKey(handle); + const s = this._stats.get(key) ?? { + invokes: 0, + totalDurationNs: 0, + lastDurationNs: 0 + }; + s.invokes++; + s.lastDurationNs = dur; + s.totalDurationNs += dur; + this._stats.set(key, s); + } + } + poolKind(poolId) { + return this.pools[poolId].kind; + } +}; +var defaultDispatcher = new Dispatcher(); + +// src/node/core.ts +var NodeCore = class { + nextId = 0; + slots = []; + values = []; + waves = []; + controls = []; + lifecycles = []; + depStates = []; + privateStates = []; + hooks = []; + syncCtxs = []; + versionStates = []; + boundary = { queue: [], head: 0 }; + createSlot(slot, state) { + const id = this.nextId++; + const full = { ...slot, id }; + this.slots[id] = full; + this.depStates[id] = state.dep; + this.lifecycles[id] = state.lifecycle; + this.values[id] = state.value; + this.waves[id] = state.wave; + this.controls[id] = state.control; + this.privateStates[id] = state.privateState; + this.hooks[id] = state.hooks; + this.syncCtxs[id] = state.syncCtx; + this.versionStates[id] = state.version; + return { id, slot: full }; + } + get(id) { + const slot = this.slots[id]; + if (slot === void 0) throw new Error("NodeCore: unknown node slot"); + return slot; + } + getValue(id) { + const value = this.values[id]; + if (value === void 0) throw new Error("NodeCore: unknown node value state"); + return value; + } + getWave(id) { + const wave = this.waves[id]; + if (wave === void 0) throw new Error("NodeCore: unknown node wave state"); + return wave; + } + getControl(id) { + const control = this.controls[id]; + if (control === void 0) throw new Error("NodeCore: unknown node control state"); + return control; + } + getLifecycle(id) { + const lifecycle = this.lifecycles[id]; + if (lifecycle === void 0) throw new Error("NodeCore: unknown node lifecycle state"); + return lifecycle; + } + getDep(id) { + const dep = this.depStates[id]; + if (dep === void 0) throw new Error("NodeCore: unknown node dep state"); + return dep; + } + getPrivateState(id) { + const state = this.privateStates[id]; + if (state === void 0) throw new Error("NodeCore: unknown node private state"); + return state; + } + getHooks(id) { + const hooks = this.hooks[id]; + if (hooks === void 0) throw new Error("NodeCore: unknown node cleanup hooks"); + return hooks; + } + getSyncCtx(id) { + const state = this.syncCtxs[id]; + if (state === void 0) throw new Error("NodeCore: unknown node ctx state"); + return state; + } + getVersion(id) { + const state = this.versionStates[id]; + if (state === void 0) throw new Error("NodeCore: unknown node version state"); + return state; + } + /** @internal D122: release graph-owned ephemeral node runtime state from core retention. */ + releaseSlot(id) { + this.slots[id] = void 0; + this.depStates[id] = void 0; + this.lifecycles[id] = void 0; + this.values[id] = void 0; + this.waves[id] = void 0; + this.controls[id] = void 0; + this.privateStates[id] = void 0; + this.hooks[id] = void 0; + this.syncCtxs[id] = void 0; + this.versionStates[id] = void 0; + } + /** @internal B49: graph-local deferred-boundary queue (rewireNext/upNext/batch-after-commit). */ + enqueueBoundaryTask(task) { + this.boundary.queue.push(task); + } + /** @internal */ + hasBoundaryTasks() { + return this.boundary.head < this.boundary.queue.length; + } + /** @internal */ + boundaryTaskCount() { + return this.boundary.queue.length - this.boundary.head; + } + /** @internal */ + shiftBoundaryTask() { + if (!this.hasBoundaryTasks()) { + this.boundary.queue = []; + this.boundary.head = 0; + return void 0; + } + const task = this.boundary.queue[this.boundary.head++]; + if (!this.hasBoundaryTasks()) { + this.boundary.queue = []; + this.boundary.head = 0; + } + return task; + } + /** @internal Put a not-yet-ready task back at this core's FIFO head. */ + unshiftBoundaryTask(task) { + const remaining = this.boundary.queue.slice(this.boundary.head); + this.boundary.queue = [task, ...remaining]; + this.boundary.head = 0; + } + /** @internal D110: discard all pending tasks caused by an uncommitted batch. */ + dropBoundaryTasksForBatch(batchToken) { + const remaining = this.boundary.queue.slice(this.boundary.head).filter((task) => task.batchToken !== batchToken); + this.boundary.queue = remaining; + this.boundary.head = 0; + } +}; +function makeDepBookkeeping(depCount2) { + return { + batch: new Array(depCount2).fill(null), + waveData: Array.from({ length: depCount2 }, () => []), + waveTokens: new Array(depCount2).fill(void 0), + waveLive: Array.from({ length: depCount2 }, () => []), + prev: new Array(depCount2).fill(SENTINEL), + hasData: new Array(depCount2).fill(false), + dirty: new Array(depCount2).fill(false), + tier: new Array(depCount2).fill(0), + terminal: new Array(depCount2).fill(void 0), + terminalInput: new Array(depCount2).fill(void 0), + unsubs: [], + idxBoxes: [] + }; +} + +// src/graph/environment.ts +var EnvironmentDrivers = class _EnvironmentDrivers { + process; + http; + sse; + websocket; + webhook; + constructor(init = {}) { + this.process = init.process; + this.http = init.http; + this.sse = init.sse; + this.websocket = init.websocket; + this.webhook = init.webhook; + Object.freeze(this); + } + static empty() { + return EMPTY_ENVIRONMENT; + } + withProcess(driver) { + return new _EnvironmentDrivers({ ...this, process: driver }); + } + withHttp(driver) { + return new _EnvironmentDrivers({ ...this, http: driver }); + } + withSse(driver) { + return new _EnvironmentDrivers({ ...this, sse: driver }); + } + withWebSocket(driver) { + return new _EnvironmentDrivers({ ...this, websocket: driver }); + } + withWebhook(driver) { + return new _EnvironmentDrivers({ ...this, webhook: driver }); + } + processDriver() { + return this.process; + } + httpDriver() { + return this.http; + } + sseDriver() { + return this.sse; + } + webSocketDriver() { + return this.websocket; + } + webhookDriver() { + return this.webhook; + } +}; +var EMPTY_ENVIRONMENT = new EnvironmentDrivers(); + +// src/node/protocol-guards.ts +function terminalView(t) { + return t === void 0 ? false : t; +} +function normalizePullDemand(demand) { + if (typeof demand !== "object" || demand === null || Array.isArray(demand)) { + throw new Error("ctx.up: PULL requires { pullId, params? } demand payload (D269)"); + } + const pullId = demand.pullId; + if (typeof pullId !== "string" && typeof pullId !== "symbol") { + throw new Error("ctx.up: PULL demand requires a string or symbol pullId (D269)"); + } + const params = demand.params; + return params === void 0 ? { pullId } : { pullId, params }; +} +function validateDownPayloads(msgs) { + for (const m of msgs) { + if (messageTier(m[0]) === void 0) { + throw new Error( + `down: ${String(m[0])} is not in the closed message-type set (R-msg-closed-set)` + ); + } + if (m[0] === "DATA" && m[1] === void 0) { + throw new Error("down: DATA requires a non-SENTINEL payload (R-data-payload)"); + } + if (m[0] === "ERROR" && isInvalidErrorPayload(m[1])) { + throw new Error("down: ERROR requires a non-SENTINEL, non-boolean payload (R-data-payload)"); + } + } +} + +// src/node/runtime-accessors.ts +var constructingCore; +var constructingEnvironment; +var ownerTokens = /* @__PURE__ */ new WeakMap(); +var topologyDepsChangedObservers = /* @__PURE__ */ new WeakMap(); +var checkpointReaders = /* @__PURE__ */ new WeakMap(); +var restoreWriters = /* @__PURE__ */ new WeakMap(); +var runtimeReleasers = /* @__PURE__ */ new WeakMap(); +var runtimeQuiescenceReaders = /* @__PURE__ */ new WeakMap(); +var subscriberCountReaders = /* @__PURE__ */ new WeakMap(); +var activationReaders = /* @__PURE__ */ new WeakMap(); +var releasedNodes = /* @__PURE__ */ new WeakSet(); +function withNodeCore(core, create) { + const prev = constructingCore; + constructingCore = core; + try { + return create(); + } finally { + constructingCore = prev; + } +} +function takeConstructingNodeCore() { + const core = constructingCore; + constructingCore = void 0; + return core; +} +function withEnvironmentDrivers(environment, create) { + const prev = constructingEnvironment; + constructingEnvironment = environment; + try { + return create(); + } finally { + constructingEnvironment = prev; + } +} +function takeConstructingEnvironmentDrivers() { + const environment = constructingEnvironment; + constructingEnvironment = void 0; + return environment; +} +function getNodeOwner(n) { + return ownerTokens.get(n); +} +function setNodeOwner(n, owner) { + ownerTokens.set(n, owner); +} +function setNodeTopologyDepsChangedObserver(n, observer) { + topologyDepsChangedObservers.set(n, observer); +} +function notifyTopologyDepsChanged(node, prevDeps, deps) { + topologyDepsChangedObservers.get(node)?.(node, prevDeps, deps); +} +function checkpointStateOfNode(n) { + const read = checkpointReaders.get(n); + if (read === void 0) throw new Error("checkpoint: unknown node state"); + return read(); +} +function releaseRuntimeOfNode(n) { + runtimeReleasers.get(n)?.(); +} +function isNodeRuntimeQuiescentForRelease(n) { + return runtimeQuiescenceReaders.get(n)?.() ?? false; +} +function subscriberCountOfNode(n) { + return subscriberCountReaders.get(n)?.() ?? 0; +} +function isNodeActiveForRelease(n) { + return activationReaders.get(n)?.() ?? false; +} +function isNodeRuntimeReleased(n) { + return releasedNodes.has(n); +} + +// src/node/node-context-runtime.ts +function nodeBuildCtx(self) { + const kind = self._slot.handle ? self._slot.dispatcher.poolKind(self._slot.handle.poolId) : "sync"; + if (kind === "sync") { + if (self._syncCtx === null) self._syncCtx = self._makeCtx(); + self._refreshCtx(self._syncCtx); + return self._syncCtx; + } + return self._makeCtx({ + waveData: self._dep.waveData.map((waves) => waves.map((w) => [...w])), + waveLive: self._dep.waveLive.map((waves) => [...waves]), + terminal: self._dep.terminalInput.map(terminalView), + latest: [...self._dep.prev] + }); +} +function nodeMakeCtx(self, snapshot) { + const ctx = { + // Wave-owner boundary (D47): a SYNC fn's emit nests under the public entry that drove + // it (cheap inc/dec, no early drain); an ASYNC-pool fn re-enters here from its stashed + // ctx at depth 0, so this is the boundary that drains any rewireNext it issued. + up: (msgs, towardDep) => { + if (self._released) return; + enterWave(); + try { + self._up(msgs, towardDep); + } finally { + exitWave(); + } + }, + down: (msgs) => { + if (self._released) return; + enterWave(); + try { + self._down(msgs); + } finally { + exitWave(); + } + }, + waveData: snapshot?.waveData ?? self._dep.waveData, + terminal: snapshot?.terminal ?? self._dep.terminalInput.map(terminalView), + state: self._makeState(), + onDeactivation: (fn) => { + if (self._released) return; + self._hooks.onDeactivation.push(fn); + }, + onInvalidate: (fn) => { + if (self._released) return; + self._hooks.onInvalidate.push(fn); + }, + environment: () => self._slot.environment, + // R-rewire-deferred (D47): defer a self-dep-set mutation to the committed boundary. + rewireNext: { + subscribeDep: (dep, fn) => self._requestRewireNext({ kind: "add", dep, fn }), + unsubscribeDep: (dep, fn) => self._requestRewireNext({ kind: "remove", dep, fn }), + replaceDeps: (deps, fn) => self._requestRewireNext({ kind: "set", deps, fn }) + }, + // R-up-routing / R-pull (D269): deferred up — route a control/demand wave (e.g. PULL) + // up the declared cone at the committed boundary. The SELF-demand path: an + // immediate ctx.up whose delivery loops back re-enters this fn (D37 / R-reentrancy). + upNext: (msgs, towardDep) => self._requestUpNext(msgs, towardDep), + ...self._control.activePull === void 0 ? {} : { pull: self._control.activePull }, + [CTX_DEP_CACHE]: { latest: snapshot?.latest ?? self._dep.prev }, + [CTX_NODE_BINDING]: { + dispatcher: self._slot.dispatcher, + create: (factory) => withEnvironmentDrivers(self._slot.environment, () => withNodeCore(self._core, factory)) + } + }; + setCtxDepWaveOrigin(ctx, { live: snapshot?.waveLive ?? self._dep.waveLive }); + if (self._slot.dynamic) { + ctx.track = (i) => ctx[CTX_DEP_CACHE]?.latest[i]; + } + return ctx; +} +function nodeRefreshCtx(self, ctx) { + ctx.waveData = self._dep.waveData; + ctx.terminal = self._dep.terminalInput.map(terminalView); + if (self._control.activePull === void 0) { + delete ctx.pull; + } else { + ctx.pull = self._control.activePull; + } + ctx[CTX_DEP_CACHE] = { latest: self._dep.prev }; + setCtxDepWaveOrigin(ctx, { live: self._dep.waveLive }); +} +function nodeMakeState(self) { + return { + get: () => self._privateState.value, + set: (v) => { + self._privateState.value = v; + }, + persist: (on = true) => { + self._privateState.persist = on; + } + }; +} + +// src/node/node-input-runtime.ts +function nodeRecordDepProjection(self, idx, delivery) { + const token = delivery?.wave ?? {}; + if (self._dep.waveTokens[idx] !== token) { + self._dep.waveData[idx].push([]); + self._dep.waveLive[idx].push(delivery !== void 0); + self._dep.waveTokens[idx] = token; + } + return self._dep.waveData[idx][self._dep.waveData[idx].length - 1]; +} +function nodeDepProjectionHasData(self, idx) { + const projection = self._dep.waveData[idx][self._dep.waveData[idx].length - 1]; + return projection?.some((v) => v !== SENTINEL) ?? false; +} +function nodeReceiveFromDep(self, idx, msg, delivery) { + if (self._released) return; + const t = msg[0]; + if (t === "START") return; + const isLastInDeliveredWave = delivery?.last ?? true; + if (self._value.terminal !== void 0) { + if (t === "TEARDOWN") self._down([["TEARDOWN"]]); + return; + } + if (t === "INVALIDATE") { + const projection = self._recordDepProjection(idx, delivery); + projection.push(SENTINEL); + if (projection.some((v) => v !== SENTINEL) && isLastInDeliveredWave) self._maybeRun(); + self._dep.prev[idx] = SENTINEL; + self._dep.hasData[idx] = false; + self._dep.batch[idx] = null; + if (self._dep.dirty[idx]) { + self._dep.dirty[idx] = false; + self._wave.pending--; + } + if (self._control.pausedDepWaveOccurred && self._dep.batch.every((b) => b === null)) { + self._control.pausedDepWaveOccurred = false; + } + const hadData = self._value.hasData; + self._invalidate(); + if (self._wave.pending === 0 && self._wave.emittedDirtyThisWave) { + if (!hadData) self._down([["RESOLVED"]]); + else self._wave.emittedDirtyThisWave = false; + } + self._fireOwedDemandIfReady(); + return; + } + if (isTerminal(t)) { + const isError = t === "ERROR"; + const errPayload = isError ? msg[1] : void 0; + self._dep.terminal[idx] = isError ? errPayload : true; + self._dep.terminalInput[idx] = isError ? errPayload : true; + self._releaseDepDirty(idx); + const ranValueBeforeTerminal = self._depProjectionHasData(idx) && isLastInDeliveredWave; + if (ranValueBeforeTerminal) self._maybeRun(); + if (isError && self._slot.errorWhenDepsError) { + self._down([["ERROR", errPayload]]); + } else if (self._slot.terminalAsRealInput) { + if (ranValueBeforeTerminal) { + self._fireOwedDemandIfReady(); + return; + } + self._maybeRun(); + } else if (self._slot.completeWhenDepsComplete && self._allDepsTerminal()) { + self._down([["COMPLETE"]]); + } else { + self._settleAfterAbsorbedTerminal(); + } + self._fireOwedDemandIfReady(); + return; + } + if (t === "TEARDOWN") { + self._down([["TEARDOWN"]]); + return; + } + if (t === "DIRTY") { + if (!self._dep.dirty[idx]) { + self._dep.dirty[idx] = true; + self._wave.pending++; + self._dep.tier[idx] = 2; + self._markDirty(); + } + return; + } + if (t === "DATA") { + const v = msg[1]; + self._recordDepProjection(idx, delivery).push(v); + const b = self._dep.batch[idx]; + if (b === null) self._dep.batch[idx] = [v]; + else b.push(v); + self._dep.prev[idx] = v; + self._dep.hasData[idx] = true; + self._dep.tier[idx] = 3; + if (self._dep.dirty[idx]) { + self._dep.dirty[idx] = false; + self._wave.pending--; + } + if (isLastInDeliveredWave) self._maybeRun(); + self._fireOwedDemandIfReady(); + return; + } + if (t === "RESOLVED") { + self._recordDepProjection(idx, delivery); + self._dep.tier[idx] = 3; + if (self._dep.dirty[idx]) { + self._dep.dirty[idx] = false; + self._wave.pending--; + } + if (isLastInDeliveredWave) self._maybeRun(); + self._fireOwedDemandIfReady(); + return; + } +} +function nodeReleaseDepDirty(self, idx) { + if (self._dep.dirty[idx]) { + self._dep.dirty[idx] = false; + self._wave.pending--; + } +} +function nodeSettleAfterAbsorbedTerminal(self) { + if (self._wave.pending !== 0 || !self._wave.emittedDirtyThisWave) return; + const sawData = self._dep.batch.some((b) => b !== null && b.length > 0); + if (sawData) self._maybeRun(); + if (self._wave.emittedDirtyThisWave) self._down([["RESOLVED"]]); +} +function nodeMarkDirty(self) { + self._value.status = "dirty"; + if (self._isPullQuiet()) return; + if (!self._wave.emittedDirtyThisWave) { + self._wave.emittedDirtyThisWave = true; + self._emitToSubs(["DIRTY"]); + } +} +function nodeMaybeRun(self) { + if (self._wave.inDepMutation) { + self._wave.rewireRunPending = true; + return; + } + if (self._slot.pausable === true && (self._isPaused() || self._isPullQuiet())) { + self._control.pausedDepWaveOccurred = true; + return; + } + self._tryRun(); +} +function nodeSettleRewire(self) { + if (self._slot.pausable === true && self._isPaused()) { + self._control.pausedDepWaveOccurred = true; + return; + } + if (self._wave.pending > 0) return; + if (self._slot.handle === null) { + self._passthroughEmit(); + return; + } + if (!self._wave.hasCalledFnOnce && !(self._slot.partial || self._allDepsSettled())) return; + self._markDirty(); + self._runWave(); +} +function nodeTryRun(self) { + if (self._wave.pending > 0) return; + if (self._slot.handle === null) { + self._passthroughEmit(); + return; + } + if (!self._wave.hasCalledFnOnce) { + if (self._slot.partial || self._allDepsSettled()) self._runWave(); + return; + } + self._runWave(); +} +function nodeAllDepsSettled(self) { + for (let i = 0; i < self._slot.deps.length; i++) { + if (self._dep.hasData[i]) continue; + if (self._slot.terminalAsRealInput && self._dep.terminal[i] !== void 0) continue; + return false; + } + return true; +} +function nodePassthroughEmit(self) { + const b = self._dep.batch[0]; + if (b !== null && b.length > 0) { + self._down([["DATA", b[b.length - 1]]]); + } else if (self._wave.emittedDirtyThisWave) { + self._down([["RESOLVED"]]); + } + self._dep.batch[0] = null; + self._wave.emittedDirtyThisWave = false; +} +function nodeRunWave(self) { + if (self._wave.insideRunWave) + throw new Error( + "synchronous feedback cycle: node fn re-entered its own wave (R-reentrancy / D37)" + ); + self._wave.hasCalledFnOnce = true; + self._hooks.onInvalidate = []; + self._hooks.onDeactivation = []; + const ctx = self._buildCtx(); + const wasDirty = self._wave.emittedDirtyThisWave; + self._wave.emittedSettleThisWave = false; + self._wave.insideRunWave = true; + try { + self._slot.dispatcher.invoke(self._slot.handle, ctx); + } finally { + self._wave.insideRunWave = false; + } + if (wasDirty && !self._wave.emittedSettleThisWave && self._value.terminal === void 0 && !self._isAsyncPool()) { + self._down([["RESOLVED"]]); + } + for (let i = 0; i < self._dep.batch.length; i++) { + self._dep.batch[i] = null; + self._dep.waveData[i] = []; + self._dep.waveTokens[i] = void 0; + self._dep.waveLive[i] = []; + self._dep.terminalInput[i] = void 0; + } + self._wave.emittedDirtyThisWave = false; +} + +// src/node/node-runtime-host.ts +function nodeRuntimeHost(node) { + return node; +} + +// src/node/node-lifecycle-runtime.ts +function nodeActivate(self) { + self._lifecycle.activated = true; + const seedRestoredDeps = self._restoredActivationPending; + self._restoredActivationPending = false; + self._dep.unsubs = new Array(self._slot.deps.length); + self._dep.idxBoxes = new Array(self._slot.deps.length); + for (const dep of self._slot.deps) self._subscribeDepAt(dep, { seedRestored: seedRestoredDeps }); + if (self._slot.deps.length === 0 && self._slot.handle !== null && !self._wave.hasCalledFnOnce) { + self._runWave(); + } +} +function nodeSubscribeDepAt(self, depNode, opts = {}) { + const idx0 = self._slot.deps.indexOf(depNode); + const box = { v: idx0 }; + let ignoreInitialPush = opts.seedRestored === true; + if (ignoreInitialPush && idx0 !== -1) { + self._seedRestoredDepAt(idx0, depNode); + const dep = nodeRuntimeHost(depNode); + if (dep._value.terminal !== void 0 && !dep._slot.resubscribable) { + self._dep.unsubs[idx0] = () => { + }; + self._dep.idxBoxes[idx0] = box; + return; + } + } + const unsub = depNode.subscribe((msg, delivery) => { + if (ignoreInitialPush && delivery === void 0) return; + if (ignoreInitialPush) ignoreInitialPush = false; + if (box.v === -1) return; + self._receiveFromDep(box.v, msg, delivery); + }); + if (ignoreInitialPush && idx0 !== -1 && box.v !== -1) self._seedRestoredDepAt(idx0, depNode); + ignoreInitialPush = false; + if (idx0 !== -1) { + self._dep.unsubs[idx0] = unsub; + self._dep.idxBoxes[idx0] = box; + } +} +function nodeSeedRestoredDepAt(self, idx, depNode) { + const dep = nodeRuntimeHost(depNode); + const seedData = dep._value.hasData && !dep._slot.pull; + self._dep.batch[idx] = null; + self._dep.waveData[idx] = []; + self._dep.waveTokens[idx] = void 0; + self._dep.waveLive[idx] = []; + self._dep.prev[idx] = seedData ? dep._value.cache : SENTINEL; + self._dep.hasData[idx] = seedData; + self._dep.dirty[idx] = false; + self._dep.tier[idx] = seedData ? 3 : 0; + self._dep.terminal[idx] = dep._value.terminal; + self._dep.terminalInput[idx] = void 0; +} +function nodeDeactivate(self) { + self._lifecycle.activated = false; + for (const u of self._dep.unsubs) if (u) u(); + self._dep.unsubs = []; + self._dep.idxBoxes = []; + for (const fn of self._hooks.onDeactivation) fn(); + self._hooks.onDeactivation = []; + self._hooks.onInvalidate = []; + const isCompute = self._slot.handle !== null || self._slot.deps.length > 0; + if (isCompute) { + self._value.cache = SENTINEL; + self._value.hasData = false; + self._value.status = "sentinel"; + } + self._resetDepState(); + self._wave.hasCalledFnOnce = false; + self._control.pauseLockset.clear(); + self._control.pauseBuffer = []; + self._control.pausedDepWaveOccurred = false; + self._control.demandOwed = void 0; + self._control.activePull = void 0; + self._control.pullDirtyOwed = false; + self._value.replayRing = []; + if (!self._privateState.persist) self._privateState.value = SENTINEL; +} +function nodeSubscriberCount(self) { + return self._lifecycle.subscribers.size; +} +function nodeIsRuntimeQuiescentForRelease(self) { + return !self._released && self._value.status !== "dirty" && self._value.status !== "pending" && self._wave.pending === 0 && !self._wave.insideRunWave && !self._wave.inDepMutation && !self._wave.rewireRunPending && !self._wave.batchDirtyOwed && self._dep.dirty.every((dirty) => !dirty) && self._control.pauseBuffer.length === 0 && !self._control.pausedDepWaveOccurred && self._control.demandOwed === void 0 && self._control.activePull === void 0 && !self._control.inDeliverDemand && self._control.pauseLockset.size === 0; +} +function nodeReleaseRuntime(self) { + if (self._released) return; + self._released = true; + const node = self; + releasedNodes.add(node); + let releaseError; + const recordReleaseError = (error) => { + if (releaseError === void 0) releaseError = error; + }; + self._lifecycle.activated = false; + for (const u of self._dep.unsubs) { + try { + u(); + } catch (error) { + recordReleaseError(error); + } + } + for (const fn of self._hooks.onDeactivation) { + try { + fn(); + } catch (error) { + recordReleaseError(error); + } + } + self._dep.unsubs = []; + self._dep.idxBoxes = []; + self._lifecycle.subscribers.clear(); + if (self._slot.handle !== null) { + self._slot.dispatcher.unregister(self._slot.handle); + self._slot.handle = null; + } + self._slot.deps = []; + self._dep.batch = []; + self._dep.waveData = []; + self._dep.waveTokens = []; + self._dep.waveLive = []; + self._dep.prev = []; + self._dep.hasData = []; + self._dep.dirty = []; + self._dep.tier = []; + self._dep.terminal = []; + self._dep.terminalInput = []; + self._value.cache = SENTINEL; + self._value.hasData = false; + self._value.status = "sentinel"; + self._value.terminal = void 0; + self._value.replayRing = []; + self._privateState.value = SENTINEL; + self._privateState.persist = false; + self._syncCtx = null; + self._resetDepState(); + self._hooks.onDeactivation = []; + self._hooks.onInvalidate = []; + self._control.pauseLockset.clear(); + self._control.pauseBuffer = []; + self._control.pausedDepWaveOccurred = false; + self._control.demandOwed = void 0; + self._control.activePull = void 0; + self._control.pullDirtyOwed = false; + self._restoredActivationPending = false; + checkpointReaders.delete(node); + restoreWriters.delete(node); + runtimeReleasers.delete(node); + runtimeQuiescenceReaders.delete(node); + subscriberCountReaders.delete(node); + activationReaders.delete(node); + ownerTokens.delete(node); + topologyDepsChangedObservers.delete(node); + self._core.releaseSlot(self._id); + if (releaseError !== void 0) throw releaseError; +} +function nodeResetDepState(self) { + const n = self._slot.deps.length; + for (let i = 0; i < n; i++) { + self._dep.batch[i] = null; + self._dep.waveData[i] = []; + self._dep.waveTokens[i] = void 0; + self._dep.waveLive[i] = []; + self._dep.prev[i] = SENTINEL; + self._dep.hasData[i] = false; + self._dep.dirty[i] = false; + self._dep.tier[i] = 0; + self._dep.terminal[i] = void 0; + self._dep.terminalInput[i] = void 0; + } + self._wave.pending = 0; + self._wave.emittedDirtyThisWave = false; +} + +// src/json/codec.ts +var JS_MIN_NORMAL_NUMBER = 2 ** -1022; +function deepFreezeStrictJson(value) { + if (value !== null && typeof value === "object") { + if (Array.isArray(value)) { + for (const item of value) deepFreezeStrictJson(item); + } else { + for (const item of Object.values(value)) deepFreezeStrictJson(item); + } + Object.freeze(value); + } + return value; +} +function assertStableJsonNumber(value, path) { + if (!Number.isFinite(value)) { + throw new TypeError(`stableJsonString: non-finite number at ${path}`); + } +} +function assertStrictJsonNumber(value, path) { + assertStableJsonNumber(value, path); + if (Object.is(value, -0)) { + throw new TypeError(`stableJsonString: non-canonical number at ${path}`); + } + const abs = Math.abs(value); + if (abs > 0 && abs < JS_MIN_NORMAL_NUMBER) { + throw new TypeError(`stableJsonString: subnormal number at ${path}`); + } + if (Number.isInteger(value) && !Number.isSafeInteger(value)) { + throw new TypeError(`stableJsonString: integer outside safe range at ${path}`); + } +} +function sortedJsonValue(value, seen = /* @__PURE__ */ new Set(), path = "$", strictNumbers = false) { + if (value === null) return null; + if (typeof value === "string" || typeof value === "boolean") return value; + if (typeof value === "number") { + if (strictNumbers) assertStrictJsonNumber(value, path); + else assertStableJsonNumber(value, path); + return value; + } + if (typeof value !== "object") { + throw new TypeError(`stableJsonString: value at ${path} is not JSON-encodable`); + } + if (seen.has(value)) throw new TypeError(`stableJsonString: circular reference at ${path}`); + const proto = Object.getPrototypeOf(value); + if (!Array.isArray(value) && proto !== Object.prototype && proto !== null) { + throw new TypeError(`stableJsonString: non-plain object at ${path}`); + } + seen.add(value); + try { + if (Array.isArray(value)) { + if (Object.getOwnPropertySymbols(value).length > 0) { + throw new TypeError(`stableJsonString: symbol-keyed properties at ${path}`); + } + for (const key of Object.getOwnPropertyNames(value)) { + const isIndex = /^(0|[1-9]\d*)$/.test(key) && Number.isSafeInteger(Number(key)) && Number(key) < value.length; + const descriptor = Object.getOwnPropertyDescriptor(value, key); + if (descriptor !== void 0 && ("get" in descriptor || "set" in descriptor)) { + throw new TypeError(`stableJsonString: accessor property at ${path}.${key}`); + } + if (key !== "length" && !isIndex) { + throw new TypeError(`stableJsonString: non-index array property at ${path}.${key}`); + } + if (key !== "length" && descriptor !== void 0 && !descriptor.enumerable) { + throw new TypeError(`stableJsonString: non-enumerable array property at ${path}.${key}`); + } + } + const out2 = []; + for (let i = 0; i < value.length; i += 1) { + if (!(i in value)) { + throw new TypeError(`stableJsonString: sparse array hole at ${path}[${i}]`); + } + out2.push(sortedJsonValue(value[i], seen, `${path}[${i}]`, strictNumbers)); + } + return out2; + } + if (Object.getOwnPropertySymbols(value).length > 0) { + throw new TypeError(`stableJsonString: symbol-keyed properties at ${path}`); + } + for (const key of Object.getOwnPropertyNames(value)) { + const descriptor = Object.getOwnPropertyDescriptor(value, key); + if (descriptor !== void 0 && ("get" in descriptor || "set" in descriptor)) { + throw new TypeError(`stableJsonString: accessor property at ${path}.${key}`); + } + if (descriptor !== void 0 && !descriptor.enumerable) { + throw new TypeError(`stableJsonString: non-enumerable property at ${path}.${key}`); + } + } + const out = /* @__PURE__ */ Object.create(null); + for (const key of Object.keys(value).sort()) { + out[key] = sortedJsonValue( + value[key], + seen, + `${path}.${key}`, + strictNumbers + ); + } + return out; + } finally { + seen.delete(value); + } +} +function stableJsonString(value) { + return JSON.stringify(sortedJsonValue(value)); +} +function strictStableJsonString(value) { + return JSON.stringify(cloneStrictJsonValue(value)); +} +function jsonCodecFor() { + const encoder = new TextEncoder(); + const decoder = new TextDecoder(); + return { + encode(value) { + return encoder.encode(stableJsonString(value)); + }, + decode(bytes) { + return JSON.parse(decoder.decode(bytes)); + } + }; +} +var jsonCodec = jsonCodecFor(); +function bytesEqual(a, b) { + if (a.byteLength !== b.byteLength) return false; + for (let i = 0; i < a.byteLength; i += 1) { + if (a[i] !== b[i]) return false; + } + return true; +} +function hasUnpairedSurrogate(value) { + for (let i = 0; i < value.length; i += 1) { + const code = value.charCodeAt(i); + if (code >= 55296 && code <= 56319) { + const next = value.charCodeAt(i + 1); + if (next >= 56320 && next <= 57343) { + i += 1; + continue; + } + return true; + } + if (code >= 56320 && code <= 57343) return true; + } + return false; +} +function assertNoUnpairedSurrogates(value, seen = /* @__PURE__ */ new Set(), path = "$") { + if (typeof value === "string") { + if (hasUnpairedSurrogate(value)) { + throw new TypeError(`strictJsonCodec: unpaired surrogate at ${path}`); + } + return; + } + if (value === null || typeof value !== "object") return; + if (seen.has(value)) return; + seen.add(value); + try { + if (Array.isArray(value)) { + for (let i = 0; i < value.length; i += 1) { + const descriptor = Object.getOwnPropertyDescriptor(value, String(i)); + if (descriptor === void 0 || "get" in descriptor || "set" in descriptor) continue; + assertNoUnpairedSurrogates(descriptor.value, seen, `${path}[${i}]`); + } + return; + } + for (const key of Object.keys(value)) { + if (hasUnpairedSurrogate(key)) { + throw new TypeError(`strictJsonCodec: unpaired surrogate at ${path}.${key}`); + } + const descriptor = Object.getOwnPropertyDescriptor(value, key); + if (descriptor === void 0 || "get" in descriptor || "set" in descriptor) continue; + assertNoUnpairedSurrogates(descriptor.value, seen, `${path}.${key}`); + } + } finally { + seen.delete(value); + } +} +function strictJsonDataErrorsInner(value, label, seen) { + if (value === null || typeof value === "string" || typeof value === "boolean") { + if (typeof value === "string" && hasUnpairedSurrogate(value)) { + return { errors: [`${label} must not contain unpaired surrogate strings`] }; + } + return { errors: [], value }; + } + if (typeof value === "number") { + try { + assertStrictJsonNumber(value, label); + } catch (error) { + return { errors: [error instanceof Error ? error.message : String(error)] }; + } + return { errors: [], value }; + } + if (typeof value !== "object") { + return { errors: [`${label} is not JSON-encodable`] }; + } + if (seen.has(value)) return { errors: [`${label} must not contain circular references`] }; + const proto = Object.getPrototypeOf(value); + if (!Array.isArray(value) && proto !== Object.prototype && proto !== null) { + return { errors: [`stableJsonString: non-plain object at ${label}`] }; + } + seen.add(value); + try { + if (Array.isArray(value)) { + const errors2 = []; + if (Object.getOwnPropertySymbols(value).length > 0) { + errors2.push(`${label} must not carry symbol keys`); + } + for (const key of Object.getOwnPropertyNames(value)) { + const descriptor = Object.getOwnPropertyDescriptor(value, key); + if (descriptor === void 0) continue; + const isIndex = /^(0|[1-9]\d*)$/.test(key) && Number.isSafeInteger(Number(key)) && Number(key) < value.length; + if ("get" in descriptor || "set" in descriptor) { + errors2.push(`${label}.${key} must be a data property`); + } + if (key !== "length" && !isIndex) { + errors2.push(`${label}.${key} must be an indexed data property`); + } + if (key !== "length" && isIndex && !descriptor.enumerable) { + errors2.push(`${label}.${key} must be enumerable`); + } + } + const out2 = []; + for (let index = 0; index < value.length; index += 1) { + const descriptor = Object.getOwnPropertyDescriptor(value, String(index)); + if (descriptor === void 0) { + errors2.push(`stableJsonString: sparse array hole at ${label}[${index}]`); + continue; + } + if ("get" in descriptor || "set" in descriptor) { + errors2.push(`${label}[${index}] must be a data property`); + continue; + } + if (!descriptor.enumerable) { + errors2.push(`${label}[${index}] must be enumerable`); + continue; + } + const nested = strictJsonDataErrorsInner(descriptor.value, `${label}[${index}]`, seen); + errors2.push(...nested.errors); + if (nested.errors.length === 0 && nested.value !== void 0) out2.push(nested.value); + } + if (errors2.length > 0) return { errors: errors2 }; + return { errors: [], value: Object.freeze(out2) }; + } + const errors = []; + if (Object.getOwnPropertySymbols(value).length > 0) { + errors.push(`${label} must not carry symbol keys`); + } + for (const key of Object.getOwnPropertyNames(value)) { + const descriptor = Object.getOwnPropertyDescriptor(value, key); + if (descriptor === void 0) continue; + if ("get" in descriptor || "set" in descriptor) { + errors.push(`${label}.${key} must be a data property`); + } + if (!descriptor.enumerable) { + errors.push(`${label}.${key} must be enumerable`); + } + if (hasUnpairedSurrogate(key)) { + errors.push(`${label}.${key} must not contain unpaired surrogate keys`); + } + } + const out = {}; + for (const key of Object.keys(value).sort()) { + const descriptor = Object.getOwnPropertyDescriptor(value, key); + if (descriptor === void 0 || "get" in descriptor || "set" in descriptor) continue; + const nested = strictJsonDataErrorsInner(descriptor.value, `${label}.${key}`, seen); + errors.push(...nested.errors); + if (nested.errors.length === 0 && nested.value !== void 0) { + Object.defineProperty(out, key, { + value: nested.value, + enumerable: true, + configurable: true, + writable: true + }); + } + } + if (errors.length > 0) return { errors }; + return { errors: [], value: Object.freeze(out) }; + } finally { + seen.delete(value); + } +} +function cloneStrictJsonValue(value, label = "strictJsonValue") { + const result = strictJsonDataErrorsInner(value, label, /* @__PURE__ */ new Set()); + if (result.errors.length > 0 || result.value === void 0) { + throw new TypeError(`${label}: ${result.errors.join("; ")}`); + } + return deepFreezeStrictJson(result.value); +} +function cloneStrictJsonObject(value, label = "strictJsonObject") { + const cloned = cloneStrictJsonValue(value, label); + if (cloned === null || typeof cloned !== "object" || Array.isArray(cloned)) { + throw new TypeError(`${label}: value must be a strict JSON object`); + } + return cloned; +} +function assertNoDuplicateJsonObjectKeys(text) { + let index = 0; + function fail(message) { + throw new TypeError(`strictJsonCodec: ${message}`); + } + function skipWhitespace() { + while (/\s/.test(text[index] ?? "")) index += 1; + } + function readJsonString() { + const start = index; + index += 1; + while (index < text.length) { + const ch = text[index]; + if (ch === '"') { + index += 1; + try { + return JSON.parse(text.slice(start, index)); + } catch { + fail("malformed JSON string"); + } + } + if (ch === "\\") { + index += 2; + continue; + } + index += 1; + } + fail("unterminated JSON string"); + } + function consumeLiteral(literal) { + if (text.slice(index, index + literal.length) !== literal) { + fail(`malformed JSON near byte ${index}`); + } + index += literal.length; + } + function consumeNumber() { + const match = /^-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?/.exec(text.slice(index)); + if (!match) fail(`malformed JSON number near byte ${index}`); + index += match[0].length; + } + function parseValue(path) { + skipWhitespace(); + const ch = text[index]; + if (ch === "{") { + parseObject(path); + return; + } + if (ch === "[") { + parseArray(path); + return; + } + if (ch === '"') { + readJsonString(); + return; + } + if (ch === "t") { + consumeLiteral("true"); + return; + } + if (ch === "f") { + consumeLiteral("false"); + return; + } + if (ch === "n") { + consumeLiteral("null"); + return; + } + if (ch === "-" || ch !== void 0 && ch >= "0" && ch <= "9") { + consumeNumber(); + return; + } + fail(`malformed JSON near byte ${index}`); + } + function parseObject(path) { + const keys = /* @__PURE__ */ new Set(); + index += 1; + skipWhitespace(); + if (text[index] === "}") { + index += 1; + return; + } + while (index < text.length) { + skipWhitespace(); + if (text[index] !== '"') fail(`expected object key near byte ${index}`); + const key = readJsonString(); + if (keys.has(key)) { + throw new TypeError( + `strictJsonCodec: duplicate object key ${JSON.stringify(key)} at ${path}` + ); + } + keys.add(key); + skipWhitespace(); + if (text[index] !== ":") fail(`expected ':' after object key near byte ${index}`); + index += 1; + parseValue(`${path}.${key}`); + skipWhitespace(); + if (text[index] === ",") { + index += 1; + continue; + } + if (text[index] === "}") { + index += 1; + return; + } + fail(`expected ',' or '}' near byte ${index}`); + } + fail("unterminated JSON object"); + } + function parseArray(path) { + index += 1; + skipWhitespace(); + if (text[index] === "]") { + index += 1; + return; + } + let item = 0; + while (index < text.length) { + parseValue(`${path}[${item}]`); + item += 1; + skipWhitespace(); + if (text[index] === ",") { + index += 1; + continue; + } + if (text[index] === "]") { + index += 1; + return; + } + fail(`expected ',' or ']' near byte ${index}`); + } + fail("unterminated JSON array"); + } + parseValue("$"); + skipWhitespace(); + if (index !== text.length) fail(`trailing JSON token near byte ${index}`); +} +function strictJsonCodecFor() { + const encoder = new TextEncoder(); + const decoder = new TextDecoder("utf-8", { fatal: true }); + return { + encode(value) { + assertNoUnpairedSurrogates(value); + return encoder.encode(strictStableJsonString(value)); + }, + decode(bytes) { + const text = decoder.decode(bytes); + assertNoDuplicateJsonObjectKeys(text); + const decoded = JSON.parse(text); + assertNoUnpairedSurrogates(decoded); + const canonical = encoder.encode(strictStableJsonString(decoded)); + if (!bytesEqual(bytes, canonical)) { + throw new TypeError("strictJsonCodec: bytes are not canonical stable JSON"); + } + return decoded; + } + }; +} +var strictJsonCodec = strictJsonCodecFor(); +function strictCanonicalJsonBytes(value) { + return strictJsonCodec.encode(value); +} +function assertStrictJsonObject(value, label = "strictJsonObject") { + return cloneStrictJsonObject(value, label); +} + +// src/node/versioning.ts +var ABSENT_V1_SEED = Object.freeze({ + "@graphrefly/node-version": "v1-absent" +}); +function fnv1a64(input) { + let hash = 0xcbf29ce484222325n; + const prime = 0x100000001b3n; + for (const byte of input) { + hash ^= BigInt(byte); + hash = BigInt.asUintN(64, hash * prime); + } + return hash.toString(16).padStart(16, "0"); +} +function defaultNodeVersionHash(bytes) { + return `fnv1a64:${fnv1a64(bytes)}`; +} +function computeV1Cid(policy, value) { + return policy.hash(strictCanonicalJsonBytes(value)); +} +function assertNodeVersionDataCompatible(policy, value) { + if (!policy.enabled || policy.level === 0) return; + strictCanonicalJsonBytes(value); +} +function snapshotNodeVersionData(policy, value) { + if (!policy.enabled || policy.level === 0) return value; + const bytes = strictCanonicalJsonBytes(value); + return strictJsonCodec.decode(bytes); +} +function resolveNodeVersioningPolicy(policy) { + if (policy === false) return { enabled: false }; + if (policy === void 0 || policy === 0) return { enabled: true, level: 0 }; + if (policy === 1) return { enabled: true, level: 1, hash: defaultNodeVersionHash }; + if (typeof policy === "object" && policy !== null) { + if (policy.level === 0) return { enabled: true, level: 0 }; + if (policy.level === 1) { + return { enabled: true, level: 1, hash: policy.hash ?? defaultNodeVersionHash }; + } + } + throw new Error("node: versioning level must be 0 or 1; V2/V3 are not locked yet (D109)"); +} +function createNodeVersion(policy, initialValue = ABSENT_V1_SEED) { + if (!policy.enabled) return void 0; + if (policy.level === 0) return Object.freeze({ level: 0, counter: 0 }); + return Object.freeze({ + level: 1, + counter: 0, + cid: computeV1Cid(policy, initialValue), + prev: null + }); +} +function advanceNodeVersion(current, policy, value) { + if (!policy.enabled) return void 0; + if (current === void 0) return createNodeVersion(policy, value); + if (policy.level === 0) { + return Object.freeze({ level: 0, counter: current.counter + 1 }); + } + const previous = current.level === 1 ? current.cid : null; + return Object.freeze({ + level: 1, + counter: current.counter + 1, + cid: computeV1Cid(policy, value), + prev: previous + }); +} +function cloneNodeVersion(version) { + if (version === void 0) return void 0; + if (version.level === 0) return Object.freeze({ level: 0, counter: version.counter }); + return Object.freeze({ + level: 1, + counter: version.counter, + cid: version.cid, + prev: version.prev + }); +} +function restoredV1Cid(policy, hasData, cache) { + return computeV1Cid(policy, hasData ? cache : ABSENT_V1_SEED); +} + +// src/node/node-output-runtime.ts +function nodeDown(self, msgs) { + if (self._released) return; + validateDownPayloads(msgs); + const deliveryWave = {}; + const assertVersionDataCompatible = (wave) => { + for (const m of wave) { + if (m[0] === "DATA") assertNodeVersionDataCompatible(self._version.policy, m[1]); + } + }; + const snapshotVersionData = (wave) => wave.map( + (m) => m[0] === "DATA" ? ["DATA", snapshotNodeVersionData(self._version.policy, m[1])] : m + ); + assertVersionDataCompatible(msgs); + if (self._value.terminal !== void 0) { + if (!msgs.some((m) => m[0] === "TEARDOWN")) return; + self._value.hasTorndown = true; + if (self._slot.resetOnTeardown) { + self._value.cache = SENTINEL; + self._value.hasData = false; + } + self._emitToSubs(["TEARDOWN"], { wave: deliveryWave, last: true }); + return; + } + let sorted = [...msgs].sort((a, b) => messageTier(a[0]) - messageTier(b[0])); + const firstInvalidate = sorted.findIndex((m) => m[0] === "INVALIDATE"); + if (firstInvalidate !== -1) { + sorted = sorted.filter((m, i) => m[0] !== "INVALIDATE" || i === firstInvalidate); + } + const hasTeardown = sorted.some((m) => m[0] === "TEARDOWN"); + const hasTerminal = sorted.some((m) => m[0] === "COMPLETE" || m[0] === "ERROR"); + if (hasTeardown && !hasTerminal && self._value.terminal === void 0 && !self._value.hasTorndown) { + sorted = [["COMPLETE"], ...sorted]; + } + if (!self._wave.insideRunWave && currentBatch()) { + const deferred = snapshotVersionData(sorted.filter((m) => isDeferredTier(m[0]))); + if (deferred.length > 0) { + if (!self._wave.emittedDirtyThisWave) { + self._wave.emittedDirtyThisWave = true; + self._value.status = "dirty"; + self._emitToSubs(["DIRTY"], { wave: deliveryWave, last: false }); + } + self._wave.batchDirtyOwed = true; + deferToBatch(self, deferred); + return; + } + } + if (self._shouldBufferOnPause()) { + const buffered = snapshotVersionData(sorted.filter((m) => isPauseBufferedTier(m[0]))); + if (buffered.length > 0) { + self._wave.emittedSettleThisWave = true; + self._control.pauseBuffer.push(buffered); + } + sorted = sorted.filter((m) => !isPauseBufferedTier(m[0])); + if (sorted.length === 0) return; + } + let dataCount = 0; + let hasTier3 = false; + let hasResolved = false; + for (const m of sorted) { + if (m[0] === "DATA") dataCount++; + if (m[0] === "RESOLVED") hasResolved = true; + if (isValueTier(m[0])) hasTier3 = true; + } + if (dataCount >= 1 && hasResolved) { + throw new Error( + "down: a wave cannot mix DATA and RESOLVED (tier-3 exclusivity, R-resolved-undirty)" + ); + } + const plannedVersions = new Array(sorted.length); + if (dataCount > 0) { + let plannedVersion = self._version.value; + for (let i = 0; i < sorted.length; i++) { + const m = sorted[i]; + if (m[0] !== "DATA") continue; + plannedVersion = advanceNodeVersion(plannedVersion, self._version.policy, m[1]); + plannedVersions[i] = plannedVersion; + } + } + if (hasTier3 && (!self._wave.insideRunWave || self._control.pullDirtyOwed) && !self._wave.emittedDirtyThisWave) { + self._wave.emittedDirtyThisWave = true; + self._value.status = "dirty"; + self._emitToSubs(["DIRTY"], { wave: deliveryWave, last: false }); + } + for (let i = 0; i < sorted.length; i++) { + const m = sorted[i]; + const delivery = { wave: deliveryWave, last: i === sorted.length - 1 }; + if (isDeferredTier(m[0])) self._wave.emittedSettleThisWave = true; + if (m[0] === "DIRTY") { + if (!self._wave.emittedDirtyThisWave) { + self._wave.emittedDirtyThisWave = true; + self._value.status = "dirty"; + self._emitToSubs(["DIRTY"], delivery); + } + continue; + } + if (m[0] === "DATA") { + const v = m[1]; + self._value.cache = v; + self._value.hasData = true; + self._value.status = "settled"; + self._version.value = plannedVersions[i]; + if (self._slot.replayN > 0) { + self._value.replayRing.push(v); + if (self._value.replayRing.length > self._slot.replayN) self._value.replayRing.shift(); + } + self._emitToSubs(["DATA", v], delivery); + continue; + } + if (m[0] === "RESOLVED") { + self._value.status = self._value.hasData ? "resolved" : "sentinel"; + self._emitToSubs(["RESOLVED"], delivery); + continue; + } + if (m[0] === "INVALIDATE") { + self._invalidate(delivery); + continue; + } + if (m[0] === "COMPLETE") { + if (self._value.terminal !== void 0) continue; + self._value.terminal = true; + self._control.pauseBuffer = []; + self._value.status = "completed"; + self._emitToSubs(["COMPLETE"], delivery); + continue; + } + if (m[0] === "ERROR") { + if (self._value.terminal !== void 0) continue; + self._value.terminal = m[1]; + self._control.pauseBuffer = []; + self._value.status = "errored"; + self._emitToSubs(["ERROR", m[1]], delivery); + continue; + } + if (m[0] === "TEARDOWN") { + self._value.hasTorndown = true; + if (self._slot.resetOnTeardown) { + self._value.cache = SENTINEL; + self._value.hasData = false; + } + self._emitToSubs(["TEARDOWN"], delivery); + } + } + if (!self._wave.insideRunWave) self._wave.emittedDirtyThisWave = false; +} +function nodeUp(self, msgs, towardDep, route) { + if (self._released) return; + const routeState = route ?? { demandFired: /* @__PURE__ */ new Map() }; + for (const m of msgs) { + const tier = messageTier(m[0]); + if (tier === void 0) { + throw new Error( + `ctx.up: ${String(m[0])} is not in the closed message-type set (R-msg-closed-set)` + ); + } + if (!isUpAllowed(m[0])) { + throw new Error( + `ctx.up: ${m[0]} is not up-going (tier ${tier}); up carries control/demand messages only (R-ctx-up)` + ); + } + } + for (const m of msgs) { + if (m[0] === "PAUSE") { + self._pauseAcquire(m[1]); + } else if (m[0] === "RESUME") { + if (self._control.pauseLockset.has(m[1])) { + self._pauseRelease(m[1]); + } else { + self._forwardUp(m, towardDep, routeState); + } + } else if (m[0] === "PULL") { + const demand = normalizePullDemand(m[1]); + if (self._slot.pull && demand.pullId === self._slot.pullLock) { + if (!self._markDemandRouted(demand.pullId, routeState)) self._onDemand(demand); + } else { + self._forwardUp(["PULL", demand], towardDep, routeState); + } + } else if (self._slot.deps.length === 0) { + if (m[0] === "INVALIDATE") self._down([["INVALIDATE"]]); + } else { + self._forwardUp(m, towardDep, routeState); + } + } +} +function nodeMarkDemandRouted(self, lockId, route) { + let holders = route.demandFired.get(lockId); + if (holders === void 0) { + holders = /* @__PURE__ */ new Set(); + route.demandFired.set(lockId, holders); + } + const node = self; + if (holders.has(node)) return true; + holders.add(node); + return false; +} +function nodeForwardUp(self, m, towardDep, route) { + if (self._slot.deps.length === 0) return; + if (towardDep !== void 0) { + const d = self._slot.deps[towardDep]; + if (d !== void 0) nodeRuntimeHost(d)._up([m], void 0, route); + } else { + for (const dep of self._slot.deps) nodeRuntimeHost(dep)._up([m], void 0, route); + } +} +function nodeIsPaused(self) { + return self._control.pauseLockset.size > 0; +} +function nodeHasBoundaryPauseLock(self) { + if (self._slot.pausable === false) return false; + return self._control.pauseLockset.size > 0; +} +function nodeIsAsyncPool(self) { + return self._slot.handle !== null && self._slot.dispatcher.poolKind(self._slot.handle.poolId) === "async"; +} +function nodePauseAcquire(self, lockId) { + self._control.pauseLockset.add(lockId); +} +function nodePauseRelease(self, lockId) { + if (!self._control.pauseLockset.has(lockId)) return; + self._control.pauseLockset.delete(lockId); + if (self._slot.pull && self._control.demandOwed !== void 0) self._fireOwedDemandIfReady(); + if (self._hasBoundaryPauseLock()) return; + scheduleBoundaryDrain(self._core); + if (self._slot.pull) return; + self._onResume(); +} +function nodeOnResume(self) { + if (self._value.terminal !== void 0) { + self._control.pauseBuffer = []; + self._control.pausedDepWaveOccurred = false; + self._control.demandOwed = void 0; + self._control.activePull = void 0; + self._control.pullDirtyOwed = false; + return; + } + if (self._control.pauseBuffer.length > 0) { + const buf = self._control.pauseBuffer; + self._control.pauseBuffer = []; + for (const wave of buf) self._down(wave); + } + if (self._control.pausedDepWaveOccurred) { + self._control.pausedDepWaveOccurred = false; + self._tryRun(); + } +} +function nodeCanFireDemand(self) { + if (self._value.terminal !== void 0 || self._wave.pending > 0) return false; + return self._control.pauseLockset.size === 0; +} +function nodeDeliverPullDemand(self, demand) { + self._control.demandOwed = void 0; + self._control.activePull = demand; + self._control.inDeliverDemand = true; + try { + self._firePullDemand(); + } finally { + self._control.activePull = void 0; + self._control.inDeliverDemand = false; + } +} +function nodeOnDemand(self, demand) { + if (self._control.inDeliverDemand) return; + if (self._canFireDemand()) self._deliverPullDemand(demand); + else self._control.demandOwed = demand; +} +function nodeFirePullDemand(self) { + let drainedBuffer = false; + if (self._control.pauseBuffer.length > 0) { + const buf = self._control.pauseBuffer; + self._control.pauseBuffer = []; + for (const wave of buf) self._down(wave); + drainedBuffer = true; + } + if (drainedBuffer) return; + if (self._control.pausedDepWaveOccurred) { + const gated = self._slot.handle !== null && !self._wave.hasCalledFnOnce && !(self._slot.partial || self._allDepsSettled()); + if (self._wave.pending > 0 || gated) return; + self._control.pausedDepWaveOccurred = false; + self._wave.emittedDirtyThisWave = false; + self._control.pullDirtyOwed = true; + try { + self._tryRun(); + } finally { + self._control.pullDirtyOwed = false; + } + return; + } + if (self._slot.handle !== null) { + if (!self._wave.hasCalledFnOnce && !(self._slot.partial || self._allDepsSettled())) return; + self._wave.emittedDirtyThisWave = false; + self._control.pullDirtyOwed = true; + try { + self._runWave(); + } finally { + self._control.pullDirtyOwed = false; + } + } +} +function nodeFireOwedDemandIfReady(self) { + if (self._control.inDeliverDemand) return; + if (self._slot.pull && self._control.demandOwed !== void 0 && self._canFireDemand()) { + self._deliverPullDemand(self._control.demandOwed); + } +} +function nodeShouldBufferOnPause(self) { + if (self._slot.pausable === false) return false; + if (!self._isPaused() && !self._isPullQuiet()) return false; + if (self._slot.pausable === "resumeAll") return true; + if (!self._wave.insideRunWave && self._isAsyncPool() && self._slot.deps.length > 0) return true; + return false; +} +function nodeInvalidate(self, delivery) { + if (!self._value.hasData) return; + self._value.cache = SENTINEL; + self._value.hasData = false; + self._value.status = "sentinel"; + self._value.replayRing = []; + for (const fn of self._hooks.onInvalidate) fn(); + self._emitToSubs(["INVALIDATE"], delivery); +} +function nodeAllDepsTerminal(self) { + if (self._slot.deps.length === 0) return false; + for (const tm of self._dep.terminal) if (tm === void 0) return false; + return true; +} +function nodeResetLifecycle(self) { + for (const u of self._dep.unsubs) if (u) u(); + self._dep.unsubs = []; + self._dep.idxBoxes = []; + self._lifecycle.subscribers.clear(); + self._lifecycle.activated = false; + self._value.terminal = void 0; + self._value.hasTorndown = false; + self._wave.hasCalledFnOnce = false; + self._resetDepState(); + self._control.pauseLockset.clear(); + self._control.pauseBuffer = []; + self._control.pausedDepWaveOccurred = false; + self._control.demandOwed = void 0; + self._control.activePull = void 0; + self._control.pullDirtyOwed = false; + self._value.replayRing = []; + const isCompute = self._slot.handle !== null || self._slot.deps.length > 0; + if (isCompute) { + self._value.cache = SENTINEL; + self._value.hasData = false; + self._value.status = "sentinel"; + } else { + self._value.status = self._value.hasData ? "settled" : "sentinel"; + } + if (!self._privateState.persist) self._privateState.value = SENTINEL; +} +function nodeEmitToSubs(self, msg, delivery) { + if (self._released) return; + const subs = [...self._lifecycle.subscribers]; + for (const sink of subs) sink(msg, delivery); +} +function nodeCommitBatchedWave(self, wave) { + self._wave.batchDirtyOwed = false; + self._down(wave); +} +function nodeRollbackBatched(self) { + if (self._wave.batchDirtyOwed) { + self._wave.batchDirtyOwed = false; + self._wave.emittedDirtyThisWave = false; + self._value.status = self._value.hasData ? "settled" : "sentinel"; + self._emitToSubs(["RESOLVED"]); + } +} +function nodeDeferBoundary(self, fn, batchToken) { + deferRewire(self._core, fn, { + batchToken, + isReady: () => !self._hasBoundaryPauseLock() + }); +} + +// src/node/node-rewire-runtime.ts +function nodeRequestRewireNext(self, op) { + deferRewire(self._core, () => self._applyRewireNext(op), { + batchToken: currentBoundaryBatchToken(), + isReady: () => !self._hasBoundaryPauseLock() + }); +} +function nodeRequestUpNext(self, msgs, towardDep) { + deferRewire( + self._core, + () => { + if (!self._released) self._up(msgs, towardDep); + }, + { + batchToken: currentBoundaryBatchToken(), + isReady: () => !self._hasBoundaryPauseLock() + } + ); +} +function nodeApplyRewireNext(self, op) { + if (self._released) return; + try { + if (op.kind === "add") { + const next = self._slot.deps.includes(op.dep) ? [...self._slot.deps] : [...self._slot.deps, op.dep]; + self._rewire(next, op.fn, { allowTerminalOwner: true }); + } else if (op.kind === "remove") { + self._rewire( + self._slot.deps.filter((d) => d !== op.dep), + op.fn, + { allowTerminalOwner: true } + ); + } else { + self._rewire(self._dedupDeps(op.deps), op.fn, { allowTerminalOwner: true }); + } + } catch (e) { + self._down([["ERROR", errorPayload(e, "rewireNext op failed")]]); + } +} +function nodeRewire(self, newDeps, fn, opts = {}) { + const node = self; + if (self._value.terminal !== void 0 && !opts.allowTerminalOwner) + throw new Error( + "rewire: node is terminal (completed/errored) \u2014 cannot rewire (R-rewire / D42)" + ); + if (self._wave.insideRunWave) + throw new Error( + "rewire: mid-fn topology mutation \u2014 a fn mutating its own deps mid-wave is the feedback cycle (R-rewire / D37)" + ); + if (self._wave.inDepMutation) + throw new Error( + "rewire: reentrant dep mutation \u2014 another replaceDeps/subscribeDep/unsubscribeDep is in flight (R-rewire)" + ); + if (newDeps.includes(node)) throw new Error("rewire: self-dependency rejected (R-rewire / D42)"); + const oldDeps = self._slot.deps; + const added = newDeps.filter((d) => !oldDeps.includes(d)); + for (const d of added) { + if (self._reachableUpstream(d, node)) + throw new Error( + "rewire: would create a cycle \u2014 dep already transitively depends on this node (R-rewire / D42)" + ); + const dep = nodeRuntimeHost(d); + if (dep._value.terminal !== void 0 && !dep._slot.resubscribable) + throw new Error( + "rewire: cannot add a non-resubscribable terminal dep \u2014 would wedge (R-rewire / D42)" + ); + self._assertRewireDepOwner(d); + } + if (deferAfterBatchForTarget(node, () => { + self._rewire(newDeps, fn, { ...opts, allowTerminalOwner: true }); + })) { + return true; + } + if (!self._lifecycle.activated) self._restoredActivationPending = false; + self._wave.inDepMutation = true; + self._wave.rewireRunPending = false; + let zeroDepUnDirty = false; + try { + const oldHandle = self._slot.handle; + self._slot.handle = self._slot.dispatcher.register(fn, self._slot.pool); + if (oldHandle !== null) self._slot.dispatcher.unregister(oldHandle); + const removed = oldDeps.filter((d) => !newDeps.includes(d)); + let removedDirtyContributor = false; + for (const d of removed) { + const oldIdx = oldDeps.indexOf(d); + if (self._dep.dirty[oldIdx]) { + removedDirtyContributor = true; + self._wave.pending--; + } + if (self._lifecycle.activated) { + const box = self._dep.idxBoxes[oldIdx]; + if (box) box.v = -1; + const unsub = self._dep.unsubs[oldIdx]; + if (unsub) unsub(); + } + } + const n = newDeps.length; + const newBatch = new Array(n).fill(null); + const newPrev = new Array(n).fill(SENTINEL); + const newHasData = new Array(n).fill(false); + const newDirty = new Array(n).fill(false); + const newTier = new Array(n).fill(0); + const newTerminal = new Array(n).fill(void 0); + const newTerminalInput = new Array(n).fill(void 0); + const newUnsubs = new Array(n); + const newBoxes = new Array(n); + for (let j = 0; j < n; j++) { + const oldIdx = oldDeps.indexOf(newDeps[j]); + if (oldIdx !== -1) { + newBatch[j] = self._dep.batch[oldIdx]; + newPrev[j] = self._dep.prev[oldIdx]; + newHasData[j] = self._dep.hasData[oldIdx]; + newDirty[j] = self._dep.dirty[oldIdx]; + newTier[j] = self._dep.tier[oldIdx]; + newTerminal[j] = self._dep.terminal[oldIdx]; + newUnsubs[j] = self._dep.unsubs[oldIdx]; + const box = self._dep.idxBoxes[oldIdx]; + if (box) box.v = j; + newBoxes[j] = box; + } + } + self._slot.deps = newDeps; + self._dep.batch = newBatch; + self._dep.prev = newPrev; + self._dep.hasData = newHasData; + self._dep.dirty = newDirty; + self._dep.tier = newTier; + self._dep.terminal = newTerminal; + self._dep.terminalInput = newTerminalInput; + self._dep.unsubs = newUnsubs; + self._dep.idxBoxes = newBoxes; + self._dep.waveData = newDeps.map(() => []); + self._dep.waveTokens = new Array(newDeps.length).fill(void 0); + self._dep.waveLive = newDeps.map(() => []); + self._syncCtx = null; + if (self._lifecycle.activated) { + for (const d of added) self._subscribeDepAt(d); + } + notifyTopologyDepsChanged(node, oldDeps, newDeps); + if (removedDirtyContributor && self._wave.pending === 0 && self._value.status === "dirty") { + if (newDeps.length > 0) self._wave.rewireRunPending = true; + else zeroDepUnDirty = true; + } + } finally { + self._wave.inDepMutation = false; + } + if (self._wave.rewireRunPending) { + self._wave.rewireRunPending = false; + self._settleRewire(); + } else if (zeroDepUnDirty) { + if (self._wave.emittedDirtyThisWave) self._down([["RESOLVED"]]); + else self._value.status = self._value.hasData ? "settled" : "sentinel"; + } + return false; +} + +// src/node/node.ts +var Node = class _Node { + _core; + _id; + _slot; + _dep; + _value; + _wave; + _control; + _lifecycle; + _privateState; + _hooks; + _syncCtxState; + _version; + _restoredActivationPending = false; + _released = false; + get _syncCtx() { + return this._syncCtxState.value; + } + set _syncCtx(ctx) { + this._syncCtxState.value = ctx; + } + static _retainIndirectRuntimeMethods(node) { + void node._dep; + void node._hooks; + void node._restoredActivationPending; + void node._requestRewireNext; + void node._requestUpNext; + void node._applyRewireNext; + void node._reachableUpstream; + void node._assertRewireDepOwner; + void node._subscribeDepAt; + void node._seedRestoredDepAt; + void node._recordDepProjection; + void node._depProjectionHasData; + void node._receiveFromDep; + void node._releaseDepDirty; + void node._settleAfterAbsorbedTerminal; + void node._markDirty; + void node._maybeRun; + void node._settleRewire; + void node._tryRun; + void node._allDepsSettled; + void node._passthroughEmit; + void node._runWave; + void node._buildCtx; + void node._makeCtx; + void node._refreshCtx; + void node._makeState; + void node._markDemandRouted; + void node._forwardUp; + void node._isPullQuiet; + void node._isPaused; + void node._hasBoundaryPauseLock; + void node._isAsyncPool; + void node._pauseAcquire; + void node._pauseRelease; + void node._onResume; + void node._canFireDemand; + void node._deliverPullDemand; + void node._onDemand; + void node._firePullDemand; + void node._fireOwedDemandIfReady; + void node._shouldBufferOnPause; + void node._invalidate; + void node._allDepsTerminal; + void node._emitToSubs; + } + constructor(deps, handleOrFn, opts = {}) { + const core = takeConstructingNodeCore(); + const dispatcher = opts.dispatcher ?? defaultDispatcher; + const environment = takeConstructingEnvironmentDrivers() ?? EnvironmentDrivers.empty(); + const pool = opts.pool ?? "sync"; + const pausable = opts.pausable ?? true; + const pullLock = opts.pullId; + const pull = opts.pullId !== void 0; + if (pull && pausable === false) + throw new Error( + "node: pullId is incompatible with pausable:false \u2014 a pull node uses the pausable delivery-content axis (R-pull / R-pause-modes / D55,D269)" + ); + let handle; + if (handleOrFn === null) handle = null; + else if (typeof handleOrFn === "function") handle = dispatcher.register(handleOrFn, pool); + else handle = handleOrFn; + const n = deps.length; + const dep = makeDepBookkeeping(n); + const versioning = resolveNodeVersioningPolicy(opts.versioning); + const value = { + cache: SENTINEL, + hasData: false, + status: "sentinel", + terminal: void 0, + hasTorndown: false, + replayRing: [] + }; + if (opts.initial !== void 0) { + value.cache = opts.initial; + value.hasData = true; + value.status = "settled"; + } + const pauseLockset = /* @__PURE__ */ new Set(); + this._core = core ?? new NodeCore(); + const created = this._core.createSlot( + { + deps, + handle, + pool, + dispatcher, + environment, + partial: opts.partial ?? false, + terminalAsRealInput: opts.terminalAsRealInput ?? false, + completeWhenDepsComplete: opts.completeWhenDepsComplete ?? true, + errorWhenDepsError: opts.errorWhenDepsError ?? true, + resubscribable: opts.resubscribable ?? false, + resetOnTeardown: opts.resetOnTeardown ?? false, + pausable, + pull, + pullLock, + replayN: opts.replayBuffer ?? 0, + dynamic: opts.dynamic ?? false, + name: opts.name, + factory: opts.factory + }, + { + dep, + lifecycle: { subscribers: /* @__PURE__ */ new Set(), activated: false }, + value, + wave: { + pending: 0, + hasCalledFnOnce: false, + emittedDirtyThisWave: false, + emittedSettleThisWave: false, + insideRunWave: false, + inDepMutation: false, + rewireRunPending: false, + batchDirtyOwed: false + }, + control: { + pauseLockset, + pausedDepWaveOccurred: false, + pauseBuffer: [], + demandOwed: void 0, + activePull: void 0, + pullDirtyOwed: false, + inDeliverDemand: false + }, + privateState: { value: SENTINEL, persist: false }, + hooks: { onDeactivation: [], onInvalidate: [] }, + syncCtx: { value: null }, + version: { + policy: versioning, + value: createNodeVersion( + versioning, + opts.initial !== void 0 ? opts.initial : void 0 + ) + } + } + ); + this._id = created.id; + this._slot = this._core.get(this._id); + this._dep = this._core.getDep(this._id); + this._value = this._core.getValue(this._id); + this._wave = this._core.getWave(this._id); + this._control = this._core.getControl(this._id); + this._lifecycle = this._core.getLifecycle(this._id); + this._privateState = this._core.getPrivateState(this._id); + this._hooks = this._core.getHooks(this._id); + this._syncCtxState = this._core.getSyncCtx(this._id); + this._version = this._core.getVersion(this._id); + checkpointReaders.set(this, () => ({ + cache: this._value.cache, + hasData: this._value.hasData, + terminal: this._value.terminal, + activated: this._lifecycle.activated, + hasCalledFnOnce: this._wave.hasCalledFnOnce, + ctxState: { + value: this._privateState.value, + persist: this._privateState.persist + }, + version: cloneNodeVersion(this._version.value), + handle: this._slot.handle + })); + restoreWriters.set(this, (state) => { + this._assertNotReleased("restoreGraph"); + this._value.cache = state.cache; + this._value.hasData = state.hasData; + this._value.status = state.status; + this._value.terminal = state.terminal; + this._value.hasTorndown = false; + this._value.replayRing = []; + this._wave.hasCalledFnOnce = state.hasCalledFnOnce; + this._wave.emittedDirtyThisWave = false; + this._wave.emittedSettleThisWave = false; + this._wave.pending = 0; + this._wave.insideRunWave = false; + this._wave.inDepMutation = false; + this._wave.rewireRunPending = false; + this._wave.batchDirtyOwed = false; + this._control.pauseBuffer = []; + this._control.pausedDepWaveOccurred = false; + this._control.demandOwed = void 0; + this._control.activePull = void 0; + this._control.pullDirtyOwed = false; + this._control.inDeliverDemand = false; + this._control.pauseLockset.clear(); + this._privateState.value = state.ctxState.value; + this._privateState.persist = state.ctxState.persist; + if (state.version === false) { + this._version.policy = { enabled: false }; + this._version.value = void 0; + } else if (state.version.level === 0) { + this._version.policy = { enabled: true, level: 0 }; + this._version.value = cloneNodeVersion(state.version); + } else { + if (!this._version.policy.enabled || this._version.policy.level !== 1) { + throw new Error( + `restoreGraph: checkpoint node version level ${state.version.level} requires matching node versioning policy` + ); + } + if (!state.hasData && state.version.counter > 0) { + throw new Error( + "restoreGraph: checkpoint node version cid cannot be verified without current DATA under V1 versioning (D109)" + ); + } + const expectedCid = restoredV1Cid(this._version.policy, state.hasData, state.cache); + if (expectedCid !== state.version.cid) { + throw new Error( + "restoreGraph: checkpoint node version cid does not match the selected node versioning hash policy (D109)" + ); + } + this._version.value = cloneNodeVersion(state.version); + } + this._syncCtx = null; + this._resetDepState(); + this._lifecycle.activated = false; + this._lifecycle.subscribers.clear(); + this._restoredActivationPending = true; + }); + runtimeReleasers.set(this, () => this._releaseRuntime()); + runtimeQuiescenceReaders.set(this, () => this._isRuntimeQuiescentForRelease()); + subscriberCountReaders.set(this, () => this._subscriberCount()); + activationReaders.set(this, () => this._lifecycle.activated); + _Node._retainIndirectRuntimeMethods(this); + } + /** R-pull (D55/D272): true while a pull node is not serving a PULL demand pulse. */ + _isPullQuiet() { + return this._slot.pull && this._control.activePull === void 0; + } + /** + * R-pull (D269/D272): this pull node's pullId (pure data, like {@link cache}/{@link handle} — + * never triggers computation). A consumer demands one delivery by cone-routing PULL of it (no + * node reference): `ctx.up([["PULL", { pullId }]])` (immediate; loops back → D37 for a self-read + * dep) or `ctx.upNext([["PULL", { pullId }]])` (boundary-deferred self-demand). Undefined for a + * non-pull node. The author writes the pullId verbatim; routing matches by identity. + */ + get pullId() { + return this._slot.pullLock; + } + get cache() { + return this._value.cache; + } + get status() { + return this._value.status; + } + get version() { + return cloneNodeVersion(this._version.value); + } + get name() { + return this._slot.name; + } + /** R-describe/D51: real factory name for a standalone graph-less node (a runtime *Map inner). */ + get factory() { + return this._slot.factory; + } + /** + * The node's CURRENT/LIVE deps (R-describe / R-edges-derived / D51) — readonly view of the + * live `_deps`, which a rewire (C-8 / C-11) mutates. The graph's describe() reads this (NOT a + * construction-time snapshot) so every edge corresponds to a real current subscription (D3). + * Inspection-only, like cache/status; never triggers computation. + */ + get deps() { + return this._slot.deps; + } + /** + * The fn handle (pure data `(poolId, handleId)`, D7) or null for state/passthrough + * nodes. Inspection-only (L1.6 handle is referenceable/inspectable) — lets the graph + * layer key a dispatcher-backed profile recorder WITHOUT putting counters on the node + * (R-node-thin / D39). + */ + get handle() { + return this._slot.handle; + } + /** R-push-subscribe: a new sink receives START, then cached DATA (or DIRTY if dirty). */ + subscribe(sink) { + this._assertNotReleased("subscribe"); + enterWave(); + try { + if (this._value.terminal !== void 0) { + if (this._slot.resubscribable) { + this._restoredActivationPending = false; + this._resetLifecycle(); + } else + throw new Error( + "subscribe: node is non-resubscribable and has terminated; the stream is permanently over (R-terminal / R2.2.7.b)" + ); + } + this._lifecycle.subscribers.add(sink); + sink(["START"]); + if (this._slot.replayN > 0 && this._value.replayRing.length > 0) { + for (const v of this._value.replayRing) sink(["DATA", v]); + } else if (this._value.hasData && !this._slot.pull) { + sink(["DATA", this._value.cache]); + } else if (this._value.status === "dirty" && !this._slot.pull) { + sink(["DIRTY"]); + } + if (!this._lifecycle.activated) this._activate(); + return () => { + if (!this._lifecycle.subscribers.delete(sink)) return; + if (this._lifecycle.subscribers.size === 0) this._deactivate(); + }; + } finally { + exitWave(); + } + } + /** External emission toward sinks (state-node push, or async late-emit). One call = one wave. */ + down(msgs) { + this._assertNotReleased("down"); + enterWave(); + try { + this._down(msgs); + } finally { + exitWave(); + } + } + /** + * Emit upstream toward deps — control tiers only (R-ctx-up). `towardDep` (a dep index) routes up + * ONE declared edge (R-up-routing directed-up); omitted = broadcast up all deps. + */ + up(msgs, towardDep) { + this._assertNotReleased("up"); + enterWave(); + try { + this._up(msgs, towardDep); + } finally { + exitWave(); + } + } + // ── rewire (R-rewire / D42): intra-graph runtime topology mutation ── + /** + * Replace this node's deps atomically (surgical, Option-C). Requires an explicit + * `fn` (SD-1 fn-deps pairing — user fns read dep input positionally). Kept deps + * keep their subscription + per-dep state; only removed deps unsubscribe and only + * added deps fresh-subscribe (push-on-subscribe for an added cached dep). The + * first-run gate and cache are PRESERVED (R-rewire Q2/Q7). Intra-graph only (D22). + */ + replaceDeps(newDeps, fn) { + this._assertNotReleased("replaceDeps"); + this._rewire(this._dedupDeps(newDeps), fn); + } + /** Subscribe to one dep (special case of replaceDeps); returns its index. fn required (SD-1). */ + subscribeDep(depNode, fn) { + this._assertNotReleased("subscribeDep"); + const next = this._slot.deps.includes(depNode) ? [...this._slot.deps] : [...this._slot.deps, depNode]; + const deferred = this._rewire(next, fn); + return deferred ? next.indexOf(depNode) : this._slot.deps.indexOf(depNode); + } + /** Unsubscribe from one dep (special case of replaceDeps); idempotent if absent (fn swap still applies). */ + unsubscribeDep(depNode, fn) { + this._assertNotReleased("unsubscribeDep"); + this._rewire( + this._slot.deps.filter((d) => d !== depNode), + fn + ); + } + _requestRewireNext(op) { + nodeRequestRewireNext(nodeRuntimeHost(this), op); + } + _requestUpNext(msgs, towardDep) { + nodeRequestUpNext(nodeRuntimeHost(this), msgs, towardDep); + } + _applyRewireNext(op) { + nodeApplyRewireNext(nodeRuntimeHost(this), op); + } + _dedupDeps(deps) { + const seen = /* @__PURE__ */ new Set(); + const out = []; + for (const d of deps) + if (!seen.has(d)) { + seen.add(d); + out.push(d); + } + return out; + } + _reachableUpstream(from, target) { + const seen = /* @__PURE__ */ new Set(); + const stack = [from]; + while (stack.length > 0) { + const n = stack.pop(); + if (n === void 0) continue; + if (n === target) return true; + if (seen.has(n)) continue; + seen.add(n); + for (const d of nodeRuntimeHost(n)._slot.deps) stack.push(d); + } + return false; + } + _assertRewireDepOwner(dep) { + const selfOwner = getNodeOwner(this); + const depOwner = getNodeOwner(dep); + if (selfOwner !== void 0 && depOwner !== void 0 && selfOwner !== depOwner) + throw new Error( + "rewire: dep belongs to a different graph; cross-graph deps require a wire bridge (D22 / R-graph-domain)" + ); + } + _rewire(newDeps, fn, opts = {}) { + return nodeRewire(nodeRuntimeHost(this), newDeps, fn, opts); + } + // ── activation / deactivation (lazy; R-rom-ram) ── + _activate() { + nodeActivate(nodeRuntimeHost(this)); + } + _deactivate() { + nodeDeactivate(nodeRuntimeHost(this)); + } + _assertNotReleased(op) { + if (this._released) + throw new Error(`${op}: node has been released from its graph lifecycle (D122)`); + } + _subscriberCount() { + return nodeSubscriberCount(nodeRuntimeHost(this)); + } + _isRuntimeQuiescentForRelease() { + return nodeIsRuntimeQuiescentForRelease(nodeRuntimeHost(this)); + } + _releaseRuntime() { + nodeReleaseRuntime(nodeRuntimeHost(this)); + } + _resetDepState() { + nodeResetDepState(nodeRuntimeHost(this)); + } + _subscribeDepAt(depNode, opts = {}) { + nodeSubscribeDepAt(nodeRuntimeHost(this), depNode, opts); + } + _seedRestoredDepAt(idx, depNode) { + nodeSeedRestoredDepAt(nodeRuntimeHost(this), idx, depNode); + } + _recordDepProjection(idx, delivery) { + return nodeRecordDepProjection(nodeRuntimeHost(this), idx, delivery); + } + _depProjectionHasData(idx) { + return nodeDepProjectionHasData(nodeRuntimeHost(this), idx); + } + _receiveFromDep(idx, msg, delivery) { + nodeReceiveFromDep(nodeRuntimeHost(this), idx, msg, delivery); + } + _releaseDepDirty(idx) { + nodeReleaseDepDirty(nodeRuntimeHost(this), idx); + } + _settleAfterAbsorbedTerminal() { + nodeSettleAfterAbsorbedTerminal(nodeRuntimeHost(this)); + } + _markDirty() { + nodeMarkDirty(nodeRuntimeHost(this)); + } + _maybeRun() { + nodeMaybeRun(nodeRuntimeHost(this)); + } + _settleRewire() { + nodeSettleRewire(nodeRuntimeHost(this)); + } + _tryRun() { + nodeTryRun(nodeRuntimeHost(this)); + } + _allDepsSettled() { + return nodeAllDepsSettled(nodeRuntimeHost(this)); + } + _passthroughEmit() { + nodePassthroughEmit(nodeRuntimeHost(this)); + } + _runWave() { + nodeRunWave(nodeRuntimeHost(this)); + } + _buildCtx() { + return nodeBuildCtx(nodeRuntimeHost(this)); + } + _makeCtx(snapshot) { + return nodeMakeCtx(nodeRuntimeHost(this), snapshot); + } + _refreshCtx(ctx) { + nodeRefreshCtx(nodeRuntimeHost(this), ctx); + } + _makeState() { + return nodeMakeState(nodeRuntimeHost(this)); + } + // ── downstream emission pipeline (the unified waist) ── + _down(msgs) { + nodeDown(nodeRuntimeHost(this), msgs); + } + _up(msgs, towardDep, route) { + nodeUp(nodeRuntimeHost(this), msgs, towardDep, route); + } + _markDemandRouted(lockId, route) { + return nodeMarkDemandRouted(nodeRuntimeHost(this), lockId, route); + } + _forwardUp(m, towardDep, route) { + nodeForwardUp(nodeRuntimeHost(this), m, towardDep, route); + } + _isPaused() { + return nodeIsPaused(nodeRuntimeHost(this)); + } + _hasBoundaryPauseLock() { + return nodeHasBoundaryPauseLock(nodeRuntimeHost(this)); + } + _isAsyncPool() { + return nodeIsAsyncPool(nodeRuntimeHost(this)); + } + _pauseAcquire(lockId) { + nodePauseAcquire(nodeRuntimeHost(this), lockId); + } + _pauseRelease(lockId) { + nodePauseRelease(nodeRuntimeHost(this), lockId); + } + _onResume() { + nodeOnResume(nodeRuntimeHost(this)); + } + _canFireDemand() { + return nodeCanFireDemand(nodeRuntimeHost(this)); + } + _deliverPullDemand(demand) { + nodeDeliverPullDemand(nodeRuntimeHost(this), demand); + } + _onDemand(demand) { + nodeOnDemand(nodeRuntimeHost(this), demand); + } + _firePullDemand() { + nodeFirePullDemand(nodeRuntimeHost(this)); + } + _fireOwedDemandIfReady() { + nodeFireOwedDemandIfReady(nodeRuntimeHost(this)); + } + _shouldBufferOnPause() { + return nodeShouldBufferOnPause(nodeRuntimeHost(this)); + } + _invalidate(delivery) { + nodeInvalidate(nodeRuntimeHost(this), delivery); + } + _allDepsTerminal() { + return nodeAllDepsTerminal(nodeRuntimeHost(this)); + } + _emitToSubs(msg, delivery) { + nodeEmitToSubs(nodeRuntimeHost(this), msg, delivery); + } + /** R-terminal: resubscribable reset clears terminal + dep state + re-arms the gate. */ + _resetLifecycle() { + nodeResetLifecycle(nodeRuntimeHost(this)); + } + /** Batch commit (R-batch-coalesce): deliver the deferred tier-3 wave now. */ + __commitBatchedWave(wave) { + nodeCommitBatchedWave(nodeRuntimeHost(this), wave); + } + /** Batch rollback: balance the immediate DIRTY with a RESOLVED so downstream un-dirties. */ + __rollbackBatched() { + nodeRollbackBatched(nodeRuntimeHost(this)); + } + /** B49: enqueue a committed-boundary task on this node's graph-local core. */ + __deferBoundary(fn, batchToken) { + nodeDeferBoundary(nodeRuntimeHost(this), fn, batchToken); + } +}; + +// src/identity.ts +function canonicalTupleKey(parts) { + return JSON.stringify(parts); +} + +// src/graph/blueprint.ts +var GRAPH_BLUEPRINT_VERSION = "graphrefly.blueprint.v2"; +function normalizeTopologyMeta(meta, label = "meta", kind = "graph meta") { + try { + return assertStrictJsonObject(meta, label); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new TypeError(`${label}: ${kind} must be strict JSON-compatible data (D177): ${message}`); + } +} +function normalizeTopology(snapshot) { + return normalizeTopologySnapshot(snapshot, /* @__PURE__ */ new WeakSet(), "$"); +} +function normalizeTopologySnapshot(snapshot, seen, path) { + if (snapshot === null || typeof snapshot !== "object") { + throw new TypeError(`normalizeTopology: snapshot at ${path} must be an object`); + } + if (seen.has(snapshot)) { + throw new TypeError(`normalizeTopology: circular subgraph reference at ${path}`); + } + seen.add(snapshot); + try { + const nodes = mapDense(snapshot.nodes, `${path}.nodes`, (node, index) => { + const nodePath = `${path}.nodes[${index}]`; + assertTopologyObject(node, nodePath); + const id = topologyString(node.id, `${nodePath}.id`); + const factory = topologyString(node.factory, `${nodePath}.factory`); + const out2 = { + id, + factory, + deps: mapDense( + node.deps, + `${labelForNode(id)}.deps`, + (dep, depIndex) => topologyString(dep, `${labelForNode(id)}.deps[${depIndex}]`) + ) + }; + if (node.name !== void 0) { + out2.name = topologyString(node.name, `${nodePath}.name`); + } + if (node.meta !== void 0) { + out2.meta = normalizeTopologyMeta(node.meta, `${labelForNode(id)}.meta`); + } + return out2; + }).sort(compareNodes); + const out = { + nodes, + edges: deriveEdges(nodes) + }; + if (snapshot.mountId !== void 0) { + out.mountId = topologyString(snapshot.mountId, `${path}.mountId`); + } + if (snapshot.name !== void 0) { + out.name = topologyString(snapshot.name, `${path}.name`); + } + if (snapshot.subgraphs !== void 0) { + out.subgraphs = mapDense( + snapshot.subgraphs, + `${path}.subgraphs`, + (subgraph, index) => normalizeTopologySnapshot(subgraph, seen, `${path}.subgraphs[${index}]`) + ).sort(compareTopologies); + } + return out; + } finally { + seen.delete(snapshot); + } +} +function mapDense(values, path, mapper) { + if (!Array.isArray(values)) { + throw new TypeError(`normalizeTopology: ${path} must be an array`); + } + const out = []; + for (let i = 0; i < values.length; i += 1) { + if (!(i in values)) { + throw new TypeError(`normalizeTopology: sparse array hole at ${path}[${i}]`); + } + out.push(mapper(values[i], i)); + } + return out; +} +function assertTopologyObject(value, path) { + if (value === null || typeof value !== "object" || Array.isArray(value)) { + throw new TypeError(`normalizeTopology: ${path} must be an object`); + } +} +function topologyString(value, path) { + if (typeof value !== "string") { + throw new TypeError(`normalizeTopology: ${path} must be a string`); + } + return value; +} +function graphBlueprintDiagnostics(topology) { + const issues = []; + collectDiagnostics(topology, issues); + issues.sort(compareIssues); + return { ok: !issues.some((issue) => issue.severity === "error"), issues }; +} +function collectDiagnostics(topology, issues) { + const seen = /* @__PURE__ */ new Set(); + const duplicateIds = /* @__PURE__ */ new Set(); + for (const node of topology.nodes) { + if (seen.has(node.id)) duplicateIds.add(node.id); + seen.add(node.id); + } + for (const id of duplicateIds) { + issues.push({ + severity: "error", + code: "duplicate-node-id", + nodeId: id, + message: `duplicate topology node id '${id}'` + }); + } + const dependents = /* @__PURE__ */ new Map(); + for (const node of topology.nodes) dependents.set(node.id, 0); + for (const node of topology.nodes) { + for (const dep of node.deps) { + if (!seen.has(dep)) { + issues.push({ + severity: "error", + code: "dangling-dep", + nodeId: node.id, + from: dep, + to: node.id, + message: `node '${node.id}' depends on missing node '${dep}'` + }); + continue; + } + dependents.set(dep, (dependents.get(dep) ?? 0) + 1); + } + } + for (const node of topology.nodes) { + if (node.deps.length === 0 && (dependents.get(node.id) ?? 0) === 0) { + issues.push({ + severity: "warning", + code: "island-node", + nodeId: node.id, + message: `node '${node.id}' has no deps and no dependents` + }); + } + } + for (const subgraph of topology.subgraphs ?? []) collectDiagnostics(subgraph, issues); +} +function deriveEdges(nodes) { + const seen = /* @__PURE__ */ new Set(); + const edges = []; + for (const node of nodes) { + for (const from of node.deps) { + const key = canonicalTupleKey([from, node.id]); + if (seen.has(key)) continue; + seen.add(key); + edges.push({ from, to: node.id }); + } + } + return edges.sort(compareEdges); +} +function labelForNode(id) { + return id === "" ? "node" : `node '${id}'`; +} +function compareText(a, b) { + return a < b ? -1 : a > b ? 1 : 0; +} +function compareNodes(a, b) { + return compareText(a.id, b.id); +} +function compareEdges(a, b) { + return compareText(a.from, b.from) || compareText(a.to, b.to); +} +function compareTopologies(a, b) { + return compareText(a.mountId ?? "", b.mountId ?? "") || compareText(stableJsonString(a), stableJsonString(b)); +} +function compareIssues(a, b) { + return compareText(a.code, b.code) || compareText(a.nodeId ?? "", b.nodeId ?? "") || compareText(a.from ?? "", b.from ?? "") || compareText(a.to ?? "", b.to ?? ""); +} + +// src/graph/checkpoint.ts +var GRAPH_CHECKPOINT_VERSION = "graphrefly.checkpoint.v1"; +function toCheckpointJson(value, path = "$") { + try { + return strictJsonCodec.decode(strictJsonCodec.encode(value)); + } catch (cause) { + throw new TypeError(`checkpoint: value at ${path} is not strict JSON compatible`, { + cause + }); + } +} +function checkpointValue(value, hasData, path) { + if (!hasData || value === SENTINEL) return { kind: "SENTINEL" }; + return { kind: "DATA", data: toCheckpointJson(value, path) }; +} +function checkpointTerminal(value, path) { + if (value === void 0) return { kind: "none" }; + if (value === true) return { kind: "COMPLETE" }; + return { kind: "ERROR", error: toCheckpointJson(value, path) }; +} +var backendStateContributors = /* @__PURE__ */ new WeakMap(); +function checkpointBackendStateOfNode(node, path) { + const contributor = backendStateContributors.get(node); + if (contributor === void 0) return void 0; + return toCheckpointJson(contributor(), path); +} + +// src/graph/describe.ts +function topologyFromDescribe(snapshot) { + const nodes = snapshot.nodes.map((node) => { + const topologyNode = { + id: node.id, + factory: node.factory, + deps: [...node.deps] + }; + if (node.name !== void 0) topologyNode.name = node.name; + if (node.meta !== void 0) topologyNode.meta = cloneTopologyMeta(node.meta); + return topologyNode; + }); + const out = { + nodes, + edges: snapshot.edges.map((edge) => ({ from: edge.from, to: edge.to })) + }; + if (snapshot.mountId !== void 0) out.mountId = snapshot.mountId; + if (snapshot.name !== void 0) out.name = snapshot.name; + if (snapshot.subgraphs !== void 0) + out.subgraphs = snapshot.subgraphs.map(topologyFromDescribe); + return out; +} +function cloneTopologyMeta(meta) { + return cloneTopologyValue(meta, /* @__PURE__ */ new WeakMap()); +} +function cloneTopologyValue(value, seen) { + if (value === null) return null; + const kind = typeof value; + if (kind === "string" || kind === "boolean") return value; + if (kind === "number") { + if (!Number.isFinite(value)) { + throw new Error("topology(): meta must be finite JSON-compatible data (D39/D173)"); + } + return value; + } + if (kind !== "object") { + throw new Error("topology(): meta must be JSON-compatible data (D39/D173)"); + } + const objectValue = value; + const cached = seen.get(objectValue); + if (cached !== void 0) return cached; + if (Array.isArray(value)) { + const out2 = new Array(value.length); + seen.set(objectValue, out2); + for (let i = 0; i < value.length; i += 1) { + if (!(i in value)) { + throw new Error("topology(): meta arrays must be dense JSON-compatible data (D39/D173)"); + } + out2[i] = cloneTopologyValue(value[i], seen); + } + return out2; + } + const proto = Object.getPrototypeOf(objectValue); + if (proto !== Object.prototype && proto !== null) { + throw new Error("topology(): meta must be plain JSON-compatible data (D39/D173)"); + } + const out = {}; + seen.set(objectValue, out); + for (const [key, item] of Object.entries(value)) + out[key] = cloneTopologyValue(item, seen); + return out; +} + +// src/graph/graph-lifecycle.ts +var restoreRegistrars = /* @__PURE__ */ new WeakMap(); +var lifecycleRegistrars = /* @__PURE__ */ new WeakMap(); + +// src/graph/graph-support.ts +function isNonAuthoritativeCollectionHelperMeta(meta) { + return meta?.kind === "collection_delta" || meta?.kind === "collection_intent" || meta?.kind === "collection_policy_apply" || meta?.kind === "collection_snapshot" || meta?.kind === "collection_snapshot_prep"; +} +function assertCheckpointQuiescentStatus(status, id, op) { + if (status === "pending" || status === "dirty") { + throw new Error( + `${op}: node '${id}' has non-quiescent status '${status}' that cannot be checkpoint-restored yet` + ); + } +} +function topologyPathMatches(eventPath, path) { + return eventPath === path || eventPath.startsWith(`${path}::`); +} +function prefixTopologyPath(prefix, path) { + return `${prefix}::${path}`; +} +function cloneTopologyEvent(event) { + const deps = Object.freeze([...event.deps]); + const prevDeps = event.prevDeps === void 0 ? void 0 : Object.freeze([...event.prevDeps]); + return Object.freeze({ + kind: event.kind, + path: event.path, + deps, + ...prevDeps !== void 0 ? { prevDeps } : {}, + ...event.factory !== void 0 ? { factory: event.factory } : {}, + seq: event.seq + }); +} +function checkpointFactory(name, node, unregistered, restore, meta) { + const state = checkpointStateOfNode(node); + if (unregistered) { + return { + kind: "local-only", + name, + reason: "node is an unregistered live dependency auto-discovered from topology" + }; + } + if (restore === void 0 && typeof meta?.kind === "string" && meta.kind.startsWith("collection_")) { + return { + kind: "local-only", + name, + reason: "collection helper node has no backend checkpoint restore metadata" + }; + } + if (restore !== void 0) { + const out = { + kind: "registry-ref", + ref: toCheckpointJson(restore.ref, `${name}.factory.ref`) + }; + if (restore.config !== void 0) + out.config = toCheckpointJson(restore.config, `${name}.factory.config`); + if (restore.configVersion !== void 0) + out.configVersion = toCheckpointJson(restore.configVersion, `${name}.factory.configVersion`); + return out; + } + if (state.handle !== null) { + return { + kind: "local-only", + name, + reason: "node uses a function body; first-cut checkpoints do not serialize local functions" + }; + } + return { kind: "registry-ref", ref: name }; +} +function explainSubset(snap, chain) { + const fwd = /* @__PURE__ */ new Map(); + const rev = /* @__PURE__ */ new Map(); + const push = (map, k, v) => { + const a = map.get(k); + if (a) a.push(v); + else map.set(k, [v]); + }; + for (const e of snap.edges) { + push(fwd, e.from, e.to); + push(rev, e.to, e.from); + } + const reach = (start, adj) => { + const seen = /* @__PURE__ */ new Set([start]); + const stack = [start]; + while (stack.length > 0) { + const cur = stack.pop(); + for (const nxt of adj.get(cur) ?? []) { + if (!seen.has(nxt)) { + seen.add(nxt); + stack.push(nxt); + } + } + } + return seen; + }; + const onPath = new Set([...reach(chain.from, fwd)].filter((id) => reach(chain.to, rev).has(id))); + return { + ...snap.name !== void 0 ? { name: snap.name } : {}, + nodes: snap.nodes.filter((n) => onPath.has(n.id)), + edges: snap.edges.filter((e) => onPath.has(e.from) && onPath.has(e.to)) + }; +} + +// src/graph/graph-topology-group.ts +var GraphTopologyGroup = class { + name; + _graph; + _members = []; + _released = false; + constructor(graph2, opts = {}) { + this._graph = graph2; + this.name = opts.name; + } + get released() { + return this._released; + } + add(node) { + this._assertLive(); + const lifecycle = lifecycleRegistrars.get(this._graph); + if (lifecycle === void 0) throw new Error("topologyGroup: graph lifecycle unavailable"); + lifecycle.assertRegisteredNode(node, `topology group '${this.name ?? "group"}' member`); + if (!this._members.includes(node)) this._members.push(node); + return node; + } + node(deps = [], fn = null, opts = {}) { + this._assertLive(); + return this.add(this._graph.node(deps, fn, opts)); + } + state(initial, opts = {}) { + this._assertLive(); + return this.add(this._graph.state(initial, opts)); + } + producer(fn, opts = {}) { + this._assertLive(); + return this.add(this._graph.producer(fn, opts)); + } + derived(deps, fn, opts = {}) { + this._assertLive(); + return this.add(this._graph.derived(deps, fn, opts)); + } + effect(deps, fn, opts = {}) { + this._assertLive(); + return this.add(this._graph.effect(deps, fn, opts)); + } + initNode(op, deps, opts = {}) { + this._assertLive(); + return this.add(this._graph.initNode(op, deps, opts)); + } + release(opts = {}) { + if (this._released) return; + const lifecycle = lifecycleRegistrars.get(this._graph); + if (lifecycle === void 0) throw new Error("topologyGroup: graph lifecycle unavailable"); + lifecycle.releaseNodes([...this._members], { + reason: opts.reason ?? this.name + }); + this._members.length = 0; + this._released = true; + } + _assertLive() { + if (this._released) { + throw new Error(`topology group '${this.name ?? "group"}' has been released (D152)`); + } + } +}; + +// src/graph/operators.ts +function initNodeWithCore(core, op, deps, opts = {}) { + return withNodeCore(core, () => makeInitNode(op, deps, opts)); +} +function makeInitNode(op, deps, opts) { + const body = operatorNodeFn(op); + return new Node([...deps], body, { factory: op.factory, ...op.opts, ...opts }); +} +function operatorNodeFn(op) { + return (ctx) => { + try { + op.body(ctx); + } catch (e) { + ctx.down([["ERROR", errorPayload(e, "operator threw without a valid error payload")]]); + } + }; +} + +// src/graph/graph.ts +function nodeOwner(n) { + return getNodeOwner(n); +} +function assertGraphLocalNode(owner, n, label) { + if (isNodeRuntimeReleased(n)) { + throw new Error(`${label} has been released from its graph lifecycle (D122)`); + } + const existing = nodeOwner(n); + if (existing !== void 0 && existing !== owner) { + throw new Error( + `${label} belongs to a different graph; cross-graph deps require a wire bridge` + ); + } +} +var StateNode = class extends Node { + set(v) { + this.down([["DATA", v]]); + } +}; +var Graph = class { + name; + _dispatcher; + _versioning; + _environment; + _core = new NodeCore(); + _entries = /* @__PURE__ */ new Map(); + _byId = /* @__PURE__ */ new Map(); + _retiredIds = /* @__PURE__ */ new Set(); + _mounts = []; + _seq = 0; + _clock = 0; + // graph-local monotonic clock for observe seq (D26) + _topologyObserverSeq = 0; + _topologyObservers = /* @__PURE__ */ new Map(); + _topologyDelivering = false; + _topologyQueue = []; + // D51: stable synthetic ids for unregistered live deps (runtime *Map inners) auto-discovered by + // describe(). WeakMap-cached so successive describes agree + the inner's id is freed when it is. + // A dedicated counter (NOT _seq) so a describe() call never perturbs registered-node id numbering. + _synthSeq = 0; + _synthIds = /* @__PURE__ */ new WeakMap(); + constructor(opts = {}) { + this.name = opts.name; + this._dispatcher = opts.dispatcher ?? defaultDispatcher; + this._versioning = opts.versioning; + this._environment = opts.environment ?? EnvironmentDrivers.empty(); + if (opts.profile) this._dispatcher.setRecording(true); + restoreRegistrars.set(this, { + stateNode: (id, stateOpts = {}) => { + const n = this._construct(() => new StateNode([], null, this._nodeOpts(stateOpts))); + this._addWithId(n, "state", [], stateOpts, id); + return n; + }, + node: (id, factory, deps, fn, nodeOpts = {}) => { + this._assertDepsLocal(deps, `dep of restored '${id}'`); + const n = this._construct(() => new Node([...deps], fn, this._nodeOpts(nodeOpts))); + this._addWithId(n, factory, deps, nodeOpts, id); + return n; + } + }); + lifecycleRegistrars.set(this, { + assertRegisteredNode: (node, label) => this._assertRegisteredNode(node, label), + releaseNodes: (nodes, releaseOpts) => this._releaseNodes(nodes, releaseOpts) + }); + } + // ── registration / inspection index ── + _add(n, factory, deps, opts) { + const id = opts.name ?? `${factory}#${this._seq++}`; + return this._addWithId(n, factory, deps, opts, id); + } + _addWithId(n, factory, deps, opts, id) { + assertGraphLocalNode(this, n, `graph node '${opts.name ?? factory}'`); + for (const dep of deps) assertGraphLocalNode(this, dep, `dep of '${opts.name ?? factory}'`); + if (this._byId.has(id)) { + throw new Error(`graph: duplicate node id '${id}' (checkpoint/describe ids must be unique)`); + } + if (this._retiredIds.has(id)) { + throw new Error(`graph: node id '${id}' was released and cannot be reused (D152/D153)`); + } + const meta = opts.meta === void 0 ? void 0 : normalizeTopologyMeta(opts.meta, `graph node '${id}' meta`); + this._entries.set(n, { + node: n, + id, + name: opts.name, + factory, + deps, + meta, + restore: opts.restore + }); + setNodeOwner(n, this); + this._byId.set(id, n); + setNodeTopologyDepsChangedObserver(n, (_node, prevDeps, nextDeps) => { + this._emitTopologyDepsChanged(n, prevDeps, nextDeps); + }); + const seqMatch = /#(\d+)$/.exec(id); + if (seqMatch) this._seq = Math.max(this._seq, Number(seqMatch[1]) + 1); + this._emitTopologyNodeRegistered(n); + return n; + } + _assertDepsLocal(deps, label) { + for (const dep of deps) assertGraphLocalNode(this, dep, label); + } + _assertRegisteredNode(node, label) { + assertGraphLocalNode(this, node, label); + if (!this._entries.has(node)) { + throw new Error(`${label} is not a registered graph node (D152)`); + } + } + _nodeOpts(opts) { + if (opts.meta !== void 0) { + normalizeTopologyMeta(opts.meta, `graph node '${opts.name ?? opts.factory ?? "node"}' meta`); + } + const { name: _n, meta: _m, restore: _r, ...rest } = opts; + return { + ...rest, + versioning: rest.versioning ?? this._versioning, + dispatcher: this._dispatcher + }; + } + _construct(create) { + return withEnvironmentDrivers(this._environment, () => withNodeCore(this._core, create)); + } + /** Look up a registered node by its id. */ + find(id) { + const local = this._byId.get(id); + if (local !== void 0) return local; + const separator = id.indexOf("::"); + if (separator < 0) return void 0; + const mountPath = id.slice(0, separator); + const childPath = id.slice(separator + 2); + return this._mounts.find((mount) => mount.at === mountPath)?.graph.find(childPath); + } + _releaseNodes(nodes, _opts = {}) { + const seen = /* @__PURE__ */ new Set(); + const entries = []; + for (const node of nodes) { + if (seen.has(node)) continue; + seen.add(node); + const entry = this._entries.get(node); + if (entry === void 0) continue; + entries.push({ node, entry }); + } + const releaseSet = new Set(entries.map(({ node }) => node)); + const releaseIds = new Map(entries.map(({ node, entry }) => [node, entry.id])); + for (const entry of this._entries.values()) { + if (releaseSet.has(entry.node)) continue; + for (const dep of entry.node.deps) { + if (!releaseSet.has(dep)) continue; + const depId = releaseIds.get(dep) ?? dep.name ?? dep.factory ?? "released node"; + throw new Error( + `graph: cannot release node group; '${entry.id}' still depends on '${depId}' (D122)` + ); + } + } + for (const { node, entry } of entries) { + if (!isNodeRuntimeQuiescentForRelease(node)) { + throw new Error( + `graph: cannot release node group; '${entry.id}' is not runtime-quiescent (D124)` + ); + } + let internalSubscribers = 0; + for (const { node: dependent } of entries) { + if (dependent === node || !isNodeActiveForRelease(dependent)) continue; + for (const dep of dependent.deps) { + if (dep === node) internalSubscribers += 1; + } + } + if (subscriberCountOfNode(node) > internalSubscribers) { + throw new Error( + `graph: cannot release node group; '${entry.id}' still has live subscribers (D124)` + ); + } + } + const releasedEvents = this._topologyObservers.size === 0 ? [] : entries.map(({ node, entry }) => ({ + path: entry.id, + factory: entry.factory, + deps: this._topologyDeps(node.deps) + })); + for (const { node, entry } of entries) { + this._entries.delete(node); + this._byId.delete(entry.id); + this._retiredIds.add(entry.id); + } + let releaseError; + for (const { node } of entries) { + try { + releaseRuntimeOfNode(node); + } catch (error) { + if (releaseError === void 0) releaseError = error; + } + } + for (const event of releasedEvents) this._emitTopologyNodeReleased(event); + if (releaseError !== void 0) throw releaseError; + } + // ── 8 verbs (core: node/state/batch + sugar: producer/derived/effect/mount) ── + /** ctx-level power surface: a raw `(ctx)=>void` fn (or a passthrough/state when null). */ + node(deps = [], fn = null, opts = {}) { + this._assertDepsLocal(deps, `dep of '${opts.name ?? "node"}'`); + const n = this._construct(() => new Node(deps, fn, this._nodeOpts(opts))); + return this._add(n, opts.factory ?? "node", deps, opts); + } + /** A manual source with `.set(v)` (L4-Q1). */ + state(initial, opts = {}) { + const n = this._construct( + () => new StateNode([], null, { ...this._nodeOpts(opts), initial }) + ); + this._add(n, "state", [], opts); + return n; + } + /** ctx-level depless source; its fn runs on activation (R-rom-ram). */ + producer(fn, opts = {}) { + const n = this._construct(() => new Node([], fn, this._nodeOpts(opts))); + return this._add(n, "producer", [], opts); + } + /** value-level pure transform: deps → value (D27 wrapped; D30 throw→ERROR). */ + derived(deps, fn, opts = {}) { + this._assertDepsLocal(deps, `dep of '${opts.name ?? "derived"}'`); + const ctxFn = (ctx) => { + try { + const args = Array.from( + { length: depCount(ctx) }, + (_, i) => depLatest(ctx, i) + ); + const result = fn(...args); + if (result !== void 0) ctx.down([["DATA", result]]); + } catch (e) { + ctx.down([["ERROR", errorPayload(e, "derived threw without a valid error payload")]]); + } + }; + const n = this._construct(() => new Node([...deps], ctxFn, this._nodeOpts(opts))); + return this._add(n, "derived", deps, opts); + } + /** value-level sink: deps → effect; return value (a fn) becomes onDeactivation (D28). */ + effect(deps, fn, opts = {}) { + this._assertDepsLocal(deps, `dep of '${opts.name ?? "effect"}'`); + const ctxFn = (ctx) => { + try { + const args = Array.from( + { length: depCount(ctx) }, + (_, i) => depLatest(ctx, i) + ); + const cleanup = fn(...args); + if (typeof cleanup === "function") ctx.onDeactivation(cleanup); + } catch (e) { + ctx.down([["ERROR", errorPayload(e, "effect threw without a valid error payload")]]); + } + }; + const n = this._construct(() => new Node([...deps], ctxFn, this._nodeOpts(opts))); + return this._add(n, "effect", deps, opts); + } + /** Declarative batch (D12): one wave, success→commit / throw→rollback. */ + batch(fn) { + return batch(fn); + } + /** Embed a child graph addressable under `at` (R-mount; mount has no deps). */ + mount(child, opts) { + if (typeof opts.at !== "string" || opts.at.length === 0) { + throw new TypeError("graph.mount: at must be a non-empty string"); + } + if (this._mounts.some((mounted) => mounted.at === opts.at)) { + throw new Error(`graph.mount: duplicate sibling mount id '${opts.at}'`); + } + const mount = { at: opts.at, graph: child }; + this._mounts.push(mount); + if (this._topologyObservers.size > 0) this._ensureMountedTopologyForwarders(); + this._emitTopologyMountChanged(opts.at); + } + // ── operator funnel (D43): instantiate any free-standing Operator (node sugar, D6/L1.5) ── + // g.initNode is the single graph-bound entry for the whole operator/source catalog (D40): + // it delegates to the FREE initNode (graph/operators.ts — the D30 throw→ERROR boundary + + // dispatcher binding live there) and records the operator's REAL factory name in the + // inspection index (_add) so describe shows it (D6/R-describe) while the node stays thin + // (R-node-thin). Operators are free-standing factory definitions (graph/operators.ts, + // graph/sources.ts) usable bare via the free initNode(); this funnel is the inspectable + // path. Replaces the per-operator methods. + /** + * Instantiate an operator (or source) factory as a registered graph node. `deps` are + * type-checked against the operator's input element type; the output type flows from the + * operator. A source is a depless operator — pass `[]`. Caller `opts` (name/meta/behavioral + * overrides) win over the operator's baked-in `opts`. + */ + initNode(op, deps, opts = {}) { + const entryOpts = op.restore !== void 0 && !("restore" in opts) ? { ...opts, restore: op.restore } : opts; + for (const dep of deps) + assertGraphLocalNode(this, dep, `dep of '${entryOpts.name ?? op.factory}'`); + const erased = deps; + const n = withEnvironmentDrivers( + this._environment, + () => initNodeWithCore(this._core, op, erased, this._nodeOpts(entryOpts)) + ); + return this._add(n, op.factory, erased, entryOpts); + } + /** + * Graph-owned activation root for internal helper nodes. This is the sanctioned keepalive shape: + * a graph, not a helper closure, owns the subscription and returns the release handle. + */ + retain(node, opts = {}) { + assertGraphLocalNode(this, node, opts.reason ?? "retained node"); + return node.subscribe(() => { + }); + } + /** + * D152 graph-owned topology/release group. Members are ordinary registered graph nodes; + * release is quiescent-only and removes ids atomically without synthesizing protocol messages. + */ + topologyGroup(opts = {}) { + return new GraphTopologyGroup(this, opts); + } + // ── inspection: describe / observe / profile (D39) ── + /** Live point-in-time structure snapshot (R-describe / D39 / D51). `_prefix` carries the mount path. */ + describe(opts = {}, _prefix = "") { + const discovered = /* @__PURE__ */ new Map(); + const localId = (n) => { + const e = this._entries.get(n); + if (e) return `${_prefix}${e.id}`; + let sid = this._synthIds.get(n); + if (sid === void 0) { + do { + sid = `~${n.factory ?? "?"}#${this._synthSeq++}`; + } while (this._byId.has(sid)); + this._synthIds.set(n, sid); + } + discovered.set(n, sid); + return `${_prefix}${sid}`; + }; + const nodes = []; + const edges = []; + for (const entry of this._entries.values()) { + const id = `${_prefix}${entry.id}`; + const liveIds = entry.node.deps.map(localId); + const dnode = { + id, + factory: entry.factory, + status: entry.node.status, + deps: liveIds + }; + if (entry.name !== void 0) dnode.name = entry.name; + if (entry.node.cache !== void 0) dnode.value = entry.node.cache; + if (entry.node.version !== void 0) dnode.version = entry.node.version; + if (entry.meta !== void 0) dnode.meta = entry.meta; + nodes.push(dnode); + for (const from of liveIds) edges.push({ from, to: id }); + } + const visited = /* @__PURE__ */ new Set(); + const queue = [...discovered.keys()]; + for (let i = 0; i < queue.length; i += 1) { + const inner = queue[i]; + if (visited.has(inner)) continue; + visited.add(inner); + const sid = discovered.get(inner); + if (sid === void 0) continue; + const liveIds = inner.deps.map(localId); + for (const dep of inner.deps) { + if (!this._entries.has(dep) && !visited.has(dep)) queue.push(dep); + } + const dnode = { + id: `${_prefix}${sid}`, + factory: inner.factory ?? "?", + status: inner.status, + deps: liveIds + }; + if (inner.cache !== void 0) dnode.value = inner.cache; + if (inner.version !== void 0) dnode.version = inner.version; + nodes.push(dnode); + for (const from of liveIds) edges.push({ from, to: dnode.id }); + } + const snap = { nodes, edges }; + if (this.name !== void 0) snap.name = this.name; + if (this._mounts.length > 0) { + snap.subgraphs = this._mounts.map((m) => { + const child = m.graph.describe({}, `${_prefix}${m.at}::`); + child.mountId = m.at; + return child; + }); + } + return opts.explain ? explainSubset(snap, opts.explain) : snap; + } + /** + * D173 pure-structure topology snapshot over the same live truth source as describe(). + * Runtime status/value/version remain on describe(); blueprint metadata is a later envelope. + */ + topology() { + return topologyFromDescribe(this.describe()); + } + /** + * D177 synchronous audit/collaboration envelope over the pure topology snapshot. + * Hashing and environment provenance enrichment stay in pure helpers outside Graph core. + */ + blueprint(opts = {}) { + const topology = normalizeTopology(this.topology()); + const out = { + version: GRAPH_BLUEPRINT_VERSION, + topology + }; + if (opts.diagnostics) { + out.diagnostics = graphBlueprintDiagnostics(topology); + } + if (opts.provenance !== void 0) { + out.provenance = normalizeTopologyMeta( + opts.provenance, + "graph blueprint provenance", + "provenance" + ); + } + return out; + } + _observeTargets(path) { + const all = [...this._entries.values()].map((e) => [ + e.id, + e.node + ]); + if (path === void 0) return all; + const exact = this._byId.get(path); + if (exact) return [[path, exact]]; + return all.filter(([id]) => id.startsWith(`${path}::`)); + } + /** + * observe(path?) = read-only enveloped EGRESS (R-observe / D39). NOT a graph node — it + * taps the target node(s) via subscribe and forwards each Message as an ObserveEvent. + * No path = whole graph; an exact id = a single node; otherwise a `::`-prefix subtree. + */ + // NOTE (R-observe/D19): observe is a real, lazily-ACTIVATING subscriber — observing a + // cold node runs its fn (and activates upstream); whole-graph observe() activates the + // graph. "Read-only" means it never emits/mutates node state, NOT that it avoids + // activation. Use it knowing inspection of a cold graph wakes it. + observe(path) { + const targets = this._observeTargets(path); + return { + subscribe: (sink) => { + const unsubs = targets.map( + ([id, n]) => n.subscribe((msg) => { + sink({ path: id, msg, tier: messageTier(msg[0]), seq: this._clock++ }); + }) + ); + return () => { + for (const u of unsubs) u(); + }; + } + }; + } + /** + * D145 observeTopology(path?) = read-only graph lifecycle egress over the existing + * graph registry. It is not a graph node, does not subscribe to nodes, and emits no DATA. + */ + observeTopology(path) { + return { + subscribe: (sink) => { + const id = this._topologyObserverSeq++; + this._topologyObservers.set(id, { path, sink }); + this._ensureMountedTopologyForwarders(); + return () => { + this._topologyObservers.delete(id); + if (this._topologyObservers.size === 0) this._releaseMountedTopologyForwarders(); + }; + } + }; + } + _idForTopologyNode(n) { + const e = this._entries.get(n); + if (e) return e.id; + let sid = this._synthIds.get(n); + if (sid === void 0) { + do { + sid = `~${n.factory ?? "?"}#${this._synthSeq++}`; + } while (this._byId.has(sid)); + this._synthIds.set(n, sid); + } + return sid; + } + _topologyDeps(deps) { + return deps.map((dep) => this._idForTopologyNode(dep)); + } + _emitTopologyNodeRegistered(node) { + if (this._topologyObservers.size === 0) return; + const entry = this._entries.get(node); + if (entry === void 0) return; + this._emitTopologyEvent({ + kind: "node-registered", + path: entry.id, + factory: entry.factory, + deps: this._topologyDeps(node.deps), + seq: this._clock++ + }); + } + _emitTopologyDepsChanged(node, prevDeps, nextDeps) { + if (this._topologyObservers.size === 0) return; + const entry = this._entries.get(node); + if (entry === void 0) return; + this._emitTopologyEvent({ + kind: "deps-changed", + path: entry.id, + prevDeps: this._topologyDeps(prevDeps), + deps: this._topologyDeps(nextDeps), + seq: this._clock++ + }); + } + _emitTopologyNodeReleased(event) { + if (this._topologyObservers.size === 0) return; + this._emitTopologyEvent({ + kind: "node-released", + path: event.path, + factory: event.factory, + deps: [...event.deps], + seq: this._clock++ + }); + } + _emitTopologyMountChanged(path) { + if (this._topologyObservers.size === 0) return; + this._emitTopologyEvent({ + kind: "mount-changed", + path, + factory: "mount", + deps: [], + seq: this._clock++ + }); + } + _ensureMountedTopologyForwarders() { + if (this._topologyObservers.size === 0) return; + for (const mount of this._mounts) { + if (mount.topologyUnsub !== void 0) continue; + const mountPath = mount.at; + mount.topologyUnsub = mount.graph.observeTopology().subscribe((event) => { + this._emitMountedTopologyEvent(mountPath, event); + }); + } + } + _releaseMountedTopologyForwarders() { + for (const mount of this._mounts) { + mount.topologyUnsub?.(); + mount.topologyUnsub = void 0; + } + } + _emitMountedTopologyEvent(mountPath, event) { + if (this._topologyObservers.size === 0) return; + this._emitTopologyEvent({ + kind: event.kind, + path: prefixTopologyPath(mountPath, event.path), + deps: event.deps.map((dep) => prefixTopologyPath(mountPath, dep)), + ...event.prevDeps !== void 0 ? { prevDeps: event.prevDeps.map((dep) => prefixTopologyPath(mountPath, dep)) } : {}, + ...event.factory !== void 0 ? { factory: event.factory } : {}, + seq: this._clock++ + }); + } + _emitTopologyEvent(event) { + if (this._topologyDelivering) { + this._topologyQueue.push(event); + return; + } + this._topologyDelivering = true; + try { + let current = event; + while (current !== void 0) { + const observers = [...this._topologyObservers.entries()]; + for (const [id, observer] of observers) { + if (this._topologyObservers.get(id) !== observer) continue; + if (observer.path !== void 0 && !topologyPathMatches(current.path, observer.path)) + continue; + try { + observer.sink(cloneTopologyEvent(current)); + } catch { + } + } + current = this._topologyQueue.shift(); + } + } finally { + this._topologyDelivering = false; + } + } + /** + * profile() = accumulated-counter snapshot (R-profile / D39). invokes + duration are + * dispatcher-backed (the invoke funnel, F-DISPATCH-ALL) — counters never live on the + * thin node (R-node-thin). Requires `graph({ profile: true })` (opt-in, F-PERF). + */ + profile() { + const nodes = {}; + let totalInvokes = 0; + for (const e of this._entries.values()) { + const h = e.node.handle; + const stat = h ? this._dispatcher.statFor(h) : void 0; + const invokes = stat?.invokes ?? 0; + nodes[e.id] = { + invokes, + totalDurationNs: stat?.totalDurationNs ?? 0, + lastDurationNs: stat?.lastDurationNs ?? 0, + status: e.node.status + }; + totalInvokes += invokes; + } + return { totalInvokes, nodes }; + } + /** Versioned graph lifecycle checkpoint (R-snapshot / D83 / D90). Pure capture, no storage I/O. */ + checkpoint() { + return this._checkpoint("", /* @__PURE__ */ new WeakSet()); + } + _checkpoint(_prefix, stack) { + if (stack.has(this)) { + throw new Error("checkpoint: cyclic graph mount detected"); + } + stack.add(this); + const discovered = /* @__PURE__ */ new Map(); + const localId = (n) => { + const e = this._entries.get(n); + if (e) return `${_prefix}${e.id}`; + let sid = this._synthIds.get(n); + if (sid === void 0) { + do { + sid = `~${n.factory ?? "?"}#${this._synthSeq++}`; + } while (this._byId.has(sid)); + this._synthIds.set(n, sid); + } + discovered.set(n, sid); + return `${_prefix}${sid}`; + }; + const nodes = []; + const edges = []; + for (const entry of this._entries.values()) { + const id = `${_prefix}${entry.id}`; + const liveIds = entry.node.deps.map(localId); + nodes.push( + this._checkpointNode(entry.node, id, { + name: entry.name, + factory: checkpointFactory(entry.factory, entry.node, false, entry.restore, entry.meta), + deps: liveIds, + meta: entry.meta + }) + ); + for (const from of liveIds) edges.push({ from, to: id }); + } + const visited = /* @__PURE__ */ new Set(); + const queue = [...discovered.keys()]; + for (let i = 0; i < queue.length; i += 1) { + const inner = queue[i]; + if (visited.has(inner)) continue; + visited.add(inner); + const sid = discovered.get(inner); + if (sid === void 0) continue; + const liveIds = inner.deps.map(localId); + for (const dep of inner.deps) { + if (!this._entries.has(dep) && !visited.has(dep)) queue.push(dep); + } + const id = `${_prefix}${sid}`; + nodes.push( + this._checkpointNode(inner, id, { + factory: checkpointFactory(inner.factory ?? "?", inner, true), + deps: liveIds + }) + ); + for (const from of liveIds) edges.push({ from, to: id }); + } + const checkpoint = { + version: GRAPH_CHECKPOINT_VERSION, + nodes, + edges + }; + if (this.name !== void 0) checkpoint.name = this.name; + if (this._mounts.length > 0) { + checkpoint.mounts = this._mounts.map((m) => ({ + at: m.at, + checkpoint: m.graph._checkpoint("", stack) + })); + } + stack.delete(this); + return toCheckpointJson(checkpoint, "checkpoint"); + } + _checkpointNode(node, id, opts) { + const state = checkpointStateOfNode(node); + assertCheckpointQuiescentStatus(node.status, id, "checkpoint"); + const nonAuthoritativeCollectionHelper = isNonAuthoritativeCollectionHelperMeta(opts.meta); + const out = { + id, + factory: opts.factory, + status: node.status, + deps: opts.deps, + value: nonAuthoritativeCollectionHelper ? { kind: "SENTINEL" } : checkpointValue(state.cache, state.hasData, `${id}.value`), + terminal: checkpointTerminal(state.terminal, `${id}.terminal`), + lifecycle: { activated: state.activated, hasCalledFnOnce: state.hasCalledFnOnce }, + ctxState: { + persist: state.ctxState.persist, + value: nonAuthoritativeCollectionHelper ? { kind: "SENTINEL" } : checkpointValue( + state.ctxState.value, + state.ctxState.value !== SENTINEL, + `${id}.ctxState` + ) + } + }; + if (opts.name !== void 0) out.name = opts.name; + const backendState = checkpointBackendStateOfNode(node, `${id}.backendState`); + if (backendState !== void 0) out.backendState = backendState; + if (state.version !== void 0) out.version = state.version; + if (opts.meta !== void 0) + out.meta = toCheckpointJson(opts.meta, `${id}.meta`); + return out; + } +}; +function graph(opts = {}) { + return new Graph(opts); +} + +// runners/local-untrusted-js/runner.ts +var COMPATIBILITY_REVISION = "graphrefly-local-untrusted-js-compute-v1"; +var RUNNER_API_REVISION = "graphrefly-runner-api-v1"; +var INPUT_PATHS = ["/input/bundle.mjs", "/input/input.json", "/input/control.json"]; +var MAX_RUNNER_NODES = 1e3; +var MAX_RUNNER_EDGES = 2e3; +var SAFE_NAME = /^[A-Za-z][A-Za-z0-9._:-]{0,127}$/; +var PRELUDE = ` +(() => { + "use strict"; + const stringify = JSON.stringify.bind(JSON); + const parse = JSON.parse.bind(JSON); + const freeze = Object.freeze.bind(Object); + const keys = Object.keys.bind(Object); + const hasOwn = Object.prototype.hasOwnProperty.call.bind(Object.prototype.hasOwnProperty); + const nodeHandle = Symbol("graphrefly-runner-node"); + const safeName = /^[A-Za-z][A-Za-z0-9._:-]{0,127}$/; + const computeById = Object.create(null); + let declared = false; + + const cloneJson = (value, label) => { + let encoded; + try { + encoded = stringify(value); + } catch { + throw new TypeError(label + " must be JSON data."); + } + if (encoded === undefined) throw new TypeError(label + " must be JSON data."); + return parse(encoded); + }; + const deepFreeze = (value) => { + if (value !== null && typeof value === "object") { + for (const key of keys(value)) deepFreeze(value[key]); + freeze(value); + } + return value; + }; + const exactNode = (value, byId) => { + if ( + value === null || + typeof value !== "object" || + value[nodeHandle] !== true || + typeof value.id !== "string" || + !hasOwn(byId, value.id) + ) throw new TypeError("GraphReFly dependency must be a node created by this run."); + return value; + }; + const name = (value, label) => { + if (typeof value !== "string" || !safeName.test(value) || value.includes("::")) + throw new TypeError(label + " must be a safe unique name."); + return value; + }; + + const declare = async (main, admittedInputJson) => { + if (declared) throw new TypeError("The GraphReFly runner accepts one declaration."); + declared = true; + if (typeof main !== "function") + throw new TypeError("The admitted bundle must export one default function."); + if (typeof admittedInputJson !== "string") + throw new TypeError("Admitted input must cross as canonical JSON text."); + const admittedInput = parse(admittedInputJson); + const nodes = []; + const byId = Object.create(null); + const names = new Set(); + let graphName = "local-code-workgraph"; + let graphDeclared = false; + let sequence = 0; + const add = (kind, nodeName, deps, value, meta, compute) => { + const safeNodeName = name(nodeName, "Node name"); + if (names.has(safeNodeName)) throw new TypeError("GraphReFly node names must be unique."); + names.add(safeNodeName); + const id = "node-" + (++sequence); + const plan = { + id, + kind, + name: safeNodeName, + deps: deps.map((dep) => exactNode(dep, byId).id), + ...(kind === "source" ? { value: cloneJson(value, "Source value") } : {}), + ...(meta === undefined ? {} : { meta: cloneJson(meta, "Node metadata") }), + }; + nodes.push(plan); + if (kind === "derived") computeById[id] = compute; + const handle = freeze({ + [nodeHandle]: true, + id, + ...(kind === "source" + ? { value: deepFreeze(cloneJson(plan.value, "Source value")) } + : {}), + }); + byId[id] = handle; + return handle; + }; + const api = freeze({ + graph(value) { + if (graphDeclared) throw new TypeError("A run may declare only one GraphReFly graph."); + graphDeclared = true; + graphName = name(value, "Graph name"); + return graphName; + }, + source(nodeName, value, meta) { + return add("source", nodeName, [], value, meta); + }, + derive(nodeName, deps, compute, meta) { + if (!Array.isArray(deps) || typeof compute !== "function") + throw new TypeError("GraphReFly derive needs node dependencies and a compute function."); + const exactDeps = deps.map((dep) => exactNode(dep, byId)); + return add("derived", nodeName, exactDeps, undefined, meta, compute); + }, + value(node) { + const exact = exactNode(node, byId); + if (!hasOwn(exact, "value")) + throw new TypeError("A derived value is available only from the actual Graph runtime."); + return exact.value; + }, + }); + const input = deepFreeze(cloneJson(admittedInput, "Admitted input")); + const returned = exactNode(await main(freeze({ graphrefly: api, input })), byId); + return stringify({ + graphName, + answerNodeId: returned.id, + nodes, + }); + }; + const compute = (nodeId, dependencyValuesJson) => { + const computeFn = + typeof nodeId === "string" && hasOwn(computeById, nodeId) + ? computeById[nodeId] + : undefined; + if ( + typeof nodeId !== "string" || + typeof computeFn !== "function" || + typeof dependencyValuesJson !== "string" + ) throw new TypeError("GraphReFly derived computation is unavailable."); + const dependencyValues = deepFreeze( + cloneJson(parse(dependencyValuesJson), "Derived dependency values"), + ); + if (!Array.isArray(dependencyValues)) + throw new TypeError("Derived dependency values must be an array."); + const value = Reflect.apply(computeFn, undefined, dependencyValues); + if (value !== null && (typeof value === "object" || typeof value === "function")) { + if (typeof value.then === "function") + throw new TypeError("GraphReFly derive must be synchronous."); + } + return stringify(cloneJson(value, "Derived value")); + }; + const controller = freeze({ declare, compute }); + for (const intrinsic of [ + Object, + Array, + Function, + Promise, + Map, + Set, + WeakMap, + WeakSet, + RegExp, + Date, + Error, + TypeError, + Number, + String, + Boolean, + Symbol, + BigInt, + JSON, + Math, + Reflect, + ]) { + if (intrinsic && intrinsic.prototype) freeze(intrinsic.prototype); + freeze(intrinsic); + } + return controller; +})(); +`; +function record(value) { + if (value === null || typeof value !== "object" || Array.isArray(value)) return false; + const proto = Object.getPrototypeOf(value); + return proto === Object.prototype || proto === null; +} +function exactKeys(value, expected, label) { + const observed = Object.keys(value).sort(); + const wanted = [...expected].sort(); + if (observed.length !== wanted.length || observed.some((key, index) => key !== wanted[index])) + throw new TypeError(`${label} has an invalid shape.`); +} +function safeString(value, label) { + if (typeof value !== "string" || value.length === 0 || value.length > 512) + throw new TypeError(`${label} is invalid.`); + return value; +} +function digest(value, label) { + const text = safeString(value, label); + if (!/^sha256:[a-f0-9]{64}$/.test(text)) throw new TypeError(`${label} is invalid.`); + return text; +} +function parseControl(value) { + if (!record(value)) throw new TypeError("Runner control must be an object."); + exactKeys( + value, + [ + "contractVersion", + "compatibilityRevision", + "runnerApiRevision", + "manifestFingerprint", + "args", + "runAdmissionId" + ], + "Runner control" + ); + if (value.contractVersion !== "1" || value.compatibilityRevision !== COMPATIBILITY_REVISION || value.runnerApiRevision !== RUNNER_API_REVISION) + throw new TypeError("Runner control identity is invalid."); + safeString(value.manifestFingerprint, "Manifest fingerprint"); + safeString(value.runAdmissionId, "Run admission id"); + if (!record(value.args)) throw new TypeError("Runner arguments must be an object."); + const args = value.args; + for (const key of [ + "runId", + "sourceRevision", + "bundleRevision", + "compilerRevision", + "allowedApiRevision", + "graphreflyPackageRevision", + "runnerRevision" + ]) + safeString(args[key], key); + for (const key of ["sourceDigest", "bundleDigest", "runnerImageDigest", "inputDigest"]) + digest(args[key], key); + if (args.contractVersion !== "1" || !Number.isSafeInteger(args.attempt) || args.attempt < 1 || !Array.isArray(args.admittedInputRefs) || args.admittedInputRefs.length === 0 || args.admittedInputRefs.some((entry) => typeof entry !== "string" || entry.length === 0)) + throw new TypeError("Runner arguments are invalid."); + return value; +} +function parseJson(bytes, label) { + try { + return JSON.parse(new TextDecoder().decode(bytes)); + } catch { + throw new TypeError(`${label} must be valid JSON.`); + } +} +function parsePlan(value) { + if (!record(value)) throw new TypeError("Runner plan must be an object."); + exactKeys(value, ["graphName", "answerNodeId", "nodes"], "Runner plan"); + if (typeof value.graphName !== "string" || !SAFE_NAME.test(value.graphName) || value.graphName.includes("::") || typeof value.answerNodeId !== "string" || !Array.isArray(value.nodes) || value.nodes.length === 0 || value.nodes.length > MAX_RUNNER_NODES) + throw new TypeError("Runner plan identity or node bound is invalid."); + const ids = /* @__PURE__ */ new Set(); + const names = /* @__PURE__ */ new Set(); + let edgeCount = 0; + const nodes = value.nodes.map((entry) => { + if (!record(entry)) throw new TypeError("Runner plan node must be an object."); + const expected = entry.kind === "source" ? entry.meta === void 0 ? ["id", "kind", "name", "deps", "value"] : ["id", "kind", "name", "deps", "value", "meta"] : entry.meta === void 0 ? ["id", "kind", "name", "deps"] : ["id", "kind", "name", "deps", "meta"]; + exactKeys(entry, expected, "Runner plan node"); + if (typeof entry.id !== "string" || !/^node-[1-9][0-9]*$/.test(entry.id) || ids.has(entry.id) || entry.kind !== "source" && entry.kind !== "derived" || typeof entry.name !== "string" || !SAFE_NAME.test(entry.name) || entry.name.includes("::") || names.has(entry.name) || !Array.isArray(entry.deps) || entry.deps.some((dep) => typeof dep !== "string" || !ids.has(dep)) || entry.kind === "source" && entry.deps.length !== 0 || entry.kind === "derived" && entry.deps.length === 0) + throw new TypeError("Runner plan node identity or dependency is invalid."); + ids.add(entry.id); + names.add(entry.name); + edgeCount += entry.deps.length; + if (edgeCount > MAX_RUNNER_EDGES) throw new TypeError("Runner plan edge bound is invalid."); + const canonical = JSON.parse( + new TextDecoder().decode(strictCanonicalJsonBytes(entry)) + ); + return canonical; + }); + if (!ids.has(value.answerNodeId)) + throw new TypeError("Runner answer must identify a node created by this run."); + return { graphName: value.graphName, answerNodeId: value.answerNodeId, nodes }; +} +async function evaluateBundle(bundleSource, input) { + const context = createContext(/* @__PURE__ */ Object.create(null), { + codeGeneration: { strings: false, wasm: false }, + name: "graphrefly-local-untrusted-js" + }); + const controller = new Script(PRELUDE, { + filename: "graphrefly-runner-prelude.js" + }).runInContext(context); + const rejectDynamicImport = new Script( + `() => Promise.reject( + new TypeError("Dynamic imports are not admitted by the GraphReFly runner API."), + )`, + { + filename: "graphrefly-runner-import-policy.js" + } + ).runInContext(context); + const userModule = new SourceTextModule(bundleSource, { + context, + identifier: "graphrefly:user-bundle", + initializeImportMeta(meta) { + Object.freeze(meta); + }, + importModuleDynamically() { + return rejectDynamicImport(); + } + }); + await userModule.link(() => { + throw new TypeError("Imports are not admitted by the GraphReFly runner API."); + }); + await userModule.evaluate(); + const main2 = Reflect.get(userModule.namespace, "default"); + const declare = Reflect.get(controller, "declare"); + const encoded = await Reflect.apply(declare, void 0, [ + main2, + new TextDecoder().decode(strictCanonicalJsonBytes(input)) + ]); + if (typeof encoded !== "string") throw new TypeError("Runner plan serialization failed."); + const plan = parsePlan(JSON.parse(encoded)); + const compute = Reflect.get(controller, "compute"); + return { + plan, + compute(nodeId, dependencyValues) { + const computed = Reflect.apply(compute, void 0, [ + nodeId, + new TextDecoder().decode(strictCanonicalJsonBytes(dependencyValues)) + ]); + if (typeof computed !== "string") + throw new TypeError("Runner derived result serialization failed."); + const value = JSON.parse(computed); + strictCanonicalJsonBytes(value); + return value; + } + }; +} +function materializeResult(evaluated, control) { + const { plan } = evaluated; + const runtimeGraph = graph({ name: plan.graphName }); + const group = runtimeGraph.topologyGroup({ name: "local-untrusted-js-runner" }); + const nodes = /* @__PURE__ */ new Map(); + const releases = []; + let result; + let failure; + let cleanupFailure; + try { + for (const node of plan.nodes) { + const opts = node.meta === void 0 ? { name: node.name } : { name: node.name, meta: node.meta }; + if (node.kind === "source") { + nodes.set(node.id, group.state(node.value, opts)); + continue; + } + const deps = node.deps.map((id) => { + const dep = nodes.get(id); + if (dep === void 0) throw new TypeError("Runner plan dependency is unavailable."); + return dep; + }); + const derived = group.derived( + deps, + (...dependencyValues) => evaluated.compute(node.id, dependencyValues), + opts + ); + nodes.set(node.id, derived); + } + const answerNode = nodes.get(plan.answerNodeId); + if (answerNode === void 0) throw new TypeError("Runner answer node is unavailable."); + const answerPlan = plan.nodes.find((node) => node.id === plan.answerNodeId); + if (answerPlan === void 0) throw new TypeError("Runner answer plan is unavailable."); + releases.push(runtimeGraph.retain(answerNode, { reason: "local untrusted JS runner answer" })); + if (answerNode.cache === void 0) + throw new TypeError("Runner answer node did not produce a Graph value."); + const answer = JSON.parse( + new TextDecoder().decode(strictCanonicalJsonBytes(answerNode.cache)) + ); + const topology = runtimeGraph.topology(); + const describe = runtimeGraph.describe(); + result = { + contractVersion: "1", + answer, + topology, + describe, + provenance: { + sourceRevision: control.args.sourceRevision, + sourceDigest: control.args.sourceDigest, + bundleRevision: control.args.bundleRevision, + bundleDigest: control.args.bundleDigest, + compilerRevision: control.args.compilerRevision, + allowedApiRevision: control.args.allowedApiRevision, + graphreflyPackageRevision: control.args.graphreflyPackageRevision, + runnerRevision: control.args.runnerRevision, + runnerImageDigest: control.args.runnerImageDigest, + manifestFingerprint: control.manifestFingerprint, + runId: control.args.runId, + attempt: control.args.attempt, + graphName: plan.graphName, + answerNodeId: answerPlan.name, + admittedInputRefs: [...control.args.admittedInputRefs], + inputDigest: control.args.inputDigest, + runAdmissionId: control.runAdmissionId + }, + cleanup: { + graphNodesAfterDispose: 0, + graphEdgesAfterDispose: 0 + } + }; + } catch (error) { + failure = error; + } finally { + for (const release of releases.reverse()) release(); + group.release({ reason: "local untrusted JS runner settled" }); + const after = runtimeGraph.topology(); + if (after.nodes.length !== 0 || after.edges.length !== 0 || after.subgraphs !== void 0) + cleanupFailure = new Error("Runner Graph did not dispose to 0N/0E."); + } + if (failure !== void 0) throw failure; + if (cleanupFailure !== void 0) throw cleanupFailure; + if (result === void 0) throw new Error("Runner result was not materialized."); + return result; +} +async function main() { + if (process.argv.length !== 5 || INPUT_PATHS.some((path, index) => process.argv[index + 2] !== path)) + throw new TypeError("Runner requires the fixed bundle, input and control paths."); + const [bundleBytes, inputBytes, controlBytes] = await Promise.all( + INPUT_PATHS.map((path) => readFile(path)) + ); + const control = parseControl(parseJson(controlBytes, "Runner control")); + const observedBundleDigest = `sha256:${createHash("sha256").update(bundleBytes).digest("hex")}`; + const observedInputDigest = `sha256:${createHash("sha256").update(strictCanonicalJsonBytes(parseJson(inputBytes, "Admitted input"))).digest("hex")}`; + if (observedBundleDigest !== control.args.bundleDigest || observedInputDigest !== control.args.inputDigest) + throw new TypeError("Runner material digest mismatch."); + const input = parseJson(inputBytes, "Admitted input"); + const evaluated = await evaluateBundle(new TextDecoder().decode(bundleBytes), input); + const result = materializeResult(evaluated, control); + process.stdout.write(strictCanonicalJsonBytes(result)); +} +main().catch((error) => { + const message = error instanceof Error ? error.message : "Local untrusted JS runner failed."; + process.stderr.write(`${message} +`); + process.exitCode = 1; +}); diff --git a/packages/ts/runners/local-untrusted-js/runner.ts b/packages/ts/runners/local-untrusted-js/runner.ts new file mode 100644 index 00000000..8398f81e --- /dev/null +++ b/packages/ts/runners/local-untrusted-js/runner.ts @@ -0,0 +1,554 @@ +import { createHash } from "node:crypto"; +import { readFile } from "node:fs/promises"; +import { createContext, Script, SourceTextModule } from "node:vm"; +import { graph } from "../../src/graph/graph.js"; +import { strictCanonicalJsonBytes } from "../../src/json/codec.js"; +import type { Node } from "../../src/node/node.js"; + +const COMPATIBILITY_REVISION = "graphrefly-local-untrusted-js-compute-v1"; +const RUNNER_API_REVISION = "graphrefly-runner-api-v1"; +const INPUT_PATHS = ["/input/bundle.mjs", "/input/input.json", "/input/control.json"] as const; +const MAX_RUNNER_NODES = 1_000; +const MAX_RUNNER_EDGES = 2_000; +const SAFE_NAME = /^[A-Za-z][A-Za-z0-9._:-]{0,127}$/; + +interface RunnerNodePlanBase { + readonly id: string; + readonly name: string; + readonly deps: readonly string[]; + readonly meta?: Record; +} + +interface RunnerSourceNodePlan extends RunnerNodePlanBase { + readonly kind: "source"; + readonly value: unknown; +} + +interface RunnerDerivedNodePlan extends RunnerNodePlanBase { + readonly kind: "derived"; +} + +type RunnerNodePlan = RunnerSourceNodePlan | RunnerDerivedNodePlan; + +interface RunnerPlan { + readonly graphName: string; + readonly answerNodeId: string; + readonly nodes: readonly RunnerNodePlan[]; +} + +interface EvaluatedBundle { + readonly plan: RunnerPlan; + compute(nodeId: string, dependencyValues: readonly unknown[]): unknown; +} + +interface SandboxRunnerController { + declare(main: unknown, admittedInputJson: string): Promise; + compute(nodeId: string, dependencyValuesJson: string): string; +} + +interface RunnerControl { + readonly contractVersion: "1"; + readonly compatibilityRevision: typeof COMPATIBILITY_REVISION; + readonly runnerApiRevision: typeof RUNNER_API_REVISION; + readonly manifestFingerprint: string; + readonly runAdmissionId: string; + readonly args: { + readonly contractVersion: "1"; + readonly runId: string; + readonly attempt: number; + readonly sourceRevision: string; + readonly sourceDigest: string; + readonly bundleRevision: string; + readonly bundleDigest: string; + readonly compilerRevision: string; + readonly allowedApiRevision: string; + readonly graphreflyPackageRevision: string; + readonly runnerRevision: string; + readonly runnerImageDigest: string; + readonly admittedInputRefs: readonly string[]; + readonly inputDigest: string; + }; +} + +const PRELUDE = ` +(() => { + "use strict"; + const stringify = JSON.stringify.bind(JSON); + const parse = JSON.parse.bind(JSON); + const freeze = Object.freeze.bind(Object); + const keys = Object.keys.bind(Object); + const hasOwn = Object.prototype.hasOwnProperty.call.bind(Object.prototype.hasOwnProperty); + const nodeHandle = Symbol("graphrefly-runner-node"); + const safeName = /^[A-Za-z][A-Za-z0-9._:-]{0,127}$/; + const computeById = Object.create(null); + let declared = false; + + const cloneJson = (value, label) => { + let encoded; + try { + encoded = stringify(value); + } catch { + throw new TypeError(label + " must be JSON data."); + } + if (encoded === undefined) throw new TypeError(label + " must be JSON data."); + return parse(encoded); + }; + const deepFreeze = (value) => { + if (value !== null && typeof value === "object") { + for (const key of keys(value)) deepFreeze(value[key]); + freeze(value); + } + return value; + }; + const exactNode = (value, byId) => { + if ( + value === null || + typeof value !== "object" || + value[nodeHandle] !== true || + typeof value.id !== "string" || + !hasOwn(byId, value.id) + ) throw new TypeError("GraphReFly dependency must be a node created by this run."); + return value; + }; + const name = (value, label) => { + if (typeof value !== "string" || !safeName.test(value) || value.includes("::")) + throw new TypeError(label + " must be a safe unique name."); + return value; + }; + + const declare = async (main, admittedInputJson) => { + if (declared) throw new TypeError("The GraphReFly runner accepts one declaration."); + declared = true; + if (typeof main !== "function") + throw new TypeError("The admitted bundle must export one default function."); + if (typeof admittedInputJson !== "string") + throw new TypeError("Admitted input must cross as canonical JSON text."); + const admittedInput = parse(admittedInputJson); + const nodes = []; + const byId = Object.create(null); + const names = new Set(); + let graphName = "local-code-workgraph"; + let graphDeclared = false; + let sequence = 0; + const add = (kind, nodeName, deps, value, meta, compute) => { + const safeNodeName = name(nodeName, "Node name"); + if (names.has(safeNodeName)) throw new TypeError("GraphReFly node names must be unique."); + names.add(safeNodeName); + const id = "node-" + (++sequence); + const plan = { + id, + kind, + name: safeNodeName, + deps: deps.map((dep) => exactNode(dep, byId).id), + ...(kind === "source" ? { value: cloneJson(value, "Source value") } : {}), + ...(meta === undefined ? {} : { meta: cloneJson(meta, "Node metadata") }), + }; + nodes.push(plan); + if (kind === "derived") computeById[id] = compute; + const handle = freeze({ + [nodeHandle]: true, + id, + ...(kind === "source" + ? { value: deepFreeze(cloneJson(plan.value, "Source value")) } + : {}), + }); + byId[id] = handle; + return handle; + }; + const api = freeze({ + graph(value) { + if (graphDeclared) throw new TypeError("A run may declare only one GraphReFly graph."); + graphDeclared = true; + graphName = name(value, "Graph name"); + return graphName; + }, + source(nodeName, value, meta) { + return add("source", nodeName, [], value, meta); + }, + derive(nodeName, deps, compute, meta) { + if (!Array.isArray(deps) || typeof compute !== "function") + throw new TypeError("GraphReFly derive needs node dependencies and a compute function."); + const exactDeps = deps.map((dep) => exactNode(dep, byId)); + return add("derived", nodeName, exactDeps, undefined, meta, compute); + }, + value(node) { + const exact = exactNode(node, byId); + if (!hasOwn(exact, "value")) + throw new TypeError("A derived value is available only from the actual Graph runtime."); + return exact.value; + }, + }); + const input = deepFreeze(cloneJson(admittedInput, "Admitted input")); + const returned = exactNode(await main(freeze({ graphrefly: api, input })), byId); + return stringify({ + graphName, + answerNodeId: returned.id, + nodes, + }); + }; + const compute = (nodeId, dependencyValuesJson) => { + const computeFn = + typeof nodeId === "string" && hasOwn(computeById, nodeId) + ? computeById[nodeId] + : undefined; + if ( + typeof nodeId !== "string" || + typeof computeFn !== "function" || + typeof dependencyValuesJson !== "string" + ) throw new TypeError("GraphReFly derived computation is unavailable."); + const dependencyValues = deepFreeze( + cloneJson(parse(dependencyValuesJson), "Derived dependency values"), + ); + if (!Array.isArray(dependencyValues)) + throw new TypeError("Derived dependency values must be an array."); + const value = Reflect.apply(computeFn, undefined, dependencyValues); + if (value !== null && (typeof value === "object" || typeof value === "function")) { + if (typeof value.then === "function") + throw new TypeError("GraphReFly derive must be synchronous."); + } + return stringify(cloneJson(value, "Derived value")); + }; + const controller = freeze({ declare, compute }); + for (const intrinsic of [ + Object, + Array, + Function, + Promise, + Map, + Set, + WeakMap, + WeakSet, + RegExp, + Date, + Error, + TypeError, + Number, + String, + Boolean, + Symbol, + BigInt, + JSON, + Math, + Reflect, + ]) { + if (intrinsic && intrinsic.prototype) freeze(intrinsic.prototype); + freeze(intrinsic); + } + return controller; +})(); +`; + +function record(value: unknown): value is Record { + if (value === null || typeof value !== "object" || Array.isArray(value)) return false; + const proto = Object.getPrototypeOf(value); + return proto === Object.prototype || proto === null; +} + +function exactKeys( + value: Record, + expected: readonly string[], + label: string, +): void { + const observed = Object.keys(value).sort(); + const wanted = [...expected].sort(); + if (observed.length !== wanted.length || observed.some((key, index) => key !== wanted[index])) + throw new TypeError(`${label} has an invalid shape.`); +} + +function safeString(value: unknown, label: string): string { + if (typeof value !== "string" || value.length === 0 || value.length > 512) + throw new TypeError(`${label} is invalid.`); + return value; +} + +function digest(value: unknown, label: string): string { + const text = safeString(value, label); + if (!/^sha256:[a-f0-9]{64}$/.test(text)) throw new TypeError(`${label} is invalid.`); + return text; +} + +function parseControl(value: unknown): RunnerControl { + if (!record(value)) throw new TypeError("Runner control must be an object."); + exactKeys( + value, + [ + "contractVersion", + "compatibilityRevision", + "runnerApiRevision", + "manifestFingerprint", + "args", + "runAdmissionId", + ], + "Runner control", + ); + if ( + value.contractVersion !== "1" || + value.compatibilityRevision !== COMPATIBILITY_REVISION || + value.runnerApiRevision !== RUNNER_API_REVISION + ) + throw new TypeError("Runner control identity is invalid."); + safeString(value.manifestFingerprint, "Manifest fingerprint"); + safeString(value.runAdmissionId, "Run admission id"); + if (!record(value.args)) throw new TypeError("Runner arguments must be an object."); + const args = value.args; + for (const key of [ + "runId", + "sourceRevision", + "bundleRevision", + "compilerRevision", + "allowedApiRevision", + "graphreflyPackageRevision", + "runnerRevision", + ] as const) + safeString(args[key], key); + for (const key of ["sourceDigest", "bundleDigest", "runnerImageDigest", "inputDigest"] as const) + digest(args[key], key); + if ( + args.contractVersion !== "1" || + !Number.isSafeInteger(args.attempt) || + (args.attempt as number) < 1 || + !Array.isArray(args.admittedInputRefs) || + args.admittedInputRefs.length === 0 || + args.admittedInputRefs.some((entry) => typeof entry !== "string" || entry.length === 0) + ) + throw new TypeError("Runner arguments are invalid."); + return value as unknown as RunnerControl; +} + +function parseJson(bytes: Uint8Array, label: string): unknown { + try { + return JSON.parse(new TextDecoder().decode(bytes)); + } catch { + throw new TypeError(`${label} must be valid JSON.`); + } +} + +function parsePlan(value: unknown): RunnerPlan { + if (!record(value)) throw new TypeError("Runner plan must be an object."); + exactKeys(value, ["graphName", "answerNodeId", "nodes"], "Runner plan"); + if ( + typeof value.graphName !== "string" || + !SAFE_NAME.test(value.graphName) || + value.graphName.includes("::") || + typeof value.answerNodeId !== "string" || + !Array.isArray(value.nodes) || + value.nodes.length === 0 || + value.nodes.length > MAX_RUNNER_NODES + ) + throw new TypeError("Runner plan identity or node bound is invalid."); + const ids = new Set(); + const names = new Set(); + let edgeCount = 0; + const nodes: RunnerNodePlan[] = value.nodes.map((entry) => { + if (!record(entry)) throw new TypeError("Runner plan node must be an object."); + const expected = + entry.kind === "source" + ? entry.meta === undefined + ? ["id", "kind", "name", "deps", "value"] + : ["id", "kind", "name", "deps", "value", "meta"] + : entry.meta === undefined + ? ["id", "kind", "name", "deps"] + : ["id", "kind", "name", "deps", "meta"]; + exactKeys(entry, expected, "Runner plan node"); + if ( + typeof entry.id !== "string" || + !/^node-[1-9][0-9]*$/.test(entry.id) || + ids.has(entry.id) || + (entry.kind !== "source" && entry.kind !== "derived") || + typeof entry.name !== "string" || + !SAFE_NAME.test(entry.name) || + entry.name.includes("::") || + names.has(entry.name) || + !Array.isArray(entry.deps) || + entry.deps.some((dep) => typeof dep !== "string" || !ids.has(dep)) || + (entry.kind === "source" && entry.deps.length !== 0) || + (entry.kind === "derived" && entry.deps.length === 0) + ) + throw new TypeError("Runner plan node identity or dependency is invalid."); + ids.add(entry.id); + names.add(entry.name); + edgeCount += entry.deps.length; + if (edgeCount > MAX_RUNNER_EDGES) throw new TypeError("Runner plan edge bound is invalid."); + const canonical = JSON.parse( + new TextDecoder().decode(strictCanonicalJsonBytes(entry)), + ) as RunnerNodePlan; + return canonical; + }); + if (!ids.has(value.answerNodeId)) + throw new TypeError("Runner answer must identify a node created by this run."); + return { graphName: value.graphName, answerNodeId: value.answerNodeId, nodes }; +} + +async function evaluateBundle(bundleSource: string, input: unknown): Promise { + const context = createContext(Object.create(null), { + codeGeneration: { strings: false, wasm: false }, + name: "graphrefly-local-untrusted-js", + }); + const controller = new Script(PRELUDE, { + filename: "graphrefly-runner-prelude.js", + }).runInContext(context) as SandboxRunnerController; + const rejectDynamicImport = new Script( + `() => Promise.reject( + new TypeError("Dynamic imports are not admitted by the GraphReFly runner API."), + )`, + { + filename: "graphrefly-runner-import-policy.js", + }, + ).runInContext(context) as () => Promise; + const userModule = new SourceTextModule(bundleSource, { + context, + identifier: "graphrefly:user-bundle", + initializeImportMeta(meta) { + Object.freeze(meta); + }, + importModuleDynamically() { + return rejectDynamicImport(); + }, + }); + await userModule.link(() => { + throw new TypeError("Imports are not admitted by the GraphReFly runner API."); + }); + await userModule.evaluate(); + const main = Reflect.get(userModule.namespace, "default"); + const declare = Reflect.get(controller, "declare"); + const encoded = await Reflect.apply(declare, undefined, [ + main, + new TextDecoder().decode(strictCanonicalJsonBytes(input)), + ]); + if (typeof encoded !== "string") throw new TypeError("Runner plan serialization failed."); + const plan = parsePlan(JSON.parse(encoded)); + const compute = Reflect.get(controller, "compute"); + return { + plan, + compute(nodeId, dependencyValues) { + const computed = Reflect.apply(compute, undefined, [ + nodeId, + new TextDecoder().decode(strictCanonicalJsonBytes(dependencyValues)), + ]); + if (typeof computed !== "string") + throw new TypeError("Runner derived result serialization failed."); + const value = JSON.parse(computed) as unknown; + strictCanonicalJsonBytes(value); + return value; + }, + }; +} + +function materializeResult( + evaluated: EvaluatedBundle, + control: RunnerControl, +): Record { + const { plan } = evaluated; + const runtimeGraph = graph({ name: plan.graphName }); + const group = runtimeGraph.topologyGroup({ name: "local-untrusted-js-runner" }); + const nodes = new Map>(); + const releases: Array<() => void> = []; + let result: Record | undefined; + let failure: unknown; + let cleanupFailure: Error | undefined; + try { + for (const node of plan.nodes) { + const opts = + node.meta === undefined ? { name: node.name } : { name: node.name, meta: node.meta }; + if (node.kind === "source") { + nodes.set(node.id, group.state(node.value, opts)); + continue; + } + const deps = node.deps.map((id) => { + const dep = nodes.get(id); + if (dep === undefined) throw new TypeError("Runner plan dependency is unavailable."); + return dep; + }); + const derived = group.derived( + deps, + (...dependencyValues) => evaluated.compute(node.id, dependencyValues), + opts, + ); + nodes.set(node.id, derived); + } + const answerNode = nodes.get(plan.answerNodeId); + if (answerNode === undefined) throw new TypeError("Runner answer node is unavailable."); + const answerPlan = plan.nodes.find((node) => node.id === plan.answerNodeId); + if (answerPlan === undefined) throw new TypeError("Runner answer plan is unavailable."); + releases.push(runtimeGraph.retain(answerNode, { reason: "local untrusted JS runner answer" })); + if (answerNode.cache === undefined) + throw new TypeError("Runner answer node did not produce a Graph value."); + const answer = JSON.parse( + new TextDecoder().decode(strictCanonicalJsonBytes(answerNode.cache)), + ) as unknown; + const topology = runtimeGraph.topology(); + const describe = runtimeGraph.describe(); + result = { + contractVersion: "1", + answer, + topology, + describe, + provenance: { + sourceRevision: control.args.sourceRevision, + sourceDigest: control.args.sourceDigest, + bundleRevision: control.args.bundleRevision, + bundleDigest: control.args.bundleDigest, + compilerRevision: control.args.compilerRevision, + allowedApiRevision: control.args.allowedApiRevision, + graphreflyPackageRevision: control.args.graphreflyPackageRevision, + runnerRevision: control.args.runnerRevision, + runnerImageDigest: control.args.runnerImageDigest, + manifestFingerprint: control.manifestFingerprint, + runId: control.args.runId, + attempt: control.args.attempt, + graphName: plan.graphName, + answerNodeId: answerPlan.name, + admittedInputRefs: [...control.args.admittedInputRefs], + inputDigest: control.args.inputDigest, + runAdmissionId: control.runAdmissionId, + }, + cleanup: { + graphNodesAfterDispose: 0, + graphEdgesAfterDispose: 0, + }, + }; + } catch (error) { + failure = error; + } finally { + for (const release of releases.reverse()) release(); + group.release({ reason: "local untrusted JS runner settled" }); + const after = runtimeGraph.topology(); + if (after.nodes.length !== 0 || after.edges.length !== 0 || after.subgraphs !== undefined) + cleanupFailure = new Error("Runner Graph did not dispose to 0N/0E."); + } + if (failure !== undefined) throw failure; + if (cleanupFailure !== undefined) throw cleanupFailure; + if (result === undefined) throw new Error("Runner result was not materialized."); + return result; +} + +async function main(): Promise { + if ( + process.argv.length !== 5 || + INPUT_PATHS.some((path, index) => process.argv[index + 2] !== path) + ) + throw new TypeError("Runner requires the fixed bundle, input and control paths."); + const [bundleBytes, inputBytes, controlBytes] = await Promise.all( + INPUT_PATHS.map((path) => readFile(path)), + ); + const control = parseControl(parseJson(controlBytes, "Runner control")); + const observedBundleDigest = `sha256:${createHash("sha256").update(bundleBytes).digest("hex")}`; + const observedInputDigest = `sha256:${createHash("sha256") + .update(strictCanonicalJsonBytes(parseJson(inputBytes, "Admitted input"))) + .digest("hex")}`; + if ( + observedBundleDigest !== control.args.bundleDigest || + observedInputDigest !== control.args.inputDigest + ) + throw new TypeError("Runner material digest mismatch."); + const input = parseJson(inputBytes, "Admitted input"); + const evaluated = await evaluateBundle(new TextDecoder().decode(bundleBytes), input); + const result = materializeResult(evaluated, control); + process.stdout.write(strictCanonicalJsonBytes(result)); +} + +main().catch((error: unknown) => { + const message = error instanceof Error ? error.message : "Local untrusted JS runner failed."; + process.stderr.write(`${message}\n`); + process.exitCode = 1; +}); diff --git a/packages/ts/runners/local-untrusted-js/tsconfig.json b/packages/ts/runners/local-untrusted-js/tsconfig.json new file mode 100644 index 00000000..6793ff9a --- /dev/null +++ b/packages/ts/runners/local-untrusted-js/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": false, + "noEmit": true, + "rootDir": "../..", + "types": ["node"] + }, + "include": ["runner.ts"] +} diff --git a/packages/ts/src/__tests__/local-untrusted-js-compute-node.live.test.ts b/packages/ts/src/__tests__/local-untrusted-js-compute-node.live.test.ts index e79f7bd7..2f718e1a 100644 --- a/packages/ts/src/__tests__/local-untrusted-js-compute-node.live.test.ts +++ b/packages/ts/src/__tests__/local-untrusted-js-compute-node.live.test.ts @@ -1,6 +1,12 @@ import { createHash } from "node:crypto"; import { describe, expect, it } from "vitest"; import { + certifyNodeLocalUntrustedJsCompute, + NODE_LOCAL_UNTRUSTED_JS_ALLOWED_API_REVISION, + NODE_LOCAL_UNTRUSTED_JS_OUTPUT_POLICY_REVISION, + NODE_LOCAL_UNTRUSTED_JS_RESOURCE_POLICY_REVISION, + NODE_LOCAL_UNTRUSTED_JS_RUNNER_REVISION, + NODE_LOCAL_UNTRUSTED_JS_SANDBOX_POLICY_REVISION, nodeLocalUntrustedJsComputeDriver, nodeLocalUntrustedJsComputeHostBindingAttestation, } from "../executors/local-untrusted-js-compute/node.js"; @@ -22,6 +28,40 @@ const input = { rows: [] }; const inputDigest = `sha256:${createHash("sha256") .update(strictCanonicalJsonBytes(input)) .digest("hex")}`; +const positiveImageRef = process.env.GRAPHREFLY_D667_RUNNER_IMAGE_REF ?? ""; +const positiveLive = + process.env.GRAPHREFLY_D667_LIVE_POSITIVE === "1" && + /^[A-Za-z0-9][A-Za-z0-9._:/+-]{0,190}@sha256:[a-f0-9]{64}$/.test(positiveImageRef); + +function positiveManifest(runnerImageDigest: string): LocalUntrustedJsComputeManifest { + return { + kind: "local-untrusted-js-compute-manifest", + manifestId: "manifest:d667:live-positive", + revision: "manifest-revision:runner-v0", + fingerprint: "manifest-fingerprint:d667-live-positive-runner-v0", + compatibilityRevision: LOCAL_UNTRUSTED_JS_COMPUTE_COMPATIBILITY, + backend: LOCAL_UNTRUSTED_JS_COMPUTE_BACKEND, + runnerApiRevision: LOCAL_UNTRUSTED_JS_COMPUTE_RUNNER_API, + runnerImageDigest, + runnerRevision: NODE_LOCAL_UNTRUSTED_JS_RUNNER_REVISION, + compilerRevision: "compiler:esbuild-fixed-v0", + allowedApiRevision: NODE_LOCAL_UNTRUSTED_JS_ALLOWED_API_REVISION, + graphreflyPackageRevision: "graphrefly-ts:0.7.0", + sandboxPolicyRevision: NODE_LOCAL_UNTRUSTED_JS_SANDBOX_POLICY_REVISION, + networkPolicyRevision: "deny-all-v1", + filesystemPolicyRevision: "read-only-input-bounded-tmp-v1", + resourcePolicyRevision: NODE_LOCAL_UNTRUSTED_JS_RESOURCE_POLICY_REVISION, + outputPolicyRevision: NODE_LOCAL_UNTRUSTED_JS_OUTPUT_POLICY_REVISION, + executionTimeoutMs: 10_000, + killGraceMs: 1_000, + cleanupTimeoutMs: 5_000, + maxBundleBytes: 16_384, + maxInputBytes: 4_096, + maxOutputBytes: 64_000, + maxTopologyNodes: 8, + maxTopologyEdges: 8, + }; +} describe.runIf(live)("D667 Node rootless-Podman broker live negative", () => { it("creates the exact contained attempt, fails on a non-runner image, and still removes it", async () => { @@ -96,3 +136,150 @@ describe.runIf(live)("D667 Node rootless-Podman broker live negative", () => { }); }); }); + +describe.runIf(positiveLive)("D667 fixed runner positive rootless-Podman execution", () => { + it("executes admitted logic into a real 3N/2E Graph result and removes the container", async () => { + const runnerImageDigest = positiveImageRef.slice(positiveImageRef.lastIndexOf("@") + 1); + const admittedBundle = new TextEncoder().encode(` +export default ({ graphrefly, input }) => { + graphrefly.graph("trusted-data-answer"); + const rows = graphrefly.source("admitted-rows", input.rows, { role: "admitted-input" }); + let graphRuntimeComputeCount = 0; + const count = graphrefly.derive("row-count", [rows], function (value) { + if (this !== undefined) throw new TypeError("Derived callback receiver is not empty."); + graphRuntimeComputeCount += 1; + return value.length; + }); + const answer = graphrefly.derive( + "answer", + [count], + (value) => { + graphRuntimeComputeCount += 1; + return { + rowCount: value, + question: input.question, + graphRuntimeComputeCount, + }; + }, + { role: "answer" }, + ); + return answer; +}; +`); + const admittedInput = { + question: "How many admitted rows are present?", + rows: [{ id: 1 }, { id: 2 }, { id: 3 }], + }; + const admittedBundleDigest = `sha256:${createHash("sha256") + .update(admittedBundle) + .digest("hex")}`; + const admittedInputDigest = `sha256:${createHash("sha256") + .update(strictCanonicalJsonBytes(admittedInput)) + .digest("hex")}`; + const host = await nodeLocalUntrustedJsComputeHostBindingAttestation(); + const manifest = positiveManifest(runnerImageDigest); + const args: LocalUntrustedJsComputeArguments = { + contractVersion: "1", + runId: "run:d667-live-positive", + attempt: 1, + epoch: "epoch:1", + sourceRevision: "source:1", + sourceDigest: `sha256:${"3".repeat(64)}`, + bundleRevision: "bundle:1", + bundleDigest: admittedBundleDigest, + compilerRevision: manifest.compilerRevision, + allowedApiRevision: manifest.allowedApiRevision, + graphreflyPackageRevision: manifest.graphreflyPackageRevision, + runnerRevision: manifest.runnerRevision, + runnerImageDigest: manifest.runnerImageDigest, + sandboxPolicyRevision: manifest.sandboxPolicyRevision, + networkPolicyRevision: manifest.networkPolicyRevision, + filesystemPolicyRevision: manifest.filesystemPolicyRevision, + resourcePolicyRevision: manifest.resourcePolicyRevision, + outputPolicyRevision: manifest.outputPolicyRevision, + admittedInputRefs: ["input:rows", "input:question"], + inputDigest: admittedInputDigest, + }; + const outcome = await nodeLocalUntrustedJsComputeDriver({ imageRef: positiveImageRef }).execute( + { + runId: args.runId, + attempt: args.attempt, + epoch: args.epoch, + manifestFingerprint: manifest.fingerprint, + hostBindingDigest: host.hostBindingDigest, + runAdmissionId: "run-admission:d667-live-positive", + signal: new AbortController().signal, + }, + args, + { bundle: admittedBundle, input: admittedInput }, + manifest, + ); + expect( + outcome.outcome === "failed" ? outcome.code : "", + JSON.stringify(outcome, undefined, 2), + ).toBe(""); + expect(outcome).toMatchObject({ + outcome: "succeeded", + cleanup: "succeeded", + result: { + answer: { + rowCount: 3, + question: "How many admitted rows are present?", + graphRuntimeComputeCount: 2, + }, + topology: { + name: "trusted-data-answer", + edges: expect.arrayContaining([ + { from: "admitted-rows", to: "row-count" }, + { from: "row-count", to: "answer" }, + ]), + }, + provenance: { + runId: args.runId, + runAdmissionId: "run-admission:d667-live-positive", + runnerImageDigest, + graphName: "trusted-data-answer", + answerNodeId: "answer", + admittedInputRefs: ["input:rows", "input:question"], + }, + cleanup: { + graphNodesAfterDispose: 0, + graphEdgesAfterDispose: 0, + }, + }, + }); + if (outcome.outcome !== "succeeded") throw new Error("Expected successful runner outcome."); + expect(outcome.result.topology.nodes).toHaveLength(3); + expect(outcome.result.topology.edges).toHaveLength(2); + expect(outcome.result.describe.nodes).toHaveLength(3); + }); + + it("independently certifies Graph execution, ambient denial, cancellation and cleanup", async () => { + const runnerImageDigest = positiveImageRef.slice(positiveImageRef.lastIndexOf("@") + 1); + const observedAtMs = Date.now(); + const readiness = await certifyNodeLocalUntrustedJsCompute({ + manifest: positiveManifest(runnerImageDigest), + imageRef: positiveImageRef, + now: () => observedAtMs, + }); + expect(readiness).toMatchObject({ + kind: "local-untrusted-js-compute-readiness", + state: "ready", + observedAtMs, + rootlessVerified: true, + imageDigestVerified: true, + runnerVerified: true, + nonRootVerified: true, + readOnlyRootFilesystemVerified: true, + noNewPrivilegesVerified: true, + capabilitiesDroppedVerified: true, + noEngineSocketMountVerified: true, + noHostBindMountVerified: true, + denyNetworkVerified: true, + resourceBoundsVerified: true, + cancellationVerified: true, + cleanupVerified: true, + }); + expect(readiness.attestationRefs).toHaveLength(6); + }, 20_000); +}); diff --git a/packages/ts/src/__tests__/local-untrusted-js-compute-node.test.ts b/packages/ts/src/__tests__/local-untrusted-js-compute-node.test.ts index 3c3c8b7b..284da375 100644 --- a/packages/ts/src/__tests__/local-untrusted-js-compute-node.test.ts +++ b/packages/ts/src/__tests__/local-untrusted-js-compute-node.test.ts @@ -48,6 +48,7 @@ interface HarnessState { | "residue" | "strict-exit" | "delayed-create" + | "delayed-create-transient-control" | "unrelated-list"; created: boolean; deleted: boolean; @@ -56,6 +57,9 @@ interface HarnessState { labels?: Record; readonly createdRunLabels: string[]; readonly deletePaths: string[]; + readonly stopPaths: string[]; + transientDeleteFailures: number; + transientListFailures: number; } const cleanupTasks: Array<() => Promise> = []; @@ -138,6 +142,9 @@ async function harness(mode: HarnessState["mode"]): Promise<{ hangInfo: false, createdRunLabels: [], deletePaths: [], + stopPaths: [], + transientDeleteFailures: 0, + transientListFailures: 0, }; const previousRuntimeDirectory = process.env.XDG_RUNTIME_DIR; vi.stubEnv("XDG_RUNTIME_DIR", directory); @@ -176,12 +183,18 @@ async function harness(mode: HarnessState["mode"]): Promise<{ if (mode === "ambiguous-create") sendJson(response, 500, { message: "response lost" }); else sendJson(response, 201, { Id: containerId, Warnings: [] }); }; - if (mode === "delayed-create") setTimeout(finishCreate, 60); + if (mode === "delayed-create" || mode === "delayed-create-transient-control") + setTimeout(finishCreate, 160); else finishCreate(); }); return; } if (path.includes("/libpod/containers/json?")) { + if (mode === "delayed-create-transient-control" && state.transientListFailures < 2) { + state.transientListFailures += 1; + sendJson(response, 503, { message: "transient list failure" }); + return; + } if (mode === "unrelated-list") { sendJson(response, 200, [ { @@ -240,12 +253,16 @@ async function harness(mode: HarnessState["mode"]): Promise<{ return; } if (path.includes("/stop?") || path.includes("/kill?")) { + if (path.includes("/stop?")) state.stopPaths.push(path); response.writeHead(204).end(); return; } if (request.method === "DELETE") { state.deletePaths.push(path); - if (mode === "residue") response.writeHead(500).end(); + if (mode === "delayed-create-transient-control" && state.transientDeleteFailures < 1) { + state.transientDeleteFailures += 1; + response.writeHead(503).end(); + } else if (mode === "residue") response.writeHead(500).end(); else { if (!path.includes("b".repeat(64))) state.deleted = true; response.writeHead(204).end(); @@ -278,7 +295,13 @@ function inspectBody( Config: { Image: imageRef, User: "65532:65532", - Entrypoint: ["/usr/local/bin/node", "/opt/graphrefly/local-untrusted-js-runner.mjs"], + Entrypoint: [ + "/nodejs/bin/node", + "--experimental-vm-modules", + "--no-addons", + "--disable-proto=throw", + "/opt/graphrefly/local-untrusted-js-runner.mjs", + ], Cmd: ["/input/bundle.mjs", "/input/input.json", "/input/control.json"], Env: mismatch ? ["SECRET=ambient"] : [], Labels: labels, @@ -300,8 +323,8 @@ function inspectBody( "/tmp": "rw,nosuid,nodev,noexec,size=16777216,mode=0700,rprivate,tmpcopyup", }, Ulimits: [ - { Name: "RLIMIT_RLIMIT_FSIZE", Soft: 16 * 1024 * 1024, Hard: 16 * 1024 * 1024 }, - { Name: "RLIMIT_RLIMIT_NOFILE", Soft: 128, Hard: 128 }, + { Name: "RLIMIT_FSIZE", Soft: 16 * 1024 * 1024, Hard: 16 * 1024 * 1024 }, + { Name: "RLIMIT_NOFILE", Soft: 128, Hard: 128 }, ], }, Mounts: [], @@ -345,7 +368,7 @@ describe("D667 Node rootless-Podman broker lifecycle", () => { it("keeps an aborted create unverifiable while reconciling a delayed allocation", async () => { const { state, hostBindingDigest } = await harness("delayed-create"); await expect( - execute(hostBindingDigest, manifest({ executionTimeoutMs: 25, cleanupTimeoutMs: 300 })), + execute(hostBindingDigest, manifest({ executionTimeoutMs: 25, cleanupTimeoutMs: 350 })), ).resolves.toMatchObject({ outcome: "timeout", code: "local-untrusted-js-compute-timeout", @@ -354,6 +377,23 @@ describe("D667 Node rootless-Podman broker lifecycle", () => { expect(state).toMatchObject({ created: true, deleted: true }); }); + it("uses the full cleanup window when delayed create reconciliation has transient control failures", async () => { + const { state, hostBindingDigest } = await harness("delayed-create-transient-control"); + await expect( + execute(hostBindingDigest, manifest({ executionTimeoutMs: 25, cleanupTimeoutMs: 400 })), + ).resolves.toMatchObject({ + outcome: "timeout", + code: "local-untrusted-js-compute-timeout", + cleanup: "unverifiable", + }); + expect(state).toMatchObject({ + created: true, + deleted: true, + transientDeleteFailures: 1, + transientListFailures: 2, + }); + }); + it("refuses an unrelated ID even when an ignored filter returns matching labels and name", async () => { const { state, hostBindingDigest } = await harness("unrelated-list"); await expect(execute(hostBindingDigest, manifest())).resolves.toMatchObject({ @@ -379,6 +419,7 @@ describe("D667 Node rootless-Podman broker lifecycle", () => { cleanup: "succeeded", }); expect(state.deleted).toBe(true); + expect(state.stopPaths).toEqual([expect.stringContaining("/stop?timeout=1")]); }); it("classifies one attempt-wide deadline as timeout and still cleans up", async () => { diff --git a/packages/ts/src/__tests__/local-untrusted-js-compute.test.ts b/packages/ts/src/__tests__/local-untrusted-js-compute.test.ts index d0ab3472..bcaa3142 100644 --- a/packages/ts/src/__tests__/local-untrusted-js-compute.test.ts +++ b/packages/ts/src/__tests__/local-untrusted-js-compute.test.ts @@ -217,6 +217,7 @@ const successfulDriver = (): LocalUntrustedJsComputeDriver => ({ runId: request.runId, attempt: request.attempt, graphName: "local-code-workgraph", + answerNodeId: "answer", admittedInputRefs: request.admittedInputRefs, inputDigest: request.inputDigest, runAdmissionId: "run-admission:1", @@ -357,6 +358,25 @@ describe("D667 local untrusted JS compute contract", () => { now: () => 200, }), ).rejects.toThrow("mismatched provenance"); + + const detachedAnswerDriver = successfulDriver(); + vi.mocked(detachedAnswerDriver.execute).mockImplementationOnce(async (...call) => { + const good = await successfulDriver().execute(...call); + if (good.outcome !== "succeeded") return good; + return { + ...good, + result: { + ...good.result, + provenance: { ...good.result.provenance, answerNodeId: "question" }, + }, + }; + }); + await expect( + runLocalUntrustedJsComputeAttempt({ + ...attemptOptions(detachedAnswerDriver), + now: () => 200, + }), + ).rejects.toThrow("topology and describe snapshots disagree"); }); it("does not accept a mutable image tag for the Node broker", () => { @@ -515,7 +535,22 @@ describe("D667 local untrusted JS compute contract", () => { vi.mocked(overflowDriver.execute).mockImplementationOnce(async (...call) => { const good = await successfulDriver().execute(...call); if (good.outcome !== "succeeded") return good; - return { ...good, result: { ...good.result, answer: "x".repeat(20_000) } }; + const oversizedAnswer = "x".repeat(20_000); + return { + ...good, + result: { + ...good.result, + answer: oversizedAnswer, + describe: { + ...good.result.describe, + nodes: good.result.describe.nodes.map((node) => + node.id === good.result.provenance.answerNodeId + ? { ...node, value: oversizedAnswer } + : node, + ), + }, + }, + }; }); await expect(runLocalUntrustedJsComputeAttempt(attemptOptions(overflowDriver))).rejects.toThrow( "output byte bound", diff --git a/packages/ts/src/__tests__/subpaths.test.ts b/packages/ts/src/__tests__/subpaths.test.ts index d03eb478..1f4c1cbf 100644 --- a/packages/ts/src/__tests__/subpaths.test.ts +++ b/packages/ts/src/__tests__/subpaths.test.ts @@ -899,6 +899,9 @@ describe("package subpath barrels (D40/D41 intent parity)", () => { expect(typeof executorLocalUntrustedJsCompute.localUntrustedJsComputeArguments).toBe( "function", ); + expect(typeof executorLocalUntrustedJsCompute.localUntrustedJsComputeDriverOutcome).toBe( + "function", + ); expect(typeof executorLocalUntrustedJsCompute.localUntrustedJsComputeRuntime).toBe("function"); expect(typeof executorLocalUntrustedJsComputeNode.nodeLocalUntrustedJsComputeDriver).toBe( "function", @@ -906,7 +909,11 @@ describe("package subpath barrels (D40/D41 intent parity)", () => { expect( typeof executorLocalUntrustedJsComputeNode.nodeLocalUntrustedJsComputeHostBindingAttestation, ).toBe("function"); + expect(typeof executorLocalUntrustedJsComputeNode.certifyNodeLocalUntrustedJsCompute).toBe( + "function", + ); expect(Object.hasOwn(rootPackage, "localUntrustedJsComputeRuntime")).toBe(false); + expect(Object.hasOwn(rootPackage, "certifyNodeLocalUntrustedJsCompute")).toBe(false); expect(Object.hasOwn(rootPackage, "certifyDockerEngineApiV0LocalContainerPostgresql")).toBe( false, ); diff --git a/packages/ts/src/executors/local-untrusted-js-compute.ts b/packages/ts/src/executors/local-untrusted-js-compute.ts index c334ac49..9a11123b 100644 --- a/packages/ts/src/executors/local-untrusted-js-compute.ts +++ b/packages/ts/src/executors/local-untrusted-js-compute.ts @@ -134,6 +134,7 @@ export interface LocalUntrustedJsComputeProvenance { readonly runId: string; readonly attempt: number; readonly graphName: string; + readonly answerNodeId: string; readonly admittedInputRefs: readonly string[]; readonly inputDigest: string; readonly runAdmissionId: string; @@ -1162,12 +1163,15 @@ export function localUntrustedJsComputeArguments( return Object.freeze({ ...value, admittedInputRefs }); } -function localUntrustedJsComputeDriverOutcome( +export function localUntrustedJsComputeDriverOutcome( value: LocalUntrustedJsComputeDriverOutcome, args: LocalUntrustedJsComputeArguments, manifest: LocalUntrustedJsComputeManifest, runAdmissionId: string, ): LocalUntrustedJsComputeDriverOutcome { + const exactManifest = localUntrustedJsComputeManifest(manifest); + const exactArgs = localUntrustedJsComputeArguments(args, exactManifest); + safe(runAdmissionId, "local untrusted JS compute run admission id"); if ( !plain(value) || !["succeeded", "failed", "timeout", "canceled"].includes(String(value.outcome)) @@ -1185,8 +1189,8 @@ function localUntrustedJsComputeDriverOutcome( code: "local-untrusted-js-compute-cleanup-not-verified", cleanup: value.cleanup, }); - const result = runnerResult(value.result, args, manifest, runAdmissionId); - if (jsonBytes(result, "local untrusted JS compute result") > manifest.maxOutputBytes) + const result = runnerResult(value.result, exactArgs, exactManifest, runAdmissionId); + if (jsonBytes(result, "local untrusted JS compute result") > exactManifest.maxOutputBytes) throw new TypeError("Local untrusted JS compute result exceeds the output byte bound."); return Object.freeze({ outcome: "succeeded", result, cleanup: "succeeded" }); } @@ -1212,6 +1216,7 @@ function runnerResult( "runId", "attempt", "graphName", + "answerNodeId", "admittedInputRefs", "inputDigest", "runAdmissionId", @@ -1242,10 +1247,19 @@ function runnerResult( const answer = cloneJson(value.answer, "local untrusted JS compute answer"); const topology = validateTopologySnapshot(value.topology, manifest); const describe = validateDescribeSnapshot(value.describe, manifest); + const answerDescribeNode = describe.nodes.find( + (node) => node.id === value.provenance.answerNodeId, + ); if ( JSON.stringify(topology) !== JSON.stringify(topologyFromDescribe(describe)) || topology.name !== value.provenance.graphName || - describe.name !== value.provenance.graphName + describe.name !== value.provenance.graphName || + !topology.nodes.some((node) => node.id === value.provenance.answerNodeId) || + answerDescribeNode === undefined || + answerDescribeNode.value === undefined || + JSON.stringify( + cloneJson(answerDescribeNode.value, "local untrusted JS compute answer node"), + ) !== JSON.stringify(answer) ) throw new TypeError("Local untrusted JS compute topology and describe snapshots disagree."); const provenance = Object.freeze({ @@ -1262,11 +1276,13 @@ function runnerResult( runId: value.provenance.runId, attempt: value.provenance.attempt, graphName: value.provenance.graphName, + answerNodeId: value.provenance.answerNodeId, admittedInputRefs: Object.freeze([...value.provenance.admittedInputRefs]), inputDigest: value.provenance.inputDigest, runAdmissionId: value.provenance.runAdmissionId, }); safe(provenance.graphName, "local untrusted JS compute graph name"); + safe(provenance.answerNodeId, "local untrusted JS compute answer node id"); return Object.freeze({ contractVersion: "1", answer, diff --git a/packages/ts/src/executors/local-untrusted-js-compute/node.ts b/packages/ts/src/executors/local-untrusted-js-compute/node.ts index 4250486e..788f1b36 100644 --- a/packages/ts/src/executors/local-untrusted-js-compute/node.ts +++ b/packages/ts/src/executors/local-untrusted-js-compute/node.ts @@ -12,14 +12,28 @@ import type { LocalUntrustedJsComputeDriverOutcome, LocalUntrustedJsComputeManifest, LocalUntrustedJsComputeMaterial, + LocalUntrustedJsComputeReadiness, LocalUntrustedJsComputeRunnerControl, LocalUntrustedJsComputeRunnerResult, + LocalUntrustedJsJson, +} from "../local-untrusted-js-compute.js"; +import { + LOCAL_UNTRUSTED_JS_COMPUTE_COMPATIBILITY, + localUntrustedJsComputeArguments, + localUntrustedJsComputeDriverOutcome, + localUntrustedJsComputeManifest, + localUntrustedJsComputeReadiness, } from "../local-untrusted-js-compute.js"; -import { LOCAL_UNTRUSTED_JS_COMPUTE_COMPATIBILITY } from "../local-untrusted-js-compute.js"; const execFileAsync = promisify(execFile); const API_REVISION = "5.0.3"; -const RUNNER_ENTRYPOINT = ["/usr/local/bin/node", "/opt/graphrefly/local-untrusted-js-runner.mjs"]; +const RUNNER_ENTRYPOINT = [ + "/nodejs/bin/node", + "--experimental-vm-modules", + "--no-addons", + "--disable-proto=throw", + "/opt/graphrefly/local-untrusted-js-runner.mjs", +]; const BOUNDARY_LABEL = "d667-local-untrusted-js-compute"; const DEFAULT_REQUEST_TIMEOUT_MS = 10_000; const MAX_ENGINE_RESPONSE_BYTES = 512 * 1024; @@ -35,6 +49,17 @@ const NOFILE_LIMIT = 128; const ID = /^[a-f0-9]{64}$/; const SAFE_IMAGE = /^[A-Za-z0-9][A-Za-z0-9._:/@+-]{0,254}$/; const NAMED_DIGEST_IMAGE = /^[A-Za-z0-9][A-Za-z0-9._:/+-]{0,190}@sha256:[a-f0-9]{64}$/; +const CERTIFICATION_TTL_MS = 5 * 60_000; +const CERTIFICATION_CANCEL_DELAY_MS = 500; + +export const NODE_LOCAL_UNTRUSTED_JS_RUNNER_REVISION = "graphrefly-local-js-runner-v0" as const; +export const NODE_LOCAL_UNTRUSTED_JS_ALLOWED_API_REVISION = + "graphrefly-source-derive-api-v0" as const; +export const NODE_LOCAL_UNTRUSTED_JS_SANDBOX_POLICY_REVISION = "podman-vm-no-imports-v0" as const; +export const NODE_LOCAL_UNTRUSTED_JS_RESOURCE_POLICY_REVISION = + "podman-256m-1cpu-64pids-v0" as const; +export const NODE_LOCAL_UNTRUSTED_JS_OUTPUT_POLICY_REVISION = + "strict-json-topology-provenance-v0" as const; interface PodmanBinding { readonly socketPath: string; @@ -67,6 +92,13 @@ export interface NodeLocalUntrustedJsComputeHostBindingAttestation { readonly rootless: true; } +export interface NodeLocalUntrustedJsComputeCertificationOptions { + readonly manifest: LocalUntrustedJsComputeManifest; + readonly imageRef: string; + readonly signal?: AbortSignal; + readonly now?: () => number; +} + /** * Returns only a digest-bound host attestation. Socket paths and engine handles * remain private; the broker rechecks the same digest before every allocation. @@ -85,6 +117,278 @@ export async function nodeLocalUntrustedJsComputeHostBindingAttestation( }); } +/** + * Mints D667 readiness only after the fixed image executes a real Graph, denies + * ambient module authority, honors cancellation and verifies terminal removal. + */ +export async function certifyNodeLocalUntrustedJsCompute( + opts: NodeLocalUntrustedJsComputeCertificationOptions, +): Promise { + const manifest = localUntrustedJsComputeManifest(opts.manifest); + if ( + manifest.runnerRevision !== NODE_LOCAL_UNTRUSTED_JS_RUNNER_REVISION || + manifest.allowedApiRevision !== NODE_LOCAL_UNTRUSTED_JS_ALLOWED_API_REVISION || + manifest.sandboxPolicyRevision !== NODE_LOCAL_UNTRUSTED_JS_SANDBOX_POLICY_REVISION || + manifest.resourcePolicyRevision !== NODE_LOCAL_UNTRUSTED_JS_RESOURCE_POLICY_REVISION || + manifest.outputPolicyRevision !== NODE_LOCAL_UNTRUSTED_JS_OUTPUT_POLICY_REVISION || + manifest.executionTimeoutMs < 2_000 || + !opts.imageRef.endsWith(`@${manifest.runnerImageDigest}`) + ) + throw new TypeError("D667 certifier requires the package-owned fixed runner profile."); + const observedAtMs = (opts.now ?? Date.now)(); + if (!Number.isSafeInteger(observedAtMs)) + throw new TypeError("D667 certification clock is invalid."); + const host = await nodeLocalUntrustedJsComputeHostBindingAttestation(opts.signal); + const driver = nodeLocalUntrustedJsComputeDriver({ imageRef: opts.imageRef }); + const probeSuffix = randomUUID(); + const runProbe = ( + label: string, + bundleSource: string, + input: LocalUntrustedJsJson, + signal: AbortSignal, + ): Promise<{ + readonly outcome: LocalUntrustedJsComputeDriverOutcome; + readonly args: LocalUntrustedJsComputeArguments; + readonly runAdmissionId: string; + }> => { + const bundle = new TextEncoder().encode(bundleSource); + const args = localUntrustedJsComputeArguments( + { + contractVersion: "1", + runId: `certify:d667:${probeSuffix}:${label}`, + attempt: 1, + epoch: "certification:1", + sourceRevision: `certification-source:${label}`, + sourceDigest: `sha256:${createHash("sha256").update(bundleSource).digest("hex")}`, + bundleRevision: `certification-bundle:${label}`, + bundleDigest: `sha256:${createHash("sha256").update(bundle).digest("hex")}`, + compilerRevision: manifest.compilerRevision, + allowedApiRevision: manifest.allowedApiRevision, + graphreflyPackageRevision: manifest.graphreflyPackageRevision, + runnerRevision: manifest.runnerRevision, + runnerImageDigest: manifest.runnerImageDigest, + sandboxPolicyRevision: manifest.sandboxPolicyRevision, + networkPolicyRevision: manifest.networkPolicyRevision, + filesystemPolicyRevision: manifest.filesystemPolicyRevision, + resourcePolicyRevision: manifest.resourcePolicyRevision, + outputPolicyRevision: manifest.outputPolicyRevision, + admittedInputRefs: [`certification-input:${label}`], + inputDigest: `sha256:${createHash("sha256") + .update(strictCanonicalJsonBytes(input)) + .digest("hex")}`, + }, + manifest, + ); + const runAdmissionId = `certification-admission:${probeSuffix}:${label}`; + return Promise.resolve( + driver.execute( + { + runId: args.runId, + attempt: args.attempt, + epoch: args.epoch, + manifestFingerprint: manifest.fingerprint, + hostBindingDigest: host.hostBindingDigest, + runAdmissionId, + signal, + }, + args, + { bundle, input }, + manifest, + ), + ).then((outcome) => ({ outcome, args, runAdmissionId })); + }; + const positiveProbe = await runProbe( + "positive", + `export default async ({ graphrefly, input }) => { + graphrefly.graph("certified-runner-graph"); + const rows = graphrefly.source("rows", input.rows); + let graphRuntimeComputeCount = 0; + const count = graphrefly.derive("count", [rows], function (value) { + if (this !== undefined) throw new TypeError("Derived callback receiver is not empty."); + graphRuntimeComputeCount += 1; + return value.length; + }); + const blocked = (attempt) => { + try { + attempt(); + return true; + } catch { + return false; + } + }; + let caughtDynamicImportConstructorEscape = true; + let caughtDynamicImportBuiltinEscape = true; + try { + await import("node:child_process"); + } catch (error) { + caughtDynamicImportConstructorEscape = blocked(() => + error.constructor.constructor("return process")(), + ); + caughtDynamicImportBuiltinEscape = blocked(() => + error.constructor + .constructor("return process")() + .getBuiltinModule("node:fs"), + ); + } + const answer = graphrefly.derive("answer", [count], (value) => { + graphRuntimeComputeCount += 1; + return { + count: value, + graphRuntimeComputeCount, + ambient: { + process: typeof process !== "undefined", + require: typeof require !== "undefined", + fetch: typeof fetch !== "undefined", + apiConstructorEscape: blocked(() => + graphrefly.source.constructor("return process")(), + ), + globalConstructorEscape: blocked(() => + globalThis.constructor.constructor("return process")(), + ), + inputConstructorEscape: blocked(() => + input.constructor.constructor("return process")(), + ), + legacyHostInputGlobal: typeof __graphreflyAdmittedInput !== "undefined", + runnerBridgeGlobal: typeof __graphreflyRunnerRun !== "undefined", + userMainBridgeGlobal: typeof __graphreflyUserMain !== "undefined", + inputJsonBridgeGlobal: typeof __graphreflyAdmittedInputJson !== "undefined", + caughtDynamicImportConstructorEscape, + caughtDynamicImportBuiltinEscape, + stringEval: blocked(() => eval("1")), + }, + }; + }); + return answer; + };`, + { rows: [{ id: 1 }, { id: 2 }] }, + opts.signal ?? new AbortController().signal, + ); + const positive = localUntrustedJsComputeDriverOutcome( + positiveProbe.outcome, + positiveProbe.args, + manifest, + positiveProbe.runAdmissionId, + ); + if (positive.outcome !== "succeeded" || positive.cleanup !== "succeeded") + throw new TypeError("D667 fixed runner positive Graph probe failed."); + const positiveAnswer = positive.result.answer; + if (!localJsonRecord(positiveAnswer)) + throw new TypeError("D667 fixed runner answer probe failed."); + const positiveAmbient = positiveAnswer.ambient; + if (!localJsonRecord(positiveAmbient)) + throw new TypeError("D667 fixed runner ambient probe failed."); + if ( + positiveAnswer.count !== 2 || + positiveAnswer.graphRuntimeComputeCount !== 2 || + Object.values(positiveAmbient).some((entry) => entry !== false) || + positive.result.topology.name !== "certified-runner-graph" || + positive.result.topology.nodes.length !== 3 || + positive.result.topology.edges.length !== 2 || + positive.result.describe.nodes.length !== 3 || + positive.result.cleanup.graphNodesAfterDispose !== 0 || + positive.result.cleanup.graphEdgesAfterDispose !== 0 + ) + throw new TypeError("D667 fixed runner positive Graph probe failed."); + const deniedStaticImport = ( + await runProbe( + "static-import-denied", + `import fs from "node:fs"; export default () => fs.readFileSync("/etc/passwd", "utf8");`, + { admitted: true }, + opts.signal ?? new AbortController().signal, + ) + ).outcome; + if ( + deniedStaticImport.outcome !== "failed" || + deniedStaticImport.code !== "local-untrusted-js-compute-runner-failed" || + deniedStaticImport.cleanup !== "succeeded" + ) + throw new TypeError("D667 fixed runner static-import denial probe failed."); + const deniedDynamicImport = ( + await runProbe( + "dynamic-import-denied", + `export default async () => import("node:child_process");`, + { admitted: true }, + opts.signal ?? new AbortController().signal, + ) + ).outcome; + if ( + deniedDynamicImport.outcome !== "failed" || + deniedDynamicImport.code !== "local-untrusted-js-compute-runner-failed" || + deniedDynamicImport.cleanup !== "succeeded" + ) + throw new TypeError("D667 fixed runner dynamic-import denial probe failed."); + const deniedDetachedAnswer = ( + await runProbe( + "detached-answer-denied", + `export default () => ({ answer: "not-a-graph-node" });`, + { admitted: true }, + opts.signal ?? new AbortController().signal, + ) + ).outcome; + if ( + deniedDetachedAnswer.outcome !== "failed" || + deniedDetachedAnswer.code !== "local-untrusted-js-compute-runner-failed" || + deniedDetachedAnswer.cleanup !== "succeeded" + ) + throw new TypeError("D667 fixed runner Graph-node answer probe failed."); + const cancellation = new AbortController(); + const cancelSignal = + opts.signal === undefined + ? cancellation.signal + : AbortSignal.any([opts.signal, cancellation.signal]); + const timer = setTimeout(() => cancellation.abort(), CERTIFICATION_CANCEL_DELAY_MS); + let canceled: LocalUntrustedJsComputeDriverOutcome; + try { + canceled = ( + await runProbe( + "cancellation", + `export default () => { while (true) {} };`, + { admitted: true }, + cancelSignal, + ) + ).outcome; + } finally { + clearTimeout(timer); + } + if (canceled.outcome !== "canceled" || canceled.cleanup !== "succeeded") + throw new TypeError( + `D667 fixed runner cancellation and cleanup probe failed (${canceled.outcome}/${"code" in canceled ? canceled.code : "none"}/${canceled.cleanup}).`, + ); + return localUntrustedJsComputeReadiness( + { + kind: "local-untrusted-js-compute-readiness", + manifestFingerprint: manifest.fingerprint, + state: "ready", + observedAtMs, + expiresAtMs: observedAtMs + CERTIFICATION_TTL_MS, + rootlessVerified: true, + imageDigestVerified: true, + runnerVerified: true, + nonRootVerified: true, + readOnlyRootFilesystemVerified: true, + noNewPrivilegesVerified: true, + capabilitiesDroppedVerified: true, + noEngineSocketMountVerified: true, + noHostBindMountVerified: true, + denyNetworkVerified: true, + resourceBoundsVerified: true, + cancellationVerified: true, + cleanupVerified: true, + hostBindingDigest: host.hostBindingDigest, + attestationRefs: [ + "d667:fixed-runner-positive-graph", + "d667:actual-graph-runtime-computation-and-node-answer", + "d667:static-and-dynamic-import-denied", + "d667:ambient-process-require-fetch-and-host-realm-constructors-absent", + "d667:rootless-containment-inspected", + "d667:cancellation-and-terminal-removal", + ], + }, + manifest, + observedAtMs, + ); +} + /** * Creates the exact D667 Node broker. The caller selects no endpoint, command, * entrypoint, network, mount, user or containment option. @@ -352,8 +656,8 @@ function containerRequest( }, ], r_limits: [ - { type: "RLIMIT_FSIZE", hard: FILE_SIZE_LIMIT_BYTES, soft: FILE_SIZE_LIMIT_BYTES }, - { type: "RLIMIT_NOFILE", hard: NOFILE_LIMIT, soft: NOFILE_LIMIT }, + { type: "FSIZE", hard: FILE_SIZE_LIMIT_BYTES, soft: FILE_SIZE_LIMIT_BYTES }, + { type: "NOFILE", hard: NOFILE_LIMIT, soft: NOFILE_LIMIT }, ], privileged: false, cap_drop: ["all"], @@ -428,14 +732,15 @@ async function stopAndKill( ): Promise { const seconds = graceMs / 1_000; const identifier = binding.containerId ?? binding.containerName; + const stopDeadline = Math.min(deadline, Date.now() + graceMs + 1_000); const stopped = await rawRequest( binding.socketPath, - `/v${API_REVISION}/libpod/containers/${identifier}/stop?t=${seconds}`, + `/v${API_REVISION}/libpod/containers/${identifier}/stop?timeout=${seconds}`, signal, "POST", undefined, undefined, - remainingMs(deadline), + remainingMs(stopDeadline), ).catch(() => undefined); if (stopped?.status === 204 || stopped?.status === 304 || stopped?.status === 404) return; await rawRequest( @@ -457,23 +762,28 @@ async function removeAndVerify( ): Promise<"succeeded" | "unverifiable" | false> { const deadline = Date.now() + cleanupTimeoutMs; const signal = AbortSignal.timeout(cleanupTimeoutMs); - await stopAndKill(binding, graceMs, signal, deadline); const identifier = binding.containerId ?? binding.containerName; - const removed = await rawRequest( - binding.socketPath, - `/v${API_REVISION}/libpod/containers/${identifier}?force=true&v=true`, - signal, - "DELETE", - undefined, - undefined, - remainingMs(deadline), - ).catch(() => undefined); - if (removed?.status !== 200 && removed?.status !== 204 && removed?.status !== 404) return false; let absentSince: number | undefined; + await stopAndKill(binding, graceMs, signal, deadline); while (Date.now() < deadline) { + await rawRequest( + binding.socketPath, + `/v${API_REVISION}/libpod/containers/${identifier}?force=true&v=true`, + signal, + "DELETE", + undefined, + undefined, + remainingMs(deadline), + ).catch(() => undefined); const residues = await labeledResidues(binding, signal, deadline); - if (residues === undefined) return false; + if (residues === undefined) { + absentSince = undefined; + if (createRequestSettled) return false; + await delay(20, signal).catch(() => undefined); + continue; + } for (const id of residues) { + await stopAndKill({ ...binding, containerId: id }, graceMs, signal, deadline); await rawRequest( binding.socketPath, `/v${API_REVISION}/libpod/containers/${id}?force=true&v=true`, @@ -486,6 +796,7 @@ async function removeAndVerify( } if (residues.length > 0) { absentSince = undefined; + await delay(20, signal).catch(() => undefined); continue; } const absent = await rawRequest( @@ -497,9 +808,14 @@ async function removeAndVerify( undefined, remainingMs(deadline), ).catch(() => undefined); - if (absent?.status !== 404) return false; + if (absent?.status !== 404) { + absentSince = undefined; + if (createRequestSettled) return false; + await delay(20, signal).catch(() => undefined); + continue; + } absentSince ??= Date.now(); - if (Date.now() - absentSince >= 100) return createRequestSettled ? "succeeded" : "unverifiable"; + if (Date.now() - absentSince >= 100 && createRequestSettled) return "succeeded"; await delay(20, signal).catch(() => undefined); } return createRequestSettled ? false : "unverifiable"; @@ -836,6 +1152,12 @@ function record(value: unknown): value is Record { return proto === Object.prototype || proto === null; } +function localJsonRecord( + value: LocalUntrustedJsJson, +): value is { readonly [key: string]: LocalUntrustedJsJson } { + return record(value); +} + function exactStrings(value: unknown, expected: readonly string[]): boolean { return ( Array.isArray(value) && @@ -903,13 +1225,13 @@ function rlimitsMatch(value: unknown): boolean { normalized.length === 2 && normalized.some( (entry) => - (entry.name === "RLIMIT_FSIZE" || entry.name === "RLIMIT_RLIMIT_FSIZE") && + entry.name === "RLIMIT_FSIZE" && entry.hard === FILE_SIZE_LIMIT_BYTES && entry.soft === FILE_SIZE_LIMIT_BYTES, ) && normalized.some( (entry) => - (entry.name === "RLIMIT_NOFILE" || entry.name === "RLIMIT_RLIMIT_NOFILE") && + entry.name === "RLIMIT_NOFILE" && entry.hard === NOFILE_LIMIT && entry.soft === NOFILE_LIMIT, ) diff --git a/scripts/build-local-untrusted-js-runner-image.mjs b/scripts/build-local-untrusted-js-runner-image.mjs new file mode 100644 index 00000000..13141209 --- /dev/null +++ b/scripts/build-local-untrusted-js-runner-image.mjs @@ -0,0 +1,36 @@ +import { spawnSync } from "node:child_process"; +import { resolve } from "node:path"; + +await import("./build-local-untrusted-js-runner.mjs"); + +const root = resolve(import.meta.dirname, ".."); +const runnerDirectory = resolve(root, "packages/ts/runners/local-untrusted-js"); +const imageTag = "localhost/graphrefly/local-untrusted-js-runner:d667-v0"; +const built = spawnSync( + "podman", + [ + "build", + "--no-cache", + "--timestamp", + "0", + "--tag", + imageTag, + "--file", + resolve(runnerDirectory, "Containerfile"), + runnerDirectory, + ], + { encoding: "utf8", stdio: ["ignore", "inherit", "inherit"] }, +); +if (built.error !== undefined) throw built.error; +if (built.status !== 0) + throw new Error(`Podman runner image build failed (${built.status ?? "signal"}).`); +const inspected = spawnSync("podman", ["image", "inspect", imageTag, "--format", "{{.Digest}}"], { + encoding: "utf8", + stdio: ["ignore", "pipe", "inherit"], +}); +if (inspected.error !== undefined) throw inspected.error; +if (inspected.status !== 0) throw new Error("Podman runner image inspection failed."); +const digest = inspected.stdout.trim(); +if (!/^sha256:[a-f0-9]{64}$/.test(digest)) + throw new Error("Podman runner image did not expose one immutable digest."); +process.stdout.write(`${imageTag.slice(0, imageTag.lastIndexOf(":"))}@${digest}\n`); diff --git a/scripts/build-local-untrusted-js-runner.mjs b/scripts/build-local-untrusted-js-runner.mjs new file mode 100644 index 00000000..0a393b27 --- /dev/null +++ b/scripts/build-local-untrusted-js-runner.mjs @@ -0,0 +1,31 @@ +import { spawnSync } from "node:child_process"; +import { resolve } from "node:path"; +import { build } from "esbuild"; + +const root = resolve(import.meta.dirname, ".."); +const runnerDirectory = resolve(root, "packages/ts/runners/local-untrusted-js"); +const checked = spawnSync( + process.execPath, + [ + resolve(root, "node_modules/typescript/bin/tsc"), + "--noEmit", + "--project", + resolve(runnerDirectory, "tsconfig.json"), + ], + { encoding: "utf8", stdio: "inherit" }, +); +if (checked.error !== undefined) throw checked.error; +if (checked.status !== 0) + throw new Error(`Local untrusted JS runner typecheck failed (${checked.status ?? "signal"}).`); + +await build({ + entryPoints: [resolve(runnerDirectory, "runner.ts")], + outfile: resolve(runnerDirectory, "local-untrusted-js-runner.mjs"), + bundle: true, + format: "esm", + platform: "node", + target: "node22", + sourcemap: false, + minify: false, + legalComments: "none", +}); diff --git a/website/src/content/docs/integrations/matrix.md b/website/src/content/docs/integrations/matrix.md index a1ab26a9..71798d5d 100644 --- a/website/src/content/docs/integrations/matrix.md +++ b/website/src/content/docs/integrations/matrix.md @@ -56,7 +56,7 @@ See [Adapters](/integrations/adapters/) for usage guidance and naming convention | Rootless Podman native Libpod API v0 PostgreSQL contract | D645 independent Podman backend family and app-private runtime host contract; it does not use the Docker-compatible API or expose a provider registry | `@graphrefly/ts/executors/local-container-postgresql-podman-libpod-api-v0-rootless` | | Node-local rootless Podman Libpod API v0 certifier | D645 Node-only candidate certifier with package-owned host coordinates, bounded CLI socket discovery, native Libpod requests, and private resource handles; it remains unavailable until every required live effect probe is certified | `@graphrefly/ts/executors/local-container-postgresql-podman-libpod-api-v0-rootless/node` | | Local untrusted JavaScript compute contract | D667 host-neutral, exact-coordinate attempt contract for immutable JavaScript bundles, admitted JSON inputs, bounded GraphReFly runner results, runtime topology, provenance, cancellation, and verified cleanup | `@graphrefly/ts/executors/local-untrusted-js-compute` | -| Node-local untrusted JavaScript Podman broker | D667 Node-only rootless-Podman broker with package-owned socket discovery, fixed runner entrypoint, digest-pinned image, non-root read-only containment, no host bind or engine-socket mount, one bounded noexec tmpfs, no network, bounded resources, and private resource handles | `@graphrefly/ts/executors/local-untrusted-js-compute/node` | +| Node-local untrusted JavaScript Podman broker and certifier | D667 Node-only rootless-Podman broker plus package-owned distroless runner artifact and live certifier: admitted bundles receive only the fixed graph/source/derive API and admitted JSON; static/dynamic imports and ambient process/require/fetch are denied; the digest-pinned image runs non-root with a read-only rootfs, no host or engine-socket mount, one bounded noexec tmpfs, no network, bounded resources, exact cancellation, verified 0N/0E Graph disposal, and terminal container removal | `@graphrefly/ts/executors/local-untrusted-js-compute/node` | | Managed-cloud PostgreSQL binding | PostgreSQL-16 atomic control-store and worker-initiated WSS session lifecycle with exact fenced leases, cancellation, and settlement | `@graphrefly/ts/executors/managed-cloud-postgresql` | | Managed untrusted JS compute | E2B Cloud v0 untrusted JavaScript compute lifecycle with deny-all network, bounded movement evidence, exact cancellation, and independently visible cleanup | `@graphrefly/ts/executors/managed-untrusted-js-compute` | | Customer-hosted PostgreSQL binding | Signed digest-pinned endpoint agent, outbound authenticated WSS, customer-resident credentials, exact cross-domain fences, and encrypted evidence-only offline outbox | `@graphrefly/ts/executors/customer-hosted-postgresql` | From 00e1aac0ae0e15bfcdfdee20c87eac3ea55bed20 Mon Sep 17 00:00:00 2001 From: David Chen Date: Sun, 26 Jul 2026 16:00:00 -0700 Subject: [PATCH 3/5] fix(ts): enforce closed wave message directions --- AGENTS.md | 12 +++++++----- CLAUDE.md | 12 +++++++----- .../ts/src/__tests__/conformance.part-03.test.ts | 7 +++++++ packages/ts/src/__tests__/core.test.ts | 11 ++++++++--- packages/ts/src/node/node-output-runtime.ts | 8 +++++++- packages/ts/src/node/protocol-guards.ts | 12 +++++++++++- packages/ts/src/protocol/messages.ts | 2 +- 7 files changed, 48 insertions(+), 16 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 14be40a6..13edce72 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -47,12 +47,14 @@ Sibling implementations (each self-contained, cross-language = wire bridge, not - **8 verbs, closed set (D4):** `node` `graph` `batch` `state` + `producer` `derived` `effect` `mount`. Operators are `node` sugar, not verbs — per-language, never in parity (D6). - **`ctx.up` / `ctx.down(msgs)` (D8):** one `msgs` array = one wave; may mix tiers. `ctx.up` is - **control-tier only** (DIRTY/PAUSE/RESUME/INVALIDATE/TEARDOWN); DATA/RESOLVED/COMPLETE/ERROR are - down-only (R-ctx-up). Handle = pure data `(pool_id, handle_id)`, no methods (D7). -- **7-tier const table + 10-message closed set (D9/D34, R-tier/R-msg-closed-set):** adding a tier + **control/demand only** (DIRTY/PAUSE/RESUME/PULL/INVALIDATE/TEARDOWN); + START/DATA/RESOLVED/COMPLETE/ERROR are not up-going (R-ctx-up). Handle = pure data + `(pool_id, handle_id)`, no methods (D7). +- **7-tier const table + 11-message closed set (D9/D34/D269, R-tier/R-msg-closed-set):** adding a tier or message type is a constitutional change. -- **graph = single-thread causal/concurrency domain (D22):** parallelism via pool callback or - multi-graph + wire bridge; rewire intra-graph only. +- **graph = single-thread concurrency domain (D22):** causal influence may cross graphs through + delayed-consistency wire bridges; parallelism via pool callback or multi-graph + wire bridge; + rewire intra-graph only. - **parity = behavioral conformance (D24):** structural `Impl` + cross-track-ledger retired. - **config dissolved (D26):** clock is graph-local (no global singleton); `messageTier` is a compile-time const table; `onMessage`/`onSubscribe` are substrate-fixed, not user-replaceable (D19). diff --git a/CLAUDE.md b/CLAUDE.md index e9efc6dd..e6f5b946 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,12 +47,14 @@ Sibling implementations (each self-contained, cross-language = wire bridge, not - **8 verbs, closed set (D4):** `node` `graph` `batch` `state` + `producer` `derived` `effect` `mount`. Operators are `node` sugar, not verbs — per-language, never in parity (D6). - **`ctx.up` / `ctx.down(msgs)` (D8):** one `msgs` array = one wave; may mix tiers. `ctx.up` is - **control-tier only** (DIRTY/PAUSE/RESUME/INVALIDATE/TEARDOWN); DATA/RESOLVED/COMPLETE/ERROR are - down-only (R-ctx-up). Handle = pure data `(pool_id, handle_id)`, no methods (D7). -- **7-tier const table + 10-message closed set (D9/D34, R-tier/R-msg-closed-set):** adding a tier + **control/demand only** (DIRTY/PAUSE/RESUME/PULL/INVALIDATE/TEARDOWN); + START/DATA/RESOLVED/COMPLETE/ERROR are not up-going (R-ctx-up). Handle = pure data + `(pool_id, handle_id)`, no methods (D7). +- **7-tier const table + 11-message closed set (D9/D34/D269, R-tier/R-msg-closed-set):** adding a tier or message type is a constitutional change. -- **graph = single-thread causal/concurrency domain (D22):** parallelism via pool callback or - multi-graph + wire bridge; rewire intra-graph only. +- **graph = single-thread concurrency domain (D22):** causal influence may cross graphs through + delayed-consistency wire bridges; parallelism via pool callback or multi-graph + wire bridge; + rewire intra-graph only. - **parity = behavioral conformance (D24):** structural `Impl` + cross-track-ledger retired. - **config dissolved (D26):** clock is graph-local (no global singleton); `messageTier` is a compile-time const table; `onMessage`/`onSubscribe` are substrate-fixed, not user-replaceable (D19). diff --git a/packages/ts/src/__tests__/conformance.part-03.test.ts b/packages/ts/src/__tests__/conformance.part-03.test.ts index 3bed6ab2..d4383dd8 100644 --- a/packages/ts/src/__tests__/conformance.part-03.test.ts +++ b/packages/ts/src/__tests__/conformance.part-03.test.ts @@ -283,6 +283,13 @@ describe("C-26 — PULL is explicit demand with params; RESUME remains pause-onl ).not.toThrow(); expect(msgs).toEqual([]); expect(() => snap.up([["DATA", 3] as Message])).toThrow(/ctx\.up|control|demand|DATA/i); + expect(() => snap.up([["START"]])).toThrow(/ctx\.up|not up-going|START/i); + expect(() => snap.up([["DEMAND", { pullId }]] as unknown as Message[])).toThrow( + /closed message-type set/i, + ); + expect(() => snap.down([["DEMAND", { pullId }]] as unknown as Message[])).toThrow( + /closed message-type set/i, + ); acc.down([["DIRTY"]]); snap.up([["PULL", { pullId, params: { cursor: 1 } }]]); diff --git a/packages/ts/src/__tests__/core.test.ts b/packages/ts/src/__tests__/core.test.ts index f863941e..db89fbc2 100644 --- a/packages/ts/src/__tests__/core.test.ts +++ b/packages/ts/src/__tests__/core.test.ts @@ -176,13 +176,18 @@ describe("first-run gate (R-first-run-gate)", () => { }); describe("ctx.up direction guard (R-ctx-up)", () => { - it("throws on a down-only tier", () => { + it("rejects START, down-only tiers, and unknown tags while down rejects unknown tags", () => { const a = node([], null, { initial: 1 }); const d = node([a], (ctx: Ctx) => ctx.down([["DATA", 1]])); d.subscribe(() => {}); - expect(() => d.up([["DATA", 5]])).toThrow(/down-only/); - expect(() => d.up([["COMPLETE"]])).toThrow(/down-only/); + expect(() => d.up([["START"]])).toThrow(/not up-going/); + expect(() => d.up([["DATA", 5]])).toThrow(/not up-going/); + expect(() => d.up([["COMPLETE"]])).toThrow(/not up-going/); expect(() => d.up([["INVALIDATE"]])).not.toThrow(); + + const custom = [["CUSTOM"]] as unknown as Message[]; + expect(() => d.up(custom)).toThrow(/closed message-type set/); + expect(() => d.down(custom)).toThrow(/closed message-type set/); }); }); diff --git a/packages/ts/src/node/node-output-runtime.ts b/packages/ts/src/node/node-output-runtime.ts index 645023fb..896a8a64 100644 --- a/packages/ts/src/node/node-output-runtime.ts +++ b/packages/ts/src/node/node-output-runtime.ts @@ -223,9 +223,15 @@ export function nodeUp( if (self._released) return; const routeState = route ?? { demandFired: new Map() }; for (const m of msgs) { + const tier = messageTier(m[0]); + if (tier === undefined) { + throw new Error( + `ctx.up: ${String(m[0])} is not in the closed message-type set (R-msg-closed-set)`, + ); + } if (!isUpAllowed(m[0])) { throw new Error( - `ctx.up: ${m[0]} is down-only (tier ${messageTier(m[0])}); up carries control tiers only (R-ctx-up)`, + `ctx.up: ${m[0]} is not up-going (tier ${tier}); up carries control/demand messages only (R-ctx-up)`, ); } } diff --git a/packages/ts/src/node/protocol-guards.ts b/packages/ts/src/node/protocol-guards.ts index 68b697dd..4821cdd4 100644 --- a/packages/ts/src/node/protocol-guards.ts +++ b/packages/ts/src/node/protocol-guards.ts @@ -1,4 +1,9 @@ -import { isInvalidErrorPayload, type PullDemand, type Wave } from "../protocol/messages.js"; +import { + isInvalidErrorPayload, + messageTier, + type PullDemand, + type Wave, +} from "../protocol/messages.js"; export function terminalView(t: unknown): unknown { return t === undefined ? false : t; @@ -18,6 +23,11 @@ export function normalizePullDemand(demand: PullDemand): PullDemand { export function validateDownPayloads(msgs: Wave): void { for (const m of msgs) { + if (messageTier(m[0]) === undefined) { + throw new Error( + `down: ${String(m[0])} is not in the closed message-type set (R-msg-closed-set)`, + ); + } if (m[0] === "DATA" && m[1] === undefined) { throw new Error("down: DATA requires a non-SENTINEL payload (R-data-payload)"); } diff --git a/packages/ts/src/protocol/messages.ts b/packages/ts/src/protocol/messages.ts index cc15dac2..74716f75 100644 --- a/packages/ts/src/protocol/messages.ts +++ b/packages/ts/src/protocol/messages.ts @@ -182,5 +182,5 @@ export function isTerminal(t: MessageType): boolean { */ export function isUpAllowed(t: MessageType): boolean { const tier = TIER[t]; - return tier !== TIER_VALUE && tier !== TIER_TERMINAL; + return tier !== undefined && tier !== TIER_START && tier !== TIER_VALUE && tier !== TIER_TERMINAL; } From 175730038df72917d0cf25d2b5b535a7135f6315 Mon Sep 17 00:00:00 2001 From: David Chen Date: Wed, 29 Jul 2026 21:23:25 -0700 Subject: [PATCH 4/5] fix(ts): close D667 release contracts --- .changeset/closed-wave-directions.md | 14 ++++ package.json | 2 +- packages/ts/package.json | 4 +- .../local-untrusted-js-runner.mjs | 64 +++++++++---------- .../ts/runners/local-untrusted-js/runner.ts | 10 ++- ...cal-untrusted-js-compute-node.live.test.ts | 5 +- .../local-untrusted-js-compute-node.test.ts | 48 +++++++++++++- .../local-untrusted-js-compute.test.ts | 43 ++++++++++++- packages/ts/src/__tests__/subpaths.test.ts | 2 + .../executors/local-untrusted-js-compute.ts | 7 +- .../local-untrusted-js-compute/node.ts | 20 ++++-- packages/ts/tsup.config.ts | 9 +++ packages/ts/vitest.config.ts | 9 +++ scripts/build-local-untrusted-js-runner.mjs | 7 ++ .../src/content/docs/integrations/matrix.md | 2 + 15 files changed, 192 insertions(+), 54 deletions(-) create mode 100644 .changeset/closed-wave-directions.md diff --git a/.changeset/closed-wave-directions.md b/.changeset/closed-wave-directions.md new file mode 100644 index 00000000..5bdc1ffd --- /dev/null +++ b/.changeset/closed-wave-directions.md @@ -0,0 +1,14 @@ +--- +"@graphrefly/ts": patch +--- + +Reject unknown, START, value, and terminal messages on the upward path and reject +unknown messages on the downward path so generated fixed-runner artifacts retain +the package's closed wave-message direction guards. Pin runner bundle generation +to the repository root so package-scoped and root builds produce the same artifact. + +Expose the package-owned fixed runner assets through focused package subpaths, +bind runtime provenance to the package version injected into both the broker and +runner, and align v0 result/topology admission with the fixed profile. Start +readiness freshness after certification, retry cleanup verification through its +window, and make the locked Node 24 declaration build heap self-contained. diff --git a/package.json b/package.json index 524d5a30..658b6292 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "probe:b49:ts": "tsx packages/ts/src/__bench__/b49-probe.ts", "bench": "pnpm run probe:b49:ts", "changeset": "changeset", - "version-packages": "changeset version", + "version-packages": "changeset version && pnpm run build:local-untrusted-js-runner", "release": "pnpm run build && changeset publish", "lint": "biome check . && tsx scripts/check-no-raw-async.ts && tsx scripts/check-typecheck.ts && pnpm --filter @graphrefly/ts test:typecheck", "lint:no-raw-async": "tsx scripts/check-no-raw-async.ts", diff --git a/packages/ts/package.json b/packages/ts/package.json index 6570628c..8b7933dd 100644 --- a/packages/ts/package.json +++ b/packages/ts/package.json @@ -256,6 +256,8 @@ "default": "./dist/executors/local-untrusted-js-compute/node.cjs" } }, + "./executors/local-untrusted-js-compute/runner/Containerfile": "./runners/local-untrusted-js/Containerfile", + "./executors/local-untrusted-js-compute/runner/local-untrusted-js-runner.mjs": "./runners/local-untrusted-js/local-untrusted-js-runner.mjs", "./executors/managed-cloud-postgresql": { "import": { "types": "./dist/executors/managed-cloud-postgresql.d.ts", @@ -807,7 +809,7 @@ "access": "public" }, "scripts": { - "build": "node ../../scripts/build-local-untrusted-js-runner.mjs && tsup && node ../../scripts/check-ts-package-exports.mjs", + "build": "node ../../scripts/build-local-untrusted-js-runner.mjs && node --max-old-space-size=6144 ../../node_modules/tsup/dist/cli-default.js && node ../../scripts/check-ts-package-exports.mjs", "test": "vitest run", "test:typecheck": "tsc --noEmit -p tsconfig.tests.json", "test:browser:agentic-memory": "node ../../scripts/run-agentic-memory-indexeddb-smoke.mjs", diff --git a/packages/ts/runners/local-untrusted-js/local-untrusted-js-runner.mjs b/packages/ts/runners/local-untrusted-js/local-untrusted-js-runner.mjs index 639b61a6..ebdd2b71 100644 --- a/packages/ts/runners/local-untrusted-js/local-untrusted-js-runner.mjs +++ b/packages/ts/runners/local-untrusted-js/local-untrusted-js-runner.mjs @@ -1,9 +1,9 @@ -// runners/local-untrusted-js/runner.ts +// packages/ts/runners/local-untrusted-js/runner.ts import { createHash } from "node:crypto"; import { readFile } from "node:fs/promises"; import { createContext, Script, SourceTextModule } from "node:vm"; -// src/batch/boundary.ts +// packages/ts/src/batch/boundary.ts var depth = 0; var pendingCores = []; var pendingHead = 0; @@ -59,7 +59,7 @@ function drain() { if (escaped !== null) throw escaped.e; } -// src/batch/batch.ts +// packages/ts/src/batch/batch.ts var active = null; var boundaryOwner = null; function currentBatch() { @@ -139,7 +139,7 @@ function batch(fn) { } } -// src/protocol/messages.ts +// packages/ts/src/protocol/messages.ts var SENTINEL = void 0; function isInvalidErrorPayload(v) { return v === SENTINEL || typeof v === "boolean"; @@ -188,7 +188,7 @@ function isUpAllowed(t) { return tier !== void 0 && tier !== TIER_START && tier !== TIER_VALUE && tier !== TIER_TERMINAL; } -// src/ctx/types.ts +// packages/ts/src/ctx/types.ts var CTX_DEP_CACHE = /* @__PURE__ */ Symbol.for("graphrefly.ctx.depCache"); var ctxDepWaveOrigins = /* @__PURE__ */ new WeakMap(); function setCtxDepWaveOrigin(ctx, origin) { @@ -202,7 +202,7 @@ function depLatest(ctx, depIndex) { return ctx[CTX_DEP_CACHE]?.latest[depIndex]; } -// src/dispatcher/index.ts +// packages/ts/src/dispatcher/index.ts var PoolTable = class { constructor(kind) { this.kind = kind; @@ -311,7 +311,7 @@ var Dispatcher = class { }; var defaultDispatcher = new Dispatcher(); -// src/node/core.ts +// packages/ts/src/node/core.ts var NodeCore = class { nextId = 0; slots = []; @@ -459,7 +459,7 @@ function makeDepBookkeeping(depCount2) { }; } -// src/graph/environment.ts +// packages/ts/src/graph/environment.ts var EnvironmentDrivers = class _EnvironmentDrivers { process; http; @@ -510,7 +510,7 @@ var EnvironmentDrivers = class _EnvironmentDrivers { }; var EMPTY_ENVIRONMENT = new EnvironmentDrivers(); -// src/node/protocol-guards.ts +// packages/ts/src/node/protocol-guards.ts function terminalView(t) { return t === void 0 ? false : t; } @@ -541,7 +541,7 @@ function validateDownPayloads(msgs) { } } -// src/node/runtime-accessors.ts +// packages/ts/src/node/runtime-accessors.ts var constructingCore; var constructingEnvironment; var ownerTokens = /* @__PURE__ */ new WeakMap(); @@ -614,7 +614,7 @@ function isNodeRuntimeReleased(n) { return releasedNodes.has(n); } -// src/node/node-context-runtime.ts +// packages/ts/src/node/node-context-runtime.ts function nodeBuildCtx(self) { const kind = self._slot.handle ? self._slot.dispatcher.poolKind(self._slot.handle.poolId) : "sync"; if (kind === "sync") { @@ -710,7 +710,7 @@ function nodeMakeState(self) { }; } -// src/node/node-input-runtime.ts +// packages/ts/src/node/node-input-runtime.ts function nodeRecordDepProjection(self, idx, delivery) { const token = delivery?.wave ?? {}; if (self._dep.waveTokens[idx] !== token) { @@ -927,12 +927,12 @@ function nodeRunWave(self) { self._wave.emittedDirtyThisWave = false; } -// src/node/node-runtime-host.ts +// packages/ts/src/node/node-runtime-host.ts function nodeRuntimeHost(node) { return node; } -// src/node/node-lifecycle-runtime.ts +// packages/ts/src/node/node-lifecycle-runtime.ts function nodeActivate(self) { self._lifecycle.activated = true; const seedRestoredDeps = self._restoredActivationPending; @@ -1105,7 +1105,7 @@ function nodeResetDepState(self) { self._wave.emittedDirtyThisWave = false; } -// src/json/codec.ts +// packages/ts/src/json/codec.ts var JS_MIN_NORMAL_NUMBER = 2 ** -1022; function deepFreezeStrictJson(value) { if (value !== null && typeof value === "object") { @@ -1561,7 +1561,7 @@ function assertStrictJsonObject(value, label = "strictJsonObject") { return cloneStrictJsonObject(value, label); } -// src/node/versioning.ts +// packages/ts/src/node/versioning.ts var ABSENT_V1_SEED = Object.freeze({ "@graphrefly/node-version": "v1-absent" }); @@ -1639,7 +1639,7 @@ function restoredV1Cid(policy, hasData, cache) { return computeV1Cid(policy, hasData ? cache : ABSENT_V1_SEED); } -// src/node/node-output-runtime.ts +// packages/ts/src/node/node-output-runtime.ts function nodeDown(self, msgs) { if (self._released) return; validateDownPayloads(msgs); @@ -2015,7 +2015,7 @@ function nodeDeferBoundary(self, fn, batchToken) { }); } -// src/node/node-rewire-runtime.ts +// packages/ts/src/node/node-rewire-runtime.ts function nodeRequestRewireNext(self, op) { deferRewire(self._core, () => self._applyRewireNext(op), { batchToken: currentBoundaryBatchToken(), @@ -2170,7 +2170,7 @@ function nodeRewire(self, newDeps, fn, opts = {}) { return false; } -// src/node/node.ts +// packages/ts/src/node/node.ts var Node = class _Node { _core; _id; @@ -2740,12 +2740,12 @@ var Node = class _Node { } }; -// src/identity.ts +// packages/ts/src/identity.ts function canonicalTupleKey(parts) { return JSON.stringify(parts); } -// src/graph/blueprint.ts +// packages/ts/src/graph/blueprint.ts var GRAPH_BLUEPRINT_VERSION = "graphrefly.blueprint.v2"; function normalizeTopologyMeta(meta, label = "meta", kind = "graph meta") { try { @@ -2918,7 +2918,7 @@ function compareIssues(a, b) { return compareText(a.code, b.code) || compareText(a.nodeId ?? "", b.nodeId ?? "") || compareText(a.from ?? "", b.from ?? "") || compareText(a.to ?? "", b.to ?? ""); } -// src/graph/checkpoint.ts +// packages/ts/src/graph/checkpoint.ts var GRAPH_CHECKPOINT_VERSION = "graphrefly.checkpoint.v1"; function toCheckpointJson(value, path = "$") { try { @@ -2945,7 +2945,7 @@ function checkpointBackendStateOfNode(node, path) { return toCheckpointJson(contributor(), path); } -// src/graph/describe.ts +// packages/ts/src/graph/describe.ts function topologyFromDescribe(snapshot) { const nodes = snapshot.nodes.map((node) => { const topologyNode = { @@ -3008,11 +3008,11 @@ function cloneTopologyValue(value, seen) { return out; } -// src/graph/graph-lifecycle.ts +// packages/ts/src/graph/graph-lifecycle.ts var restoreRegistrars = /* @__PURE__ */ new WeakMap(); var lifecycleRegistrars = /* @__PURE__ */ new WeakMap(); -// src/graph/graph-support.ts +// packages/ts/src/graph/graph-support.ts function isNonAuthoritativeCollectionHelperMeta(meta) { return meta?.kind === "collection_delta" || meta?.kind === "collection_intent" || meta?.kind === "collection_policy_apply" || meta?.kind === "collection_snapshot" || meta?.kind === "collection_snapshot_prep"; } @@ -3111,7 +3111,7 @@ function explainSubset(snap, chain) { }; } -// src/graph/graph-topology-group.ts +// packages/ts/src/graph/graph-topology-group.ts var GraphTopologyGroup = class { name; _graph; @@ -3173,7 +3173,7 @@ var GraphTopologyGroup = class { } }; -// src/graph/operators.ts +// packages/ts/src/graph/operators.ts function initNodeWithCore(core, op, deps, opts = {}) { return withNodeCore(core, () => makeInitNode(op, deps, opts)); } @@ -3191,7 +3191,7 @@ function operatorNodeFn(op) { }; } -// src/graph/graph.ts +// packages/ts/src/graph/graph.ts function nodeOwner(n) { return getNodeOwner(n); } @@ -3890,9 +3890,10 @@ function graph(opts = {}) { return new Graph(opts); } -// runners/local-untrusted-js/runner.ts +// packages/ts/runners/local-untrusted-js/runner.ts var COMPATIBILITY_REVISION = "graphrefly-local-untrusted-js-compute-v1"; var RUNNER_API_REVISION = "graphrefly-runner-api-v1"; +var GRAPHREFLY_PACKAGE_REVISION = "graphrefly-ts:0.7.0"; var INPUT_PATHS = ["/input/bundle.mjs", "/input/input.json", "/input/control.json"]; var MAX_RUNNER_NODES = 1e3; var MAX_RUNNER_EDGES = 2e3; @@ -4099,11 +4100,10 @@ function parseControl(value) { ], "Runner control" ); - if (value.contractVersion !== "1" || value.compatibilityRevision !== COMPATIBILITY_REVISION || value.runnerApiRevision !== RUNNER_API_REVISION) + if (value.contractVersion !== "1" || value.compatibilityRevision !== COMPATIBILITY_REVISION || value.runnerApiRevision !== RUNNER_API_REVISION || !record(value.args) || value.args.graphreflyPackageRevision !== GRAPHREFLY_PACKAGE_REVISION) throw new TypeError("Runner control identity is invalid."); safeString(value.manifestFingerprint, "Manifest fingerprint"); safeString(value.runAdmissionId, "Run admission id"); - if (!record(value.args)) throw new TypeError("Runner arguments must be an object."); const args = value.args; for (const key of [ "runId", @@ -4261,7 +4261,7 @@ function materializeResult(evaluated, control) { bundleDigest: control.args.bundleDigest, compilerRevision: control.args.compilerRevision, allowedApiRevision: control.args.allowedApiRevision, - graphreflyPackageRevision: control.args.graphreflyPackageRevision, + graphreflyPackageRevision: GRAPHREFLY_PACKAGE_REVISION, runnerRevision: control.args.runnerRevision, runnerImageDigest: control.args.runnerImageDigest, manifestFingerprint: control.manifestFingerprint, diff --git a/packages/ts/runners/local-untrusted-js/runner.ts b/packages/ts/runners/local-untrusted-js/runner.ts index 8398f81e..d4486b06 100644 --- a/packages/ts/runners/local-untrusted-js/runner.ts +++ b/packages/ts/runners/local-untrusted-js/runner.ts @@ -5,8 +5,11 @@ import { graph } from "../../src/graph/graph.js"; import { strictCanonicalJsonBytes } from "../../src/json/codec.js"; import type { Node } from "../../src/node/node.js"; +declare const __GRAPHREFLY_TS_PACKAGE_REVISION__: string; + const COMPATIBILITY_REVISION = "graphrefly-local-untrusted-js-compute-v1"; const RUNNER_API_REVISION = "graphrefly-runner-api-v1"; +const GRAPHREFLY_PACKAGE_REVISION = __GRAPHREFLY_TS_PACKAGE_REVISION__; const INPUT_PATHS = ["/input/bundle.mjs", "/input/input.json", "/input/control.json"] as const; const MAX_RUNNER_NODES = 1_000; const MAX_RUNNER_EDGES = 2_000; @@ -284,12 +287,13 @@ function parseControl(value: unknown): RunnerControl { if ( value.contractVersion !== "1" || value.compatibilityRevision !== COMPATIBILITY_REVISION || - value.runnerApiRevision !== RUNNER_API_REVISION + value.runnerApiRevision !== RUNNER_API_REVISION || + !record(value.args) || + value.args.graphreflyPackageRevision !== GRAPHREFLY_PACKAGE_REVISION ) throw new TypeError("Runner control identity is invalid."); safeString(value.manifestFingerprint, "Manifest fingerprint"); safeString(value.runAdmissionId, "Run admission id"); - if (!record(value.args)) throw new TypeError("Runner arguments must be an object."); const args = value.args; for (const key of [ "runId", @@ -490,7 +494,7 @@ function materializeResult( bundleDigest: control.args.bundleDigest, compilerRevision: control.args.compilerRevision, allowedApiRevision: control.args.allowedApiRevision, - graphreflyPackageRevision: control.args.graphreflyPackageRevision, + graphreflyPackageRevision: GRAPHREFLY_PACKAGE_REVISION, runnerRevision: control.args.runnerRevision, runnerImageDigest: control.args.runnerImageDigest, manifestFingerprint: control.manifestFingerprint, diff --git a/packages/ts/src/__tests__/local-untrusted-js-compute-node.live.test.ts b/packages/ts/src/__tests__/local-untrusted-js-compute-node.live.test.ts index 2f718e1a..62c36e9d 100644 --- a/packages/ts/src/__tests__/local-untrusted-js-compute-node.live.test.ts +++ b/packages/ts/src/__tests__/local-untrusted-js-compute-node.live.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest"; import { certifyNodeLocalUntrustedJsCompute, NODE_LOCAL_UNTRUSTED_JS_ALLOWED_API_REVISION, + NODE_LOCAL_UNTRUSTED_JS_GRAPHREFLY_PACKAGE_REVISION, NODE_LOCAL_UNTRUSTED_JS_OUTPUT_POLICY_REVISION, NODE_LOCAL_UNTRUSTED_JS_RESOURCE_POLICY_REVISION, NODE_LOCAL_UNTRUSTED_JS_RUNNER_REVISION, @@ -46,7 +47,7 @@ function positiveManifest(runnerImageDigest: string): LocalUntrustedJsComputeMan runnerRevision: NODE_LOCAL_UNTRUSTED_JS_RUNNER_REVISION, compilerRevision: "compiler:esbuild-fixed-v0", allowedApiRevision: NODE_LOCAL_UNTRUSTED_JS_ALLOWED_API_REVISION, - graphreflyPackageRevision: "graphrefly-ts:0.7.0", + graphreflyPackageRevision: NODE_LOCAL_UNTRUSTED_JS_GRAPHREFLY_PACKAGE_REVISION, sandboxPolicyRevision: NODE_LOCAL_UNTRUSTED_JS_SANDBOX_POLICY_REVISION, networkPolicyRevision: "deny-all-v1", filesystemPolicyRevision: "read-only-input-bounded-tmp-v1", @@ -78,7 +79,7 @@ describe.runIf(live)("D667 Node rootless-Podman broker live negative", () => { runnerRevision: "runner:missing-negative-control", compilerRevision: "compiler:negative-control", allowedApiRevision: "api:negative-control", - graphreflyPackageRevision: "graphrefly-ts:0.7.0", + graphreflyPackageRevision: NODE_LOCAL_UNTRUSTED_JS_GRAPHREFLY_PACKAGE_REVISION, sandboxPolicyRevision: "sandbox:d667-v0", networkPolicyRevision: "deny-all-v1", filesystemPolicyRevision: "read-only-input-bounded-tmp-v1", diff --git a/packages/ts/src/__tests__/local-untrusted-js-compute-node.test.ts b/packages/ts/src/__tests__/local-untrusted-js-compute-node.test.ts index 284da375..a2ad1a31 100644 --- a/packages/ts/src/__tests__/local-untrusted-js-compute-node.test.ts +++ b/packages/ts/src/__tests__/local-untrusted-js-compute-node.test.ts @@ -6,6 +6,13 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; import { + certifyNodeLocalUntrustedJsCompute, + NODE_LOCAL_UNTRUSTED_JS_ALLOWED_API_REVISION, + NODE_LOCAL_UNTRUSTED_JS_GRAPHREFLY_PACKAGE_REVISION, + NODE_LOCAL_UNTRUSTED_JS_OUTPUT_POLICY_REVISION, + NODE_LOCAL_UNTRUSTED_JS_RESOURCE_POLICY_REVISION, + NODE_LOCAL_UNTRUSTED_JS_RUNNER_REVISION, + NODE_LOCAL_UNTRUSTED_JS_SANDBOX_POLICY_REVISION, nodeLocalUntrustedJsComputeDriver, nodeLocalUntrustedJsComputeHostBindingAttestation, } from "../executors/local-untrusted-js-compute/node.js"; @@ -49,6 +56,7 @@ interface HarnessState { | "strict-exit" | "delayed-create" | "delayed-create-transient-control" + | "transient-settled-cleanup" | "unrelated-list"; created: boolean; deleted: boolean; @@ -82,7 +90,7 @@ const manifest = (overrides: Partial = {}) => runnerRevision: "runner:harness", compilerRevision: "compiler:harness", allowedApiRevision: "api:harness", - graphreflyPackageRevision: "graphrefly-ts:0.7.0", + graphreflyPackageRevision: NODE_LOCAL_UNTRUSTED_JS_GRAPHREFLY_PACKAGE_REVISION, sandboxPolicyRevision: "sandbox:d667-v0", networkPolicyRevision: "deny-all-v1", filesystemPolicyRevision: "read-only-input-bounded-tmp-v1", @@ -110,7 +118,7 @@ const args = (): LocalUntrustedJsComputeArguments => ({ bundleDigest: digest(bundle), compilerRevision: "compiler:harness", allowedApiRevision: "api:harness", - graphreflyPackageRevision: "graphrefly-ts:0.7.0", + graphreflyPackageRevision: NODE_LOCAL_UNTRUSTED_JS_GRAPHREFLY_PACKAGE_REVISION, runnerRevision: "runner:harness", runnerImageDigest: imageDigest, sandboxPolicyRevision: "sandbox:d667-v0", @@ -190,7 +198,10 @@ async function harness(mode: HarnessState["mode"]): Promise<{ return; } if (path.includes("/libpod/containers/json?")) { - if (mode === "delayed-create-transient-control" && state.transientListFailures < 2) { + if ( + (mode === "delayed-create-transient-control" || mode === "transient-settled-cleanup") && + state.transientListFailures < 2 + ) { state.transientListFailures += 1; sendJson(response, 503, { message: "transient list failure" }); return; @@ -394,6 +405,37 @@ describe("D667 Node rootless-Podman broker lifecycle", () => { }); }); + it("retries transient settled-create verification failures through the cleanup window", async () => { + const { state, hostBindingDigest } = await harness("transient-settled-cleanup"); + await expect( + execute(hostBindingDigest, manifest({ cleanupTimeoutMs: 400 })), + ).resolves.toMatchObject({ + outcome: "succeeded", + cleanup: "succeeded", + }); + expect(state).toMatchObject({ + deleted: true, + transientListFailures: 2, + }); + }); + + it("rejects a caller-authored package revision before fixed-profile certification", async () => { + await expect( + certifyNodeLocalUntrustedJsCompute({ + manifest: manifest({ + runnerRevision: NODE_LOCAL_UNTRUSTED_JS_RUNNER_REVISION, + allowedApiRevision: NODE_LOCAL_UNTRUSTED_JS_ALLOWED_API_REVISION, + graphreflyPackageRevision: "graphrefly-ts:caller-authored", + sandboxPolicyRevision: NODE_LOCAL_UNTRUSTED_JS_SANDBOX_POLICY_REVISION, + resourcePolicyRevision: NODE_LOCAL_UNTRUSTED_JS_RESOURCE_POLICY_REVISION, + outputPolicyRevision: NODE_LOCAL_UNTRUSTED_JS_OUTPUT_POLICY_REVISION, + executionTimeoutMs: 2_000, + }), + imageRef, + }), + ).rejects.toThrow("package-owned fixed runner profile"); + }); + it("refuses an unrelated ID even when an ignored filter returns matching labels and name", async () => { const { state, hostBindingDigest } = await harness("unrelated-list"); await expect(execute(hostBindingDigest, manifest())).resolves.toMatchObject({ diff --git a/packages/ts/src/__tests__/local-untrusted-js-compute.test.ts b/packages/ts/src/__tests__/local-untrusted-js-compute.test.ts index bcaa3142..4e140722 100644 --- a/packages/ts/src/__tests__/local-untrusted-js-compute.test.ts +++ b/packages/ts/src/__tests__/local-untrusted-js-compute.test.ts @@ -1,6 +1,9 @@ import { createHash } from "node:crypto"; import { describe, expect, it, vi } from "vitest"; -import { nodeLocalUntrustedJsComputeDriver } from "../executors/local-untrusted-js-compute/node.js"; +import { + NODE_LOCAL_UNTRUSTED_JS_GRAPHREFLY_PACKAGE_REVISION, + nodeLocalUntrustedJsComputeDriver, +} from "../executors/local-untrusted-js-compute/node.js"; import { LOCAL_UNTRUSTED_JS_COMPUTE_BACKEND, LOCAL_UNTRUSTED_JS_COMPUTE_COMPATIBILITY, @@ -10,6 +13,7 @@ import { type LocalUntrustedJsComputeDriver, type LocalUntrustedJsComputeManifest, type LocalUntrustedJsComputeReadiness, + localUntrustedJsComputeManifest, localUntrustedJsComputeRuntime, runLocalUntrustedJsComputeAttempt, } from "../executors/local-untrusted-js-compute.js"; @@ -45,7 +49,7 @@ const manifest = (): LocalUntrustedJsComputeManifest => ({ runnerRevision: "runner:1", compilerRevision: "compiler:1", allowedApiRevision: "api:1", - graphreflyPackageRevision: "graphrefly-ts:0.7.0", + graphreflyPackageRevision: NODE_LOCAL_UNTRUSTED_JS_GRAPHREFLY_PACKAGE_REVISION, sandboxPolicyRevision: "sandbox:1", networkPolicyRevision: "deny-all-v1", filesystemPolicyRevision: "read-only-input-bounded-tmp-v1", @@ -95,7 +99,7 @@ const args = (): LocalUntrustedJsComputeArguments => ({ bundleDigest, compilerRevision: "compiler:1", allowedApiRevision: "api:1", - graphreflyPackageRevision: "graphrefly-ts:0.7.0", + graphreflyPackageRevision: NODE_LOCAL_UNTRUSTED_JS_GRAPHREFLY_PACKAGE_REVISION, runnerRevision: "runner:1", runnerImageDigest: imageDigest, sandboxPolicyRevision: "sandbox:1", @@ -249,6 +253,39 @@ const eventually = async (predicate: () => boolean): Promise => { }; describe("D667 local untrusted JS compute contract", () => { + it("admits only the fixed v0 result and topology ceilings", () => { + expect( + localUntrustedJsComputeManifest({ + ...manifest(), + maxOutputBytes: 1024 * 1024, + maxTopologyNodes: 1_000, + maxTopologyEdges: 2_000, + }), + ).toMatchObject({ + maxOutputBytes: 1024 * 1024, + maxTopologyNodes: 1_000, + maxTopologyEdges: 2_000, + }); + expect(() => + localUntrustedJsComputeManifest({ + ...manifest(), + maxOutputBytes: 1024 * 1024 + 1, + }), + ).toThrow("v0 ceiling"); + expect(() => + localUntrustedJsComputeManifest({ + ...manifest(), + maxTopologyNodes: 1_001, + }), + ).toThrow("v0 ceiling"); + expect(() => + localUntrustedJsComputeManifest({ + ...manifest(), + maxTopologyEdges: 2_001, + }), + ).toThrow("v0 ceiling"); + }); + it("runs one exact admitted attempt and returns bounded actual topology/provenance only after cleanup", async () => { const driver = successfulDriver(); const outcome = await runLocalUntrustedJsComputeAttempt({ diff --git a/packages/ts/src/__tests__/subpaths.test.ts b/packages/ts/src/__tests__/subpaths.test.ts index 1f4c1cbf..d2f510d5 100644 --- a/packages/ts/src/__tests__/subpaths.test.ts +++ b/packages/ts/src/__tests__/subpaths.test.ts @@ -262,6 +262,8 @@ describe("package subpath barrels (D40/D41 intent parity)", () => { "./executors/local-container-postgresql-podman-libpod-api-v0-rootless/node", "./executors/local-untrusted-js-compute", "./executors/local-untrusted-js-compute/node", + "./executors/local-untrusted-js-compute/runner/Containerfile", + "./executors/local-untrusted-js-compute/runner/local-untrusted-js-runner.mjs", "./executors/managed-cloud-postgresql", "./executors/managed-untrusted-js-compute", "./executors/postgresql-run-operations", diff --git a/packages/ts/src/executors/local-untrusted-js-compute.ts b/packages/ts/src/executors/local-untrusted-js-compute.ts index 9a11123b..ff18a25a 100644 --- a/packages/ts/src/executors/local-untrusted-js-compute.ts +++ b/packages/ts/src/executors/local-untrusted-js-compute.ts @@ -30,8 +30,9 @@ const MAX_EXECUTION_TIMEOUT_MS = 5 * 60_000; const MAX_KILL_GRACE_MS = 60_000; const MAX_CLEANUP_TIMEOUT_MS = 5 * 60_000; const MAX_MATERIAL_BYTES = 16 * 1024 * 1024; -const MAX_TOPOLOGY_NODES = 100_000; -const MAX_TOPOLOGY_EDGES = 200_000; +const MAX_OUTPUT_BYTES = 1024 * 1024; +const MAX_TOPOLOGY_NODES = 1_000; +const MAX_TOPOLOGY_EDGES = 2_000; export type LocalUntrustedJsJson = | null @@ -1027,7 +1028,7 @@ export function localUntrustedJsComputeManifest( value.cleanupTimeoutMs > MAX_CLEANUP_TIMEOUT_MS || value.maxBundleBytes > MAX_MATERIAL_BYTES || value.maxInputBytes > MAX_MATERIAL_BYTES || - value.maxOutputBytes > MAX_MATERIAL_BYTES || + value.maxOutputBytes > MAX_OUTPUT_BYTES || value.maxTopologyNodes > MAX_TOPOLOGY_NODES || value.maxTopologyEdges > MAX_TOPOLOGY_EDGES ) diff --git a/packages/ts/src/executors/local-untrusted-js-compute/node.ts b/packages/ts/src/executors/local-untrusted-js-compute/node.ts index 788f1b36..ebe0b0c8 100644 --- a/packages/ts/src/executors/local-untrusted-js-compute/node.ts +++ b/packages/ts/src/executors/local-untrusted-js-compute/node.ts @@ -25,6 +25,8 @@ import { localUntrustedJsComputeReadiness, } from "../local-untrusted-js-compute.js"; +declare const __GRAPHREFLY_TS_PACKAGE_REVISION__: string; + const execFileAsync = promisify(execFile); const API_REVISION = "5.0.3"; const RUNNER_ENTRYPOINT = [ @@ -37,7 +39,7 @@ const RUNNER_ENTRYPOINT = [ const BOUNDARY_LABEL = "d667-local-untrusted-js-compute"; const DEFAULT_REQUEST_TIMEOUT_MS = 10_000; const MAX_ENGINE_RESPONSE_BYTES = 512 * 1024; -const MAX_LOG_BYTES = 1024 * 1024; +const MAX_LOG_BYTES = 1024 * 1024 + 8 * 1024; const MAX_CONTROL_BYTES = 32 * 1024; const MEMORY_LIMIT_BYTES = 256 * 1024 * 1024; const TMPFS_LIMIT_BYTES = 16 * 1024 * 1024; @@ -53,6 +55,8 @@ const CERTIFICATION_TTL_MS = 5 * 60_000; const CERTIFICATION_CANCEL_DELAY_MS = 500; export const NODE_LOCAL_UNTRUSTED_JS_RUNNER_REVISION = "graphrefly-local-js-runner-v0" as const; +export const NODE_LOCAL_UNTRUSTED_JS_GRAPHREFLY_PACKAGE_REVISION: string = + __GRAPHREFLY_TS_PACKAGE_REVISION__; export const NODE_LOCAL_UNTRUSTED_JS_ALLOWED_API_REVISION = "graphrefly-source-derive-api-v0" as const; export const NODE_LOCAL_UNTRUSTED_JS_SANDBOX_POLICY_REVISION = "podman-vm-no-imports-v0" as const; @@ -131,13 +135,11 @@ export async function certifyNodeLocalUntrustedJsCompute( manifest.sandboxPolicyRevision !== NODE_LOCAL_UNTRUSTED_JS_SANDBOX_POLICY_REVISION || manifest.resourcePolicyRevision !== NODE_LOCAL_UNTRUSTED_JS_RESOURCE_POLICY_REVISION || manifest.outputPolicyRevision !== NODE_LOCAL_UNTRUSTED_JS_OUTPUT_POLICY_REVISION || + manifest.graphreflyPackageRevision !== NODE_LOCAL_UNTRUSTED_JS_GRAPHREFLY_PACKAGE_REVISION || manifest.executionTimeoutMs < 2_000 || !opts.imageRef.endsWith(`@${manifest.runnerImageDigest}`) ) throw new TypeError("D667 certifier requires the package-owned fixed runner profile."); - const observedAtMs = (opts.now ?? Date.now)(); - if (!Number.isSafeInteger(observedAtMs)) - throw new TypeError("D667 certification clock is invalid."); const host = await nodeLocalUntrustedJsComputeHostBindingAttestation(opts.signal); const driver = nodeLocalUntrustedJsComputeDriver({ imageRef: opts.imageRef }); const probeSuffix = randomUUID(); @@ -354,6 +356,9 @@ export async function certifyNodeLocalUntrustedJsCompute( throw new TypeError( `D667 fixed runner cancellation and cleanup probe failed (${canceled.outcome}/${"code" in canceled ? canceled.code : "none"}/${canceled.cleanup}).`, ); + const observedAtMs = (opts.now ?? Date.now)(); + if (!Number.isSafeInteger(observedAtMs)) + throw new TypeError("D667 certification clock is invalid."); return localUntrustedJsComputeReadiness( { kind: "local-untrusted-js-compute-readiness", @@ -426,6 +431,11 @@ async function executeAttempt( try { if (!imageRef.endsWith(`@${manifest.runnerImageDigest}`)) throw new TypeError("D667 runner image does not match the admitted manifest digest."); + if ( + manifest.graphreflyPackageRevision !== NODE_LOCAL_UNTRUSTED_JS_GRAPHREFLY_PACKAGE_REVISION || + args.graphreflyPackageRevision !== NODE_LOCAL_UNTRUSTED_JS_GRAPHREFLY_PACKAGE_REVISION + ) + throw new TypeError("D667 package revision does not match the fixed runner profile."); const observedBundleDigest = `sha256:${createHash("sha256").update(material.bundle).digest("hex")}`; if (observedBundleDigest !== args.bundleDigest) throw new TypeError("D667 bundle bytes do not match the admitted digest."); @@ -778,7 +788,6 @@ async function removeAndVerify( const residues = await labeledResidues(binding, signal, deadline); if (residues === undefined) { absentSince = undefined; - if (createRequestSettled) return false; await delay(20, signal).catch(() => undefined); continue; } @@ -810,7 +819,6 @@ async function removeAndVerify( ).catch(() => undefined); if (absent?.status !== 404) { absentSince = undefined; - if (createRequestSettled) return false; await delay(20, signal).catch(() => undefined); continue; } diff --git a/packages/ts/tsup.config.ts b/packages/ts/tsup.config.ts index 6e0d3e2c..a0d766ac 100644 --- a/packages/ts/tsup.config.ts +++ b/packages/ts/tsup.config.ts @@ -1,5 +1,11 @@ +import { readFileSync } from "node:fs"; import { defineConfig } from "tsup"; +const packageMetadata = JSON.parse( + readFileSync(new URL("./package.json", import.meta.url), "utf8"), +); +const packageRevision = `graphrefly-ts:${packageMetadata.version}`; + export default defineConfig({ entry: [ "src/index.ts", @@ -73,6 +79,9 @@ export default defineConfig({ "src/work-queue/index.ts", ], format: ["esm", "cjs"], + define: { + __GRAPHREFLY_TS_PACKAGE_REVISION__: JSON.stringify(packageRevision), + }, dts: true, clean: true, sourcemap: true, diff --git a/packages/ts/vitest.config.ts b/packages/ts/vitest.config.ts index 2f671880..b55a2413 100644 --- a/packages/ts/vitest.config.ts +++ b/packages/ts/vitest.config.ts @@ -1,6 +1,15 @@ +import { readFileSync } from "node:fs"; import { defineConfig } from "vitest/config"; +const packageMetadata = JSON.parse( + readFileSync(new URL("./package.json", import.meta.url), "utf8"), +); +const packageRevision = `graphrefly-ts:${packageMetadata.version}`; + export default defineConfig({ + define: { + __GRAPHREFLY_TS_PACKAGE_REVISION__: JSON.stringify(packageRevision), + }, test: { include: ["src/**/*.test.ts"], }, diff --git a/scripts/build-local-untrusted-js-runner.mjs b/scripts/build-local-untrusted-js-runner.mjs index 0a393b27..8b2f689a 100644 --- a/scripts/build-local-untrusted-js-runner.mjs +++ b/scripts/build-local-untrusted-js-runner.mjs @@ -1,9 +1,12 @@ import { spawnSync } from "node:child_process"; +import { readFileSync } from "node:fs"; import { resolve } from "node:path"; import { build } from "esbuild"; const root = resolve(import.meta.dirname, ".."); const runnerDirectory = resolve(root, "packages/ts/runners/local-untrusted-js"); +const packageMetadata = JSON.parse(readFileSync(resolve(root, "packages/ts/package.json"), "utf8")); +const packageRevision = `graphrefly-ts:${packageMetadata.version}`; const checked = spawnSync( process.execPath, [ @@ -19,12 +22,16 @@ if (checked.status !== 0) throw new Error(`Local untrusted JS runner typecheck failed (${checked.status ?? "signal"}).`); await build({ + absWorkingDir: root, entryPoints: [resolve(runnerDirectory, "runner.ts")], outfile: resolve(runnerDirectory, "local-untrusted-js-runner.mjs"), bundle: true, format: "esm", platform: "node", target: "node22", + define: { + __GRAPHREFLY_TS_PACKAGE_REVISION__: JSON.stringify(packageRevision), + }, sourcemap: false, minify: false, legalComments: "none", diff --git a/website/src/content/docs/integrations/matrix.md b/website/src/content/docs/integrations/matrix.md index 71798d5d..c9cf3a1b 100644 --- a/website/src/content/docs/integrations/matrix.md +++ b/website/src/content/docs/integrations/matrix.md @@ -57,6 +57,8 @@ See [Adapters](/integrations/adapters/) for usage guidance and naming convention | Node-local rootless Podman Libpod API v0 certifier | D645 Node-only candidate certifier with package-owned host coordinates, bounded CLI socket discovery, native Libpod requests, and private resource handles; it remains unavailable until every required live effect probe is certified | `@graphrefly/ts/executors/local-container-postgresql-podman-libpod-api-v0-rootless/node` | | Local untrusted JavaScript compute contract | D667 host-neutral, exact-coordinate attempt contract for immutable JavaScript bundles, admitted JSON inputs, bounded GraphReFly runner results, runtime topology, provenance, cancellation, and verified cleanup | `@graphrefly/ts/executors/local-untrusted-js-compute` | | Node-local untrusted JavaScript Podman broker and certifier | D667 Node-only rootless-Podman broker plus package-owned distroless runner artifact and live certifier: admitted bundles receive only the fixed graph/source/derive API and admitted JSON; static/dynamic imports and ambient process/require/fetch are denied; the digest-pinned image runs non-root with a read-only rootfs, no host or engine-socket mount, one bounded noexec tmpfs, no network, bounded resources, exact cancellation, verified 0N/0E Graph disposal, and terminal container removal | `@graphrefly/ts/executors/local-untrusted-js-compute/node` | +| Local untrusted JavaScript fixed runner Containerfile | D667 package-owned immutable image recipe, exported only under the focused compute family so installed hosts can build the certified runner without traversing package internals | `@graphrefly/ts/executors/local-untrusted-js-compute/runner/Containerfile` | +| Local untrusted JavaScript fixed runner bundle | D667 package-version-bound runner bundle consumed by the focused Containerfile; it is an asset subpath, not a package root API | `@graphrefly/ts/executors/local-untrusted-js-compute/runner/local-untrusted-js-runner.mjs` | | Managed-cloud PostgreSQL binding | PostgreSQL-16 atomic control-store and worker-initiated WSS session lifecycle with exact fenced leases, cancellation, and settlement | `@graphrefly/ts/executors/managed-cloud-postgresql` | | Managed untrusted JS compute | E2B Cloud v0 untrusted JavaScript compute lifecycle with deny-all network, bounded movement evidence, exact cancellation, and independently visible cleanup | `@graphrefly/ts/executors/managed-untrusted-js-compute` | | Customer-hosted PostgreSQL binding | Signed digest-pinned endpoint agent, outbound authenticated WSS, customer-resident credentials, exact cross-domain fences, and encrypted evidence-only offline outbox | `@graphrefly/ts/executors/customer-hosted-postgresql` | From 0ead8c2d262fbbfecc2ae7c2bcbab48796e014d3 Mon Sep 17 00:00:00 2001 From: David Chen Date: Wed, 29 Jul 2026 21:29:02 -0700 Subject: [PATCH 5/5] fix(ts): settle bounded Podman responses once --- .../local-untrusted-js-compute-node.test.ts | 9 +++++++- .../local-untrusted-js-compute/node.ts | 22 ++++++++++++++----- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/packages/ts/src/__tests__/local-untrusted-js-compute-node.test.ts b/packages/ts/src/__tests__/local-untrusted-js-compute-node.test.ts index a2ad1a31..af14d05f 100644 --- a/packages/ts/src/__tests__/local-untrusted-js-compute-node.test.ts +++ b/packages/ts/src/__tests__/local-untrusted-js-compute-node.test.ts @@ -477,7 +477,14 @@ describe("D667 Node rootless-Podman broker lifecycle", () => { }); it("fails closed on bounded log overflow and strict exit-code parsing", async () => { - for (const mode of ["output-overflow", "strict-exit"] as const) { + for (const mode of [ + "output-overflow", + "output-overflow", + "output-overflow", + "output-overflow", + "output-overflow", + "strict-exit", + ] as const) { const { state, hostBindingDigest } = await harness(mode); await expect(execute(hostBindingDigest, manifest())).resolves.toMatchObject({ outcome: "failed", diff --git a/packages/ts/src/executors/local-untrusted-js-compute/node.ts b/packages/ts/src/executors/local-untrusted-js-compute/node.ts index ebe0b0c8..df2c2e4c 100644 --- a/packages/ts/src/executors/local-untrusted-js-compute/node.ts +++ b/packages/ts/src/executors/local-untrusted-js-compute/node.ts @@ -1017,6 +1017,12 @@ function rawRequest( maxResponseBytes = MAX_ENGINE_RESPONSE_BYTES, ): Promise { return new Promise((resolve, reject) => { + let settled = false; + const rejectOnce = (error: unknown): void => { + if (settled) return; + settled = true; + reject(error); + }; const request = httpRequest( { socketPath, @@ -1037,22 +1043,26 @@ function rawRequest( response.on("data", (chunk: Buffer) => { total += chunk.byteLength; if (total > maxResponseBytes) { - request.destroy(new ResponseBoundError()); + rejectOnce(new ResponseBoundError()); + response.destroy(); + request.destroy(); return; } chunks.push(chunk); }); - response.on("end", () => + response.on("end", () => { + if (settled) return; + settled = true; resolve({ status: response.statusCode ?? 0, body: new Uint8Array(Buffer.concat(chunks)), - }), - ); - response.on("error", reject); + }); + }); + response.on("error", rejectOnce); }, ); request.setTimeout(timeoutMs, () => request.destroy(new Error("Podman request timed out."))); - request.on("error", reject); + request.on("error", rejectOnce); if (body !== undefined) request.write(body); request.end(); });