mirror of
https://github.com/Canner/WrenAI.git
synced 2026-10-16 06:57:43 +08:00
fix(genbi): keep managed runtime readiness read-only
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 }); }
|
||||
|
||||
@@ -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<void> {
|
||||
// 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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user