From 99da1da319aeabb3450f33006693f70d49d33574 Mon Sep 17 00:00:00 2001 From: Jax Liu Date: Mon, 7 Sep 2026 13:02:23 +0800 Subject: [PATCH] fix(genbi): keep managed runtime readiness read-only --- .github/workflows/managed-wren-runtime.yml | 3 +- apps/genbi/scripts/probe-managed-wren-pbs.mjs | 30 +++++++++++--- apps/genbi/server/bin.ts | 21 ++++------ apps/genbi/server/managed-wren-runtime.ts | 18 ++++++++- apps/genbi/test/managed-wren-runtime.test.ts | 40 ++++++++++++++++++- 5 files changed, 90 insertions(+), 22 deletions(-) diff --git a/.github/workflows/managed-wren-runtime.yml b/.github/workflows/managed-wren-runtime.yml index 7b1031e90..428b3e9ed 100644 --- a/.github/workflows/managed-wren-runtime.yml +++ b/.github/workflows/managed-wren-runtime.yml @@ -46,7 +46,8 @@ jobs: curl --fail --location --proto '=https' --tlsv1.2 -o release/python.tar.gz 'https://github.com/astral-sh/python-build-standalone/releases/download/20260901/cpython-3.11.16%2B20260901-aarch64-apple-darwin-install_only.tar.gz' test "$(shasum -a 256 release/python.tar.gz | awk '{print $1}')" = 50424fa409e8ae84b82a3052522f64695b47dff2158b70bb7358e0ebd6c085c9 /usr/bin/tar -tzf release/python.tar.gz | python -c "import json,sys; paths=[p.strip() for p in sys.stdin if any(x in p.upper() for x in ('LICENSE','COPYING','NOTICE'))]; print(json.dumps({'schema':1,'archiveSha256':'50424fa409e8ae84b82a3052522f64695b47dff2158b70bb7358e0ebd6c085c9','bundledLicenseEvidence':[{'path':p,'declaredLicense':'REVIEW_REQUIRED'} for p in paths]},indent=2))" > release/pbs-license-inventory.json - /usr/bin/tar -xzf release/python.tar.gz -C release/runtime + /usr/bin/tar -xpzf release/python.tar.gz -C release/runtime + /bin/chmod 700 release/runtime release/runtime/python/bin/python3.11 -m venv release/runtime/venv python - <<'PY' import json, pathlib diff --git a/apps/genbi/scripts/probe-managed-wren-pbs.mjs b/apps/genbi/scripts/probe-managed-wren-pbs.mjs index f3395325a..a34f22c6a 100644 --- a/apps/genbi/scripts/probe-managed-wren-pbs.mjs +++ b/apps/genbi/scripts/probe-managed-wren-pbs.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node /** Non-publishing reproducibility probe for the exact managed PBS input. */ import { createHash } from "node:crypto"; -import { chmod, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { chmod, lstat, mkdtemp, readFile, readdir, readlink, realpath, rm, writeFile } from "node:fs/promises"; import { execFile } from "node:child_process"; import { promisify } from "node:util"; import os from "node:os"; @@ -9,13 +9,33 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; const run = promisify(execFile); const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const input = JSON.parse(await readFile(path.join(root, "managed-wren", "release-inputs.json"), "utf8")).python; -const temp = await mkdtemp(path.join(os.tmpdir(), "genbi-pbs-probe-")); +const digest = (value) => createHash("sha256").update(value).digest("hex"); +function contained(root, target) { const relative = path.relative(root, target); return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)); } +async function treeDigest(root) { + const entries = []; + const visit = async (directory) => { + for (const name of (await readdir(directory)).sort()) { + if (name === "__pycache__") continue; + const target = path.join(directory, name); const metadata = await lstat(target); const mode = (metadata.mode & 0o777).toString(8); + if (metadata.isSymbolicLink()) { const canonical = await realpath(target); if (!contained(root, canonical)) throw new Error("PBS link escapes tree"); entries.push(path.relative(root, target) + "\0" + mode + "\0link\0" + await readlink(target)); } + else if (metadata.isDirectory()) await visit(target); else if (metadata.isFile()) entries.push(path.relative(root, target) + "\0" + mode + "\0file\0" + digest(await readFile(target))); else throw new Error("unsupported PBS tree entry"); + } + }; + await visit(root); return digest(entries.join("\n")); +} +async function extractWithUmask(archive, target, umask) { + await run("/bin/sh", ["-c", "umask \"$1\"; exec /usr/bin/tar -xpzf \"$2\" -C \"$3\"", "managed-wren-pbs", umask, archive, target]); +} +const temp = await realpath(await mkdtemp(path.join(os.tmpdir(), "genbi-pbs-probe-"))); try { const archive = path.join(temp, input.filename); await run("/usr/bin/curl", ["--fail", "--location", "--proto", "=https", "--tlsv1.2", "-o", archive, input.url]); - if (createHash("sha256").update(await readFile(archive)).digest("hex") !== input.sha256) throw new Error("PBS hash mismatch"); - const staging = path.join(temp, "staging"), final = path.join(temp, "final"); await run("/bin/mkdir", [staging]); await run("/usr/bin/tar", ["-xzf", archive, "-C", staging]); + if (digest(await readFile(archive)) !== input.sha256) throw new Error("PBS hash mismatch"); + const staging = path.join(temp, "staging-022"), alternate = path.join(temp, "staging-077"), final = path.join(temp, "final"); await run("/bin/mkdir", [staging]); await run("/bin/mkdir", [alternate]); + await extractWithUmask(archive, staging, "022"); await extractWithUmask(archive, alternate, "077"); await chmod(staging, 0o700); await chmod(alternate, 0o700); + const pythonTreeSha256 = await treeDigest(path.join(staging, "python")); + if (pythonTreeSha256 !== await treeDigest(path.join(alternate, "python"))) throw new Error("PBS tree digest depends on umask"); await run(path.join(staging, "python", "bin", "python3.11"), ["-m", "venv", "--copies", path.join(staging, "venv")]); await run("/bin/mv", [staging, final]); const config = path.join(final, "venv", "pyvenv.cfg"); const text = (await readFile(config, "utf8")).split(staging).join(final); if (text.includes(staging)) throw new Error("staging reference remains"); await writeFile(config, text, { mode: 0o600 }); const launcher = path.join(final, "venv", "bin", "wren-probe"); await writeFile(launcher, `#!${path.join(final, "venv", "bin", "python")}\nimport sys; print(sys.executable)\n`, { mode: 0o700 }); await chmod(launcher, 0o700); - const { stdout } = await run(launcher, []); if (stdout.trim() !== path.join(final, "venv", "bin", "python")) throw new Error("promoted launcher mismatch"); console.log(`ok sha256=${input.sha256}`); + const { stdout } = await run(launcher, []); if (stdout.trim() !== path.join(final, "venv", "bin", "python")) throw new Error("promoted launcher mismatch"); console.log(`ok sha256=${input.sha256} pythonTreeSha256=${pythonTreeSha256}`); } finally { if (path.basename(temp).startsWith("genbi-pbs-probe-")) await rm(temp, { recursive: true, force: true }); } diff --git a/apps/genbi/server/bin.ts b/apps/genbi/server/bin.ts index 0c3c7f2fb..f9d55ec71 100644 --- a/apps/genbi/server/bin.ts +++ b/apps/genbi/server/bin.ts @@ -144,7 +144,7 @@ import { NativeSessionService } from "./native-sessions.js"; import { NativeArtifactService } from "./native-artifacts.js"; import { RuntimeHost } from "./runtime-host/local.js"; import { runtimeNotReady } from "./runtime-host/policy.js"; -import { ManagedWrenRuntimeError, provisionManagedWrenRuntime, resolveManagedWrenRuntime } from "./managed-wren-runtime.js"; +import { managedWrenReadinessFailure, resolveManagedWrenRuntime } from "./managed-wren-runtime.js"; import { assertNativeExecutableIdentity, assertNativeRuntimeSpec, attestNativeExecutable, buildNativeChildEnvironment, buildNativeRuntimeSpec, resolveNativeExecutable } from "./native-runtime-spec.js"; import type { NativeExecutableIdentity } from "./native-runtime-spec.js"; import { initializeNativeSessionStateBase, legacyInteractiveWorkspace, validateLegacyInteractiveWorkspace } from "./native-session-workspace.js"; @@ -581,23 +581,18 @@ async function main(): Promise { // This is the sole production composition of the Phase-1 policy. Browser // requests never reach this selection or its executable/policy inputs. const managedCodexRuntimeProbe = async () => { - try { - const record = await provisionManagedWrenRuntime({ packageRoot }); - // Provisioning is only one Codex readiness input. This phase must not - // enable the local PTY as a production Codex adapter before the future - // app-server capability/identity contract is certified. - void record; - return { - readiness: runtimeNotReady("codex-app-server", "unprovisioned", "codex_app_server_unprovisioned"), - diagnostic: { phase: "provisioning" as const }, - }; - } catch (error) { - const code = error instanceof ManagedWrenRuntimeError ? error.code : "codex_wren_runtime_unprovisioned"; + const code = managedWrenReadinessFailure({ packageRoot }); + if (code !== undefined) { return { readiness: runtimeNotReady("codex-app-server", "unprovisioned", code), diagnostic: { phase: "provisioning" as const }, }; } + // A verified runtime alone does not enable the future app-server adapter. + return { + readiness: runtimeNotReady("codex-app-server", "unprovisioned", "codex_app_server_unprovisioned"), + diagnostic: { phase: "provisioning" as const }, + }; }; const nativeRuntimeHost = new RuntimeHost({ selected: "local", diff --git a/apps/genbi/server/managed-wren-runtime.ts b/apps/genbi/server/managed-wren-runtime.ts index bbf934119..f9f106654 100644 --- a/apps/genbi/server/managed-wren-runtime.ts +++ b/apps/genbi/server/managed-wren-runtime.ts @@ -38,7 +38,7 @@ const managedWrenManifestSchema = z.object({ if (new Set(value.wheels.map((wheel) => wheel.filename)).size !== value.wheels.length) context.addIssue({ code: "custom", message: "duplicate wheel filename" }); if (value.wheels[0]?.distribution !== "wrenai" || value.wheels[0].version !== value.compatibility.wren) context.addIssue({ code: "custom", message: "wrenai must be the first exact wheel" }); const approved = value.activation === "approved"; - if (approved !== (value.licenseApproval.state === "approved") || (approved && (!value.licenseApproval.evidence || value.runtime.pythonTreeSha256 === "staged" || value.runtime.packageTreeSha256 === "staged" || value.runtime.closureSha256 === "staged"))) { + if (approved !== (value.licenseApproval.state === "approved") || (approved && (!value.licenseApproval.evidence || value.runtime.pythonTreeSha256 === "staged" || value.runtime.packageTreeSha256 === "staged" || value.runtime.sitePackagesTreeSha256 === "staged" || value.runtime.closureSha256 === "staged"))) { context.addIssue({ code: "custom", message: "activation requires approved licence evidence and attested digests" }); } }); @@ -309,6 +309,20 @@ export function resolveManagedWrenRuntime(options: { readonly packageRoot?: stri return Object.freeze({ version: "1", shim: launcher, launcher, venv_python: interpreter, tool_root: path.join(generation, "venv"), site_packages: path.dirname(packagePath), source_root: packagePath, interpreter: python, interpreter_root: path.dirname(path.dirname(python)), manifest_digest: digest, generation_root: generation, closure_digest: actualClosure, package_digest: actualPackage }); } +/** + * Readiness is deliberately resolve-only: checking any RuntimeHost backend + * must neither create the GenBI runtime root nor fetch/install managed bytes. + * An explicit future first-use path may call the exported provisioner below. + */ +export function managedWrenReadinessFailure(options: { readonly packageRoot?: string; readonly runtimeRoot?: string } = {}): ManagedWrenFailureCode | undefined { + try { + resolveManagedWrenRuntime(options); + return undefined; + } catch (error) { + return error instanceof ManagedWrenRuntimeError ? error.code : "codex_wren_runtime_unprovisioned"; + } +} + /** * Provision is intentionally unavailable for a staged manifest. A release can * only turn this on after the protected licence approval job writes an approved @@ -352,7 +366,7 @@ export async function provisionManagedWrenRuntime(options: { readonly packageRoo // Fetching any other URL, index, or dependency resolver is intentionally absent. const archive = path.join(staging, manifest.runtime.pythonArchivePath); const download = async (url: string, target: string, expected: string) => { const response = await fetch(url); if (!response.ok) throw new Error("download"); const bytes = Buffer.from(await response.arrayBuffer()); if (sha256(bytes) !== expected) throw new Error("digest"); writeFileSync(target, bytes, { mode: 0o600, flag: "wx" }); }; await download(manifest.python.mirror.url, archive, manifest.python.mirror.sha256); - assertSafeArchive(archive); execFileSync("/usr/bin/tar", ["-xzf", archive, "-C", staging], { stdio: "ignore" }); + assertSafeArchive(archive); execFileSync("/usr/bin/tar", ["-xpzf", archive, "-C", staging], { stdio: "ignore" }); chmodSync(staging, 0o700); const python = path.join(staging, manifest.python.interpreterPath); regularExecutable(staging, python, "codex_wren_interpreter_mismatch"); const wheels = path.join(staging, "wheels"); mkdirSync(wheels, { mode: 0o700 }); for (const wheel of manifest.wheels) await download(wheel.url, path.join(wheels, wheel.filename), wheel.sha256); diff --git a/apps/genbi/test/managed-wren-runtime.test.ts b/apps/genbi/test/managed-wren-runtime.test.ts index 21d0d560a..4b65696d1 100644 --- a/apps/genbi/test/managed-wren-runtime.test.ts +++ b/apps/genbi/test/managed-wren-runtime.test.ts @@ -4,7 +4,9 @@ import { createHash } from "node:crypto"; import { tmpdir } from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { ManagedWrenRuntimeError, cleanupManagedWrenGenerations, managedWrenClosureDigest, managedWrenTreeDigest, manifestDigest, provisionManagedWrenRuntime, readManagedWrenManifest, resolveManagedWrenRuntime } from "../server/managed-wren-runtime.js"; +import { ManagedWrenRuntimeError, cleanupManagedWrenGenerations, managedWrenClosureDigest, managedWrenReadinessFailure, managedWrenTreeDigest, manifestDigest, provisionManagedWrenRuntime, readManagedWrenManifest, resolveManagedWrenRuntime } from "../server/managed-wren-runtime.js"; +import { RuntimeHost } from "../server/runtime-host/local.js"; +import { runtimeNotReady } from "../server/runtime-host/policy.js"; const roots: string[] = []; const digest = (value: string | Uint8Array) => createHash("sha256").update(value).digest("hex"); @@ -124,6 +126,42 @@ describe("managed Wren runtime", () => { } }); + it("rejects every staged attestation placeholder in an approved manifest", () => { + const value = fixture(); const manifestPath = path.join(value.packageRoot, "managed-wren", "manifest.json"); + for (const field of ["pythonTreeSha256", "packageTreeSha256", "sitePackagesTreeSha256", "closureSha256"] as const) { + const changed = JSON.parse(JSON.stringify(value.manifest)); changed.runtime[field] = "staged"; writeFileSync(manifestPath, JSON.stringify(changed)); + try { readManagedWrenManifest(value.packageRoot); throw new Error("approved staged digest was accepted"); } catch (error) { expect(error).toMatchObject({ code: "codex_wren_manifest_invalid" }); } + } + }); + + it("keeps production RuntimeHost composition resolve-only across local, Codex, and Claude probes", async () => { + const value = provisionFixture(); rmSync(value.runtimeRoot, { recursive: true, force: true }); + const fetch = vi.fn(); vi.stubGlobal("fetch", fetch); + const localAvailable = vi.fn(() => true); + const host = new RuntimeHost({ + selected: "local", + deployment: "development", + localAvailable, + vendorProbes: { + "codex-app-server": async () => { + const code = managedWrenReadinessFailure({ packageRoot: value.packageRoot, runtimeRoot: value.runtimeRoot }); + return { readiness: runtimeNotReady("codex-app-server", "unprovisioned", code ?? "codex_app_server_unprovisioned"), diagnostic: { phase: "provisioning" as const } }; + }, + }, + }); + await expect(host.probe()).resolves.toMatchObject({ + selectedReadiness: { state: "ready" }, + backends: { + "codex-app-server": { state: "unprovisioned", code: "codex_wren_runtime_unprovisioned" }, + "claude-sandbox-runtime": { state: "unprovisioned", code: "claude_sandbox_runtime_unprovisioned" }, + }, + }); + expect(localAvailable).toHaveBeenCalledTimes(1); expect(fetch).not.toHaveBeenCalled(); expect(existsSync(value.runtimeRoot)).toBe(false); + const productionComposition = readFileSync(path.resolve("server", "bin.ts"), "utf8"); + expect(productionComposition).toContain("managedWrenReadinessFailure({ packageRoot })"); + expect(productionComposition).not.toContain("provisionManagedWrenRuntime"); + }); + it("provisions deterministic fixture assets offline and then reuses the immutable generation", async () => { const value = provisionFixture(); value.installFetch(); const first = await provisionManagedWrenRuntime({ packageRoot: value.packageRoot, runtimeRoot: value.runtimeRoot });