From a126dde42ed97987652eec900a10e04ebd9980e1 Mon Sep 17 00:00:00 2001 From: Jax Liu Date: Mon, 7 Sep 2026 11:04:34 +0800 Subject: [PATCH] feat(genbi): provision managed Wren runtime --- .github/workflows/managed-wren-runtime.yml | 134 ++++++++++ apps/genbi/managed-wren/manifest.json | 44 ++++ apps/genbi/managed-wren/release-inputs.json | 21 ++ apps/genbi/package.json | 3 +- .../scripts/installed-package-acceptance.mjs | 2 +- apps/genbi/scripts/managed-wren-release.mjs | 54 +++++ apps/genbi/server/bin.ts | 27 ++- apps/genbi/server/managed-wren-runtime.ts | 229 ++++++++++++++++++ apps/genbi/server/native-sessions.ts | 15 +- apps/genbi/test/managed-wren-runtime.test.ts | 175 +++++++++++++ apps/genbi/test/packaging-contract.test.ts | 17 ++ 11 files changed, 713 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/managed-wren-runtime.yml create mode 100644 apps/genbi/managed-wren/manifest.json create mode 100644 apps/genbi/managed-wren/release-inputs.json create mode 100644 apps/genbi/scripts/managed-wren-release.mjs create mode 100644 apps/genbi/server/managed-wren-runtime.ts create mode 100644 apps/genbi/test/managed-wren-runtime.test.ts diff --git a/.github/workflows/managed-wren-runtime.yml b/.github/workflows/managed-wren-runtime.yml new file mode 100644 index 000000000..5f8ddfece --- /dev/null +++ b/.github/workflows/managed-wren-runtime.yml @@ -0,0 +1,134 @@ +name: Stage managed Wren runtime + +on: + workflow_dispatch: + inputs: + runtime_tag: + description: Immutable managed-Wren release tag + required: true + type: string + license_approval: + description: Must be approved after legal review; protected environment is the authority + required: true + default: pending + type: choice + options: [pending, approved] + +permissions: + contents: read + +jobs: + stage: + runs-on: macos-14 + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.sha }} + persist-credentials: false + - uses: actions/setup-node@v4 + with: + node-version: 24 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Resolve and download the exact darwin-arm64 wheel closure + run: | + mkdir -p release/wheels + python -m pip download --only-binary=:all: --platform macosx_11_0_arm64 --python-version 3.11 --implementation cp --dest release/wheels 'wrenai==0.13.0' + python - <<'PY' + import hashlib, pathlib + target = pathlib.Path('release/wheels/wrenai-0.13.0-py3-none-any.whl') + assert target.is_file() + assert hashlib.sha256(target.read_bytes()).hexdigest() == '1cb14927c9a1e1925a7295c2772e5852254c6cc370db640ccc33b6cd04e95dae' + PY + - name: Verify and stage the exact CPython archive + run: | + python - <<'PY' + import hashlib, pathlib, urllib.request + target = pathlib.Path('release/cpython-3.11.16+20260901-aarch64-apple-darwin-install_only.tar.gz') + urllib.request.urlretrieve('https://github.com/astral-sh/python-build-standalone/releases/download/20260901/cpython-3.11.16%2B20260901-aarch64-apple-darwin-install_only.tar.gz', target) + assert hashlib.sha256(target.read_bytes()).hexdigest() == '50424fa409e8ae84b82a3052522f64695b47dff2158b70bb7358e0ebd6c085c9' + PY + - name: Inventory every selected wheel licence and digest + run: | + python - <<'PY' + import hashlib, json, pathlib, urllib.request, zipfile + rows = [] + for wheel in sorted(pathlib.Path('release/wheels').glob('*.whl')): + with zipfile.ZipFile(wheel) as archive: + metadata = next(name for name in archive.namelist() if name.endswith('.dist-info/METADATA')) + fields = {} + for line in archive.read(metadata).decode('utf-8', 'replace').splitlines(): + if ': ' in line: + key, value = line.split(': ', 1) + if key in ('Name', 'Version', 'License'): fields[key] = value + if not fields.get('Name') or not fields.get('Version'): + raise SystemExit(f'missing wheel identity: {wheel.name}') + sha256 = hashlib.sha256(wheel.read_bytes()).hexdigest() + project = json.load(urllib.request.urlopen(f"https://pypi.org/pypi/{fields['Name']}/{fields['Version']}/json")) + match = next((item for item in project['urls'] if item['filename'] == wheel.name and item['digests']['sha256'] == sha256), None) + if match is None: + raise SystemExit(f'missing immutable source identity: {wheel.name}') + rows.append({'filename': wheel.name, 'sha256': sha256, 'distribution': fields['Name'].lower().replace('-', '_'), 'version': fields['Version'], 'license': fields.get('License', 'UNKNOWN'), 'sourceUrl': match['url']}) + pathlib.Path('release/wheel-license-inventory.json').write_text(json.dumps({'schema': 1, 'platform': 'darwin-arm64', 'wheels': rows}, indent=2) + '\n') + pathlib.Path('release/wheel-inputs.json').write_text(json.dumps(rows, indent=2) + '\n') + PY + - name: Build the selected runtime without an index or dependency resolver + run: | + mkdir -p release/runtime + tar -xzf release/cpython-*.tar.gz -C release/runtime + release/runtime/python/install/bin/python3.11 -m venv release/runtime/venv + python - <<'PY' + import json, pathlib + wheels = json.loads(pathlib.Path('release/wheel-inputs.json').read_text()) + pathlib.Path('release/requirements.txt').write_text(''.join(f"{wheel['distribution']}=={wheel['version']} --hash=sha256:{wheel['sha256']}\n" for wheel in wheels)) + PY + release/runtime/venv/bin/python -m pip install --no-index --no-deps --require-hashes --find-links release/wheels -r release/requirements.txt + - name: Generate attested candidate manifest and preliminary licence inventory + run: node apps/genbi/scripts/managed-wren-release.mjs release "${{ inputs.runtime_tag }}" + - name: Upload review-only release evidence + uses: actions/upload-artifact@v4 + with: + name: managed-wren-staged-evidence + path: release/ + + publish-approved-runtime: + needs: stage + if: ${{ inputs.license_approval == 'approved' }} + environment: managed-wren-license-approved + permissions: + contents: write + runs-on: macos-14 + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.sha }} + persist-credentials: false + - uses: actions/download-artifact@v4 + with: + name: managed-wren-staged-evidence + path: release + - name: Require protected-environment approval evidence + run: | + test "${{ inputs.license_approval }}" = approved + test -s release/license-inventory.json + test -s release/wheel-license-inventory.json + test -s release/provenance.json + test -s release/managed-wren-manifest.candidate.json + - name: Emit the approval-bound immutable manifest + run: | + node - <<'NODE' + const fs = require('node:fs'); + const manifest = JSON.parse(fs.readFileSync('release/managed-wren-manifest.candidate.json', 'utf8')); + manifest.activation = 'approved'; + manifest.licenseApproval = { state: 'approved', evidence: `protected-environment:${process.env.RUNTIME_TAG}` }; + fs.writeFileSync('release/managed-wren-manifest.json', JSON.stringify(manifest, null, 2) + '\n', { mode: 0o600 }); + NODE + env: + RUNTIME_TAG: ${{ inputs.runtime_tag }} + - name: Publication is release-only and must be completed by the approved release job + run: | + gh release create "${{ inputs.runtime_tag }}" \ + --title "Managed Wren runtime ${{ inputs.runtime_tag }}" \ + release/cpython-*.tar.gz release/wheels/*.whl release/*.json + echo "The staging job cannot publish assets or activate a package manifest." diff --git a/apps/genbi/managed-wren/manifest.json b/apps/genbi/managed-wren/manifest.json new file mode 100644 index 000000000..ae980d98a --- /dev/null +++ b/apps/genbi/managed-wren/manifest.json @@ -0,0 +1,44 @@ +{ + "schema": 1, + "activation": "staged", + "platform": "darwin-arm64", + "compatibility": { + "genbi": "0.0.4", + "profile": "genbi-native-v4", + "wren": "0.13.0" + }, + "python": { + "implementation": "cpython", + "version": "3.11.16", + "upstream": { + "release": "20260901", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20260901/cpython-3.11.16%2B20260901-aarch64-apple-darwin-install_only.tar.gz", + "sha256": "50424fa409e8ae84b82a3052522f64695b47dff2158b70bb7358e0ebd6c085c9" + }, + "mirror": { + "url": "https://github.com/Canner/WrenAI/releases/download/managed-wren-v0.0.4/cpython-3.11.16%2B20260901-aarch64-apple-darwin-install_only.tar.gz", + "sha256": "50424fa409e8ae84b82a3052522f64695b47dff2158b70bb7358e0ebd6c085c9" + }, + "interpreterPath": "python/install/bin/python3.11" + }, + "wheels": [ + { + "distribution": "wrenai", + "version": "0.13.0", + "filename": "wrenai-0.13.0-py3-none-any.whl", + "url": "https://github.com/Canner/WrenAI/releases/download/managed-wren-v0.0.4/wrenai-0.13.0-py3-none-any.whl", + "sourceUrl": "https://files.pythonhosted.org/packages/7f/40/2de81f1a3fe332ebfb8ec5db0161ce4b521dee5c1d0d4adf9c5693651b51/wrenai-0.13.0-py3-none-any.whl", + "sha256": "1cb14927c9a1e1925a7295c2772e5852254c6cc370db640ccc33b6cd04e95dae" + } + ], + "runtime": { + "pythonArchivePath": "python.tar.gz", + "venvInterpreterPath": "venv/bin/python", + "launcherPath": "venv/bin/wren", + "module": "wren.cli:app", + "packagePath": "venv/lib/python3.11/site-packages/wren", + "packageTreeSha256": "staged", + "closureSha256": "staged" + }, + "licenseApproval": { "state": "pending" } +} diff --git a/apps/genbi/managed-wren/release-inputs.json b/apps/genbi/managed-wren/release-inputs.json new file mode 100644 index 000000000..b22a847c1 --- /dev/null +++ b/apps/genbi/managed-wren/release-inputs.json @@ -0,0 +1,21 @@ +{ + "schema": 1, + "platform": "darwin-arm64", + "compatibility": { + "genbi": "0.0.4", + "profile": "genbi-native-v4" + }, + "python": { + "version": "3.11.16", + "release": "20260901", + "filename": "cpython-3.11.16+20260901-aarch64-apple-darwin-install_only.tar.gz", + "url": "https://github.com/astral-sh/python-build-standalone/releases/download/20260901/cpython-3.11.16%2B20260901-aarch64-apple-darwin-install_only.tar.gz", + "sha256": "50424fa409e8ae84b82a3052522f64695b47dff2158b70bb7358e0ebd6c085c9" + }, + "wrenai": { + "version": "0.13.0", + "filename": "wrenai-0.13.0-py3-none-any.whl", + "url": "https://files.pythonhosted.org/packages/7f/40/2de81f1a3fe332ebfb8ec5db0161ce4b521dee5c1d0d4adf9c5693651b51/wrenai-0.13.0-py3-none-any.whl", + "sha256": "1cb14927c9a1e1925a7295c2772e5852254c6cc370db640ccc33b6cd04e95dae" + } +} diff --git a/apps/genbi/package.json b/apps/genbi/package.json index ea499b850..7b2fb6768 100644 --- a/apps/genbi/package.json +++ b/apps/genbi/package.json @@ -89,7 +89,8 @@ "dist-server/server", "dist-server/harness", "dist-server/providers", - "profiles" + "profiles", + "managed-wren" ], "publishConfig": { "access": "public" diff --git a/apps/genbi/scripts/installed-package-acceptance.mjs b/apps/genbi/scripts/installed-package-acceptance.mjs index 037033cc7..31a1e6200 100644 --- a/apps/genbi/scripts/installed-package-acceptance.mjs +++ b/apps/genbi/scripts/installed-package-acceptance.mjs @@ -382,7 +382,7 @@ function assertPublishedFiles(files) { const forbidden = /(^|\/)(scripts|test|tests|fixtures|examples|\.git)(\/|$)|(^|\/)node_modules(\/|$)/; const unexpected = files.filter((file) => forbidden.test(file)); if (unexpected.length > 0) throw new Error(`package tarball contains repository-only files: ${JSON.stringify(unexpected)}`); - for (const required of ["package/bin/genbi.mjs", "package/dist/index.html", "package/dist-server/server/bin.js"]) { + for (const required of ["package/bin/genbi.mjs", "package/dist/index.html", "package/dist-server/server/bin.js", "package/managed-wren/manifest.json"]) { if (!files.includes(required)) throw new Error(`package tarball is missing ${required}`); } } diff --git a/apps/genbi/scripts/managed-wren-release.mjs b/apps/genbi/scripts/managed-wren-release.mjs new file mode 100644 index 000000000..466c7b0db --- /dev/null +++ b/apps/genbi/scripts/managed-wren-release.mjs @@ -0,0 +1,54 @@ +#!/usr/bin/env node +/** Generate review-only managed-Wren release evidence; it never publishes. */ +import { createHash } from "node:crypto"; +import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const [output, runtimeTag] = process.argv.slice(2); +if (!output || !runtimeTag || !/^[A-Za-z0-9._-]+$/.test(runtimeTag)) throw new Error("usage: managed-wren-release.mjs "); +const inputs = JSON.parse(await readFile(path.join(packageRoot, "managed-wren", "release-inputs.json"), "utf8")); +const digest = (value) => createHash("sha256").update(value).digest("hex"); +const fileDigest = async (target) => digest(await readFile(target)); +const stableJson = (value) => Array.isArray(value) ? "[" + value.map(stableJson).join(",") + "]" : value && typeof value === "object" ? "{" + Object.keys(value).sort().map((key) => JSON.stringify(key) + ":" + stableJson(value[key])).join(",") + "}" : JSON.stringify(value); +const treeDigest = async (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 stat(target); + if (metadata.isDirectory()) await visit(target); else if (metadata.isFile()) entries.push(path.relative(root, target) + "\0" + await fileDigest(target)); else throw new Error("unsupported runtime entry: " + target); + } + }; + await visit(root); return digest(entries.join("\n")); +}; +for (const artifact of [inputs.python, inputs.wrenai]) { + if (!artifact || !/^https:/.test(artifact.url) || !/^[a-f0-9]{64}$/.test(artifact.sha256) || !/^[A-Za-z0-9._+%-]+$/.test(artifact.filename)) throw new Error("invalid exact release input"); +} +const wheelInputs = JSON.parse(await readFile(path.join(output, "wheel-inputs.json"), "utf8")); +if (!Array.isArray(wheelInputs) || wheelInputs.length === 0) throw new Error("missing selected wheel closure"); +const wheelLicenses = new Map(wheelInputs.map((wheel) => [wheel.filename, wheel.license])); +const wheels = wheelInputs.map((wheel) => { + if (!/^[a-z0-9][a-z0-9._-]*$/.test(wheel.distribution) || !/^[A-Za-z0-9!+._-]+$/.test(wheel.version) || !/^[A-Za-z0-9._+%-]+$/.test(wheel.filename) || !/^https:/.test(wheel.sourceUrl) || !/^[a-f0-9]{64}$/.test(wheel.sha256)) throw new Error("invalid selected wheel closure"); + return { distribution: wheel.distribution, version: wheel.version, filename: wheel.filename, sourceUrl: wheel.sourceUrl, sha256: wheel.sha256, url: "https://github.com/Canner/WrenAI/releases/download/" + runtimeTag + "/" + wheel.filename }; +}).sort((a, b) => a.filename.localeCompare(b.filename)); +const wrenIndex = wheels.findIndex((wheel) => wheel.distribution === "wrenai" && wheel.version === inputs.wrenai.version); +if (wrenIndex < 0) throw new Error("wrenai is absent from the selected wheel closure"); +const [wrenai] = wheels.splice(wrenIndex, 1); wheels.unshift(wrenai); +const packageTreeSha256 = await treeDigest(path.join(output, "runtime", "venv", "lib", "python3.11", "site-packages", "wren")); +const closureSha256 = digest(wheels.map((wheel) => wheel.filename + "\0" + wheel.sha256).sort().join("\n")); +const manifest = { + schema: 1, activation: "staged", platform: "darwin-arm64", + compatibility: { genbi: inputs.compatibility.genbi, profile: inputs.compatibility.profile, wren: inputs.wrenai.version }, + python: { implementation: "cpython", version: inputs.python.version, upstream: { release: inputs.python.release, url: inputs.python.url, sha256: inputs.python.sha256 }, mirror: { url: "https://github.com/Canner/WrenAI/releases/download/" + runtimeTag + "/" + inputs.python.filename, sha256: inputs.python.sha256 }, interpreterPath: "python/install/bin/python3.11" }, + wheels, + runtime: { pythonArchivePath: "python.tar.gz", venvInterpreterPath: "venv/bin/python", launcherPath: "venv/bin/wren", module: "wren.cli:app", packagePath: "venv/lib/python3.11/site-packages/wren", packageTreeSha256, closureSha256 }, + licenseApproval: { state: "pending" }, +}; +const inventory = { schema: 1, status: "pending-explicit-approval", components: [{ name: "python-build-standalone", version: "cpython-" + inputs.python.release, declaredLicense: "MPL-2.0", artifact: inputs.python.filename }, { name: "CPython", version: inputs.python.version, declaredLicense: "PSF-2.0", artifact: inputs.python.filename }, ...wheels.map((wheel) => ({ name: wheel.distribution, version: wheel.version, declaredLicense: wheelLicenses.get(wheel.filename) ?? "UNKNOWN", artifact: wheel.filename }))], note: "The protected release environment must approve the completed expanded wheel and bundled-library inventory before public assets or an approved manifest can be emitted." }; +const provenance = { schema: 1, inputs, selectedWheels: wheels, stagedManifestSha256: digest(stableJson(manifest)), generatedAt: new Date().toISOString() }; +await mkdir(output, { recursive: true, mode: 0o700 }); +await writeFile(path.join(output, "managed-wren-manifest.candidate.json"), JSON.stringify(manifest, null, 2) + "\n", { mode: 0o600 }); +await writeFile(path.join(output, "license-inventory.json"), JSON.stringify(inventory, null, 2) + "\n", { mode: 0o600 }); +await writeFile(path.join(output, "provenance.json"), JSON.stringify(provenance, null, 2) + "\n", { mode: 0o600 }); diff --git a/apps/genbi/server/bin.ts b/apps/genbi/server/bin.ts index c4fd65ba0..0c3c7f2fb 100644 --- a/apps/genbi/server/bin.ts +++ b/apps/genbi/server/bin.ts @@ -42,9 +42,6 @@ * binding reads. Falls back to this repo's own Rust build; resolution * loud-fails rather than degrading to Warble's built-in MDL adapter * (see harness/compile/context-loader.ts) - * WREN_HARNESS_WREN_SHIM optional absolute server-owned Wren shim for - * native Codex runtime permissions; defaults to - * the fixed local installation when unset * WREN_HARNESS_AGENT_SDK_BIN path to the agent-sdk CLI binary (dispatched) * WREN_HARNESS_CODEX_LOCAL_BIN path to the warble-codex-local dispatcher CLI * WREN_HARNESS_CODEX_BIN optional path to the Codex CLI used by that dispatcher @@ -146,6 +143,8 @@ import { createProbedPtyFactory, ensureDarwinNodePtySpawnHelper } from "./node-p 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 { 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,10 +580,30 @@ 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"; + return { + readiness: runtimeNotReady("codex-app-server", "unprovisioned", code), + diagnostic: { phase: "provisioning" as const }, + }; + } + }; const nativeRuntimeHost = new RuntimeHost({ selected: "local", deployment: process.env["NODE_ENV"] === "production" ? "production" : "development", localAvailable: nativeTerminalHostAvailable, + vendorProbes: { "codex-app-server": managedCodexRuntimeProbe }, }); const nativeSessions = new NativeSessionService({ store, @@ -612,9 +631,9 @@ async function main(): Promise { // The service's option is a required string; when resolution failed the bare // name is what the preflight will report as unresolvable, with its reason. warbleBin: producerExecutable?.executable ?? "warble", - ...(process.env["WREN_HARNESS_WREN_SHIM"] !== undefined ? { wrenShim: process.env["WREN_HARNESS_WREN_SHIM"] } : {}), terminalHostAvailable: nativeTerminalHostAvailable, runtimeHost: nativeRuntimeHost, + resolveManagedCodexWrenRuntime: () => resolveManagedWrenRuntime({ packageRoot }), nativeHome, sourceWrenHome: nativeSourceWrenHome, ...(baseRouteOptions.codexHome ? { codexHome: baseRouteOptions.codexHome } : {}), diff --git a/apps/genbi/server/managed-wren-runtime.ts b/apps/genbi/server/managed-wren-runtime.ts new file mode 100644 index 000000000..8a8004ec8 --- /dev/null +++ b/apps/genbi/server/managed-wren-runtime.ts @@ -0,0 +1,229 @@ +/** + * Immutable, release-managed Wren runtime for native Codex sessions. + * + * The packaged manifest is deliberately staged until the release workflow has + * mirrored every byte and passed its protected licence-approval environment. + * This module never substitutes a local checkout, PATH Python, user venv, or + * upstream URL for a missing approved record. + */ +import { execFileSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { chmodSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { z } from "zod"; +import type { NativeWrenRuntime } from "./native-wren-runtime.js"; + +const SHA256 = z.string().regex(/^[a-f0-9]{64}$/); +const RELATIVE = z.string().min(1).refine((value) => !path.isAbsolute(value) && !value.split(/[\\/]/).includes("..")); +const URL = z.string().url().refine((value) => value.startsWith("https://github.com/Canner/WrenAI/releases/download/")); +const stagedDigest = z.union([SHA256, z.literal("staged")]); + +const managedWrenManifestSchema = z.object({ + schema: z.literal(1), + activation: z.enum(["staged", "approved"]), + platform: z.literal("darwin-arm64"), + compatibility: z.object({ genbi: z.string().regex(/^\d+\.\d+\.\d+$/), profile: z.literal("genbi-native-v4"), wren: z.string().regex(/^\d+\.\d+\.\d+$/) }).strict(), + python: z.object({ + implementation: z.literal("cpython"), version: z.string().regex(/^3\.11\.\d+$/), + upstream: z.object({ release: z.string().regex(/^\d{8}$/), url: z.string().url(), sha256: SHA256 }).strict(), + mirror: z.object({ url: URL, sha256: SHA256 }).strict(), interpreterPath: RELATIVE, + }).strict(), + wheels: z.array(z.object({ distribution: z.string().regex(/^[a-z0-9][a-z0-9._-]*$/), version: z.string().regex(/^[A-Za-z0-9!+._-]+$/), filename: RELATIVE, url: URL, sourceUrl: z.string().url(), sha256: SHA256 }).strict()).min(1), + runtime: z.object({ pythonArchivePath: RELATIVE, venvInterpreterPath: RELATIVE, launcherPath: RELATIVE, module: z.literal("wren.cli:app"), packagePath: RELATIVE, packageTreeSha256: stagedDigest, closureSha256: stagedDigest }).strict(), + licenseApproval: z.object({ state: z.enum(["pending", "approved"]), evidence: z.string().min(1).optional() }).strict(), +}).strict().superRefine((value, context) => { + 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.packageTreeSha256 === "staged" || value.runtime.closureSha256 === "staged"))) { + context.addIssue({ code: "custom", message: "activation requires approved licence evidence and attested digests" }); + } +}); + +export type ManagedWrenManifest = Readonly>; +export class ManagedWrenRuntimeError extends Error { constructor(readonly code: ManagedWrenFailureCode, message = code) { super(message); } } +export type ManagedWrenFailureCode = "codex_wren_manifest_missing" | "codex_wren_manifest_invalid" | "codex_wren_platform_unsupported" | "codex_wren_runtime_unprovisioned" | "codex_wren_provision_failed" | "codex_wren_interpreter_mismatch" | "codex_wren_launcher_mismatch" | "codex_wren_package_mismatch" | "codex_wren_closure_mismatch"; + +export interface ManagedWrenRuntimeRecord extends NativeWrenRuntime { + readonly manifest_digest: string; + readonly generation_root: string; + readonly closure_digest: string; + readonly package_digest: string; +} + +function failure(code: ManagedWrenFailureCode): never { throw new ManagedWrenRuntimeError(code); } +function sha256(value: Uint8Array | string): string { return createHash("sha256").update(value).digest("hex"); } +function contained(root: string, target: string): boolean { const relative = path.relative(root, target); return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)); } +function platform(): "darwin-arm64" | undefined { return process.platform === "darwin" && process.arch === "arm64" ? "darwin-arm64" : undefined; } +function packageRoot(): string { return path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); } +export function defaultManagedWrenRoot(): string { return path.join(os.homedir(), "Library", "Application Support", "WrenAI", "genbi", "managed-wren"); } +export function managedWrenManifestPath(root = packageRoot()): string { return path.join(root, "managed-wren", "manifest.json"); } + +export function readManagedWrenManifest(root = packageRoot()): ManagedWrenManifest { + let raw: unknown; + try { raw = JSON.parse(readFileSync(managedWrenManifestPath(root), "utf8")); } catch { return failure("codex_wren_manifest_missing"); } + const parsed = managedWrenManifestSchema.safeParse(raw); + if (!parsed.success) return failure("codex_wren_manifest_invalid"); + if (platform() !== parsed.data.platform) return failure("codex_wren_platform_unsupported"); + return Object.freeze(parsed.data); +} + +function stableJson(value: unknown): string { + if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`; + if (value && typeof value === "object") { + const object = value as Record; + return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableJson(object[key])}`).join(",")}}`; + } + return JSON.stringify(value); +} +export function manifestDigest(manifest: ManagedWrenManifest): string { return sha256(stableJson(manifest)); } +function immutableDirectory(root: string, digest: string): string { return path.join(root, digest); } +function assertPrivateDirectory(directory: string): void { + try { const entry = lstatSync(directory); if (!entry.isDirectory() || entry.isSymbolicLink() || realpathSync(directory) !== directory || (entry.mode & 0o777) !== 0o700) failure("codex_wren_runtime_unprovisioned"); } catch (error) { if (error instanceof ManagedWrenRuntimeError) throw error; failure("codex_wren_runtime_unprovisioned"); } +} +function assertContainedRealpath(root: string, target: string, code: ManagedWrenFailureCode): string { + try { const canonical = realpathSync(target); if (!contained(root, canonical)) failure(code); return canonical; } catch (error) { if (error instanceof ManagedWrenRuntimeError) throw error; return failure(code); } +} +function regularExecutable(root: string, target: string, code: ManagedWrenFailureCode): string { + const canonical = assertContainedRealpath(root, target, code); const metadata = statSync(canonical); if (!metadata.isFile() || (metadata.mode & 0o111) === 0) failure(code); return canonical; +} +function regularFile(root: string, target: string, code: ManagedWrenFailureCode): string { + const canonical = assertContainedRealpath(root, target, code); const metadata = statSync(canonical); if (!metadata.isFile()) failure(code); return canonical; +} +function assertSecureTree(root: string, code: ManagedWrenFailureCode): void { + try { + const visit = (directory: string) => { + for (const name of readdirSync(directory)) { + const target = path.join(directory, name); const entry = lstatSync(target); + if (entry.isSymbolicLink() || (entry.mode & 0o022) !== 0) failure(code); + if (entry.isDirectory()) visit(target); else if (!entry.isFile()) failure(code); + } + }; + visit(root); + } catch (error) { if (error instanceof ManagedWrenRuntimeError) throw error; failure(code); } +} +function treeDigest(root: string): string { + const entries: string[] = []; + const visit = (directory: string) => { for (const name of readdirSync(directory).sort()) { if (name === "__pycache__") continue; const target = path.join(directory, name); const stat = lstatSync(target); if (stat.isSymbolicLink()) throw new Error("symlink"); if (stat.isDirectory()) visit(target); else if (stat.isFile()) entries.push(`${path.relative(root, target)}\0${sha256(readFileSync(target))}`); else throw new Error("special"); } }; + visit(root); return sha256(entries.join("\n")); +} +function closureDigest(manifest: ManagedWrenManifest): string { return sha256(manifest.wheels.map((wheel) => `${wheel.filename}\0${wheel.sha256}`).sort().join("\n")); } +function ownershipMarker(root: string): string { return path.join(root, ".genbi-managed-wren.json"); } +function stagingMarker(root: string): string { return path.join(root, ".genbi-managed-wren-staging.json"); } +function actualClosureDigest(root: string, manifest: ManagedWrenManifest): string { + const hashes: string[] = []; + for (const wheel of manifest.wheels) { + const target = regularFile(root, path.join(root, "wheels", wheel.filename), "codex_wren_closure_mismatch"); + const actual = sha256(readFileSync(target)); if (actual !== wheel.sha256) failure("codex_wren_closure_mismatch"); + hashes.push(`${wheel.filename}\0${actual}`); + } + return sha256(hashes.sort().join("\n")); +} +function isRecognisedStaging(root: string, candidate: string, digest: string): boolean { + try { + const target = path.join(root, candidate); const entry = lstatSync(target); + if (!entry.isDirectory() || entry.isSymbolicLink() || !contained(root, target) || (entry.mode & 0o777) !== 0o700) return false; + const marker = stagingMarker(target); const markerEntry = lstatSync(marker); + if (!markerEntry.isFile() || markerEntry.isSymbolicLink() || (markerEntry.mode & 0o777) !== 0o600) return false; + return JSON.parse(readFileSync(marker, "utf8")).manifestDigest === digest; + } catch { return false; } +} +function cleanupRecognisedStaging(root: string, digest: string): void { + for (const candidate of readdirSync(root)) { + if (!candidate.startsWith(`.staging-${digest}-`) || !isRecognisedStaging(root, candidate, digest)) continue; + rmSync(path.join(root, candidate), { recursive: true, force: true }); + } +} + +/** Revalidate a record before every native Codex launch. */ +export function resolveManagedWrenRuntime(options: { readonly packageRoot?: string; readonly runtimeRoot?: string } = {}): ManagedWrenRuntimeRecord { + const manifest = readManagedWrenManifest(options.packageRoot); + if (manifest.activation !== "approved") return failure("codex_wren_runtime_unprovisioned"); + const digest = manifestDigest(manifest); const root = options.runtimeRoot ?? defaultManagedWrenRoot(); const generation = immutableDirectory(root, digest); + assertPrivateDirectory(root); assertPrivateDirectory(generation); + assertSecureTree(generation, "codex_wren_closure_mismatch"); + let marker: { manifestDigest?: unknown; closureDigest?: unknown; packageDigest?: unknown; interpreterDigest?: unknown; launcherDigest?: unknown }; + try { const markerPath = ownershipMarker(generation); const entry = lstatSync(markerPath); if (!entry.isFile() || entry.isSymbolicLink() || (entry.mode & 0o777) !== 0o600) return failure("codex_wren_runtime_unprovisioned"); marker = JSON.parse(readFileSync(markerPath, "utf8")); } catch { return failure("codex_wren_runtime_unprovisioned"); } + if (marker.manifestDigest !== digest) return failure("codex_wren_closure_mismatch"); + const archive = regularFile(generation, path.join(generation, manifest.runtime.pythonArchivePath), "codex_wren_interpreter_mismatch"); + if (sha256(readFileSync(archive)) !== manifest.python.mirror.sha256) return failure("codex_wren_interpreter_mismatch"); + const interpreter = regularExecutable(generation, path.join(generation, manifest.runtime.venvInterpreterPath), "codex_wren_interpreter_mismatch"); + const launcher = regularExecutable(generation, path.join(generation, manifest.runtime.launcherPath), "codex_wren_launcher_mismatch"); + if (marker.interpreterDigest !== sha256(readFileSync(interpreter))) return failure("codex_wren_interpreter_mismatch"); + if (marker.launcherDigest !== sha256(readFileSync(launcher))) return failure("codex_wren_launcher_mismatch"); + let launcherText = ""; try { launcherText = readFileSync(launcher, "utf8"); } catch { return failure("codex_wren_launcher_mismatch"); } + if (!launcherText.startsWith(`#!${path.join(generation, manifest.runtime.venvInterpreterPath)}`) || !launcherText.includes("from wren.cli import app")) return failure("codex_wren_launcher_mismatch"); + const packagePath = assertContainedRealpath(generation, path.join(generation, manifest.runtime.packagePath), "codex_wren_package_mismatch"); + let actualPackage: string; try { actualPackage = treeDigest(packagePath); } catch { return failure("codex_wren_package_mismatch"); } + if (actualPackage !== manifest.runtime.packageTreeSha256 || marker.packageDigest !== actualPackage) return failure("codex_wren_package_mismatch"); + const actualClosure = actualClosureDigest(generation, manifest); if (actualClosure !== manifest.runtime.closureSha256 || marker.closureDigest !== actualClosure) return failure("codex_wren_closure_mismatch"); + const python = regularExecutable(generation, path.join(generation, manifest.python.interpreterPath), "codex_wren_interpreter_mismatch"); + 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 }); +} + +/** + * Provision is intentionally unavailable for a staged manifest. A release can + * only turn this on after the protected licence approval job writes an approved + * manifest with release-attested digests. + */ +export async function provisionManagedWrenRuntime(options: { readonly packageRoot?: string; readonly runtimeRoot?: string } = {}): Promise { + const manifest = readManagedWrenManifest(options.packageRoot); + if (manifest.activation !== "approved") return failure("codex_wren_runtime_unprovisioned"); + const root = options.runtimeRoot ?? defaultManagedWrenRoot(); + mkdirSync(root, { recursive: true, mode: 0o700 }); chmodSync(root, 0o700); assertPrivateDirectory(root); + const digest = manifestDigest(manifest); const destination = immutableDirectory(root, digest); + try { return resolveManagedWrenRuntime({ ...(options.packageRoot ? { packageRoot: options.packageRoot } : {}), runtimeRoot: root }); } catch (error) { if (!(error instanceof ManagedWrenRuntimeError) || error.code !== "codex_wren_runtime_unprovisioned") throw error; } + const lock = path.join(root, ".provision.lock"); let descriptor: number | undefined; + try { + // A second first-use caller waits for the owner instead of falling through + // to a partial directory or declaring a separate runtime valid. + const deadline = Date.now() + 10_000; + while (descriptor === undefined && Date.now() < deadline) { + try { descriptor = openSync(lock, "wx", 0o600); } catch (error: unknown) { + if (!(error && typeof error === "object" && "code" in error && error.code === "EEXIST")) throw error; + await new Promise((resolve) => setTimeout(resolve, 25)); + } + } + if (descriptor === undefined) return failure("codex_wren_provision_failed"); + // A waiter must re-check after it owns the lock: the original owner may + // have completed while this caller was waiting. + try { return resolveManagedWrenRuntime({ ...(options.packageRoot ? { packageRoot: options.packageRoot } : {}), runtimeRoot: root }); } catch (error) { if (!(error instanceof ManagedWrenRuntimeError) || error.code !== "codex_wren_runtime_unprovisioned") throw error; } + // Only a marker written by this provisioner is eligible for interrupted-run + // recovery, after the exclusive lock prevents a live writer race. + cleanupRecognisedStaging(root, digest); + const staging = path.join(root, `.staging-${digest}-${process.pid}`); mkdirSync(staging, { mode: 0o700 }); chmodSync(staging, 0o700); + writeFileSync(stagingMarker(staging), JSON.stringify({ manifestDigest: digest }) + "\n", { mode: 0o600, flag: "wx" }); + // The release workflow makes these assets available as a closed mirror. + // 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); + execFileSync("tar", ["-xzf", archive, "-C", staging], { stdio: "ignore" }); + 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); + const requirements = manifest.wheels.map((wheel) => `${wheel.distribution}==${wheel.version} --hash=sha256:${wheel.sha256}`).join("\n") + "\n"; + writeFileSync(path.join(staging, "requirements.txt"), requirements, { mode: 0o600, flag: "wx" }); + const venv = path.join(staging, "venv"); execFileSync(python, ["-m", "venv", venv], { stdio: "ignore", env: { PATH: path.dirname(python), HOME: staging, PYTHONNOUSERSITE: "1" } }); + execFileSync(path.join(venv, "bin", "python"), ["-m", "pip", "install", "--no-index", "--no-deps", "--require-hashes", "--find-links", wheels, "-r", path.join(staging, "requirements.txt")], { stdio: "ignore", env: { PATH: path.join(venv, "bin"), HOME: staging, PYTHONNOUSERSITE: "1" } }); + const packagePath = path.join(staging, manifest.runtime.packagePath); const packageDigest = treeDigest(packagePath); const closure = actualClosureDigest(staging, manifest); + if (packageDigest !== manifest.runtime.packageTreeSha256 || closure !== manifest.runtime.closureSha256) throw new Error("attestation"); + // `venv` console launchers contain an absolute interpreter shebang. The + // staging name must never escape into an active generation after rename. + const stagedInterpreter = path.join(staging, manifest.runtime.venvInterpreterPath); + const stagedLauncher = path.join(staging, manifest.runtime.launcherPath); + const launcherText = readFileSync(stagedLauncher, "utf8"); + if (!launcherText.startsWith(`#!${stagedInterpreter}\n`) || !launcherText.includes("from wren.cli import app")) return failure("codex_wren_launcher_mismatch"); + renameSync(staging, destination); + const finalInterpreter = path.join(destination, manifest.runtime.venvInterpreterPath); + const finalLauncher = path.join(destination, manifest.runtime.launcherPath); + writeFileSync(finalLauncher, `#!${finalInterpreter}\n${launcherText.slice(stagedInterpreter.length + 3)}`, { mode: 0o700 }); + rmSync(stagingMarker(destination), { force: true }); + writeFileSync(ownershipMarker(destination), JSON.stringify({ manifestDigest: digest, packageDigest, closureDigest: closure, interpreterDigest: sha256(readFileSync(finalInterpreter)), launcherDigest: sha256(readFileSync(finalLauncher)) }) + "\n", { mode: 0o600, flag: "wx" }); + return resolveManagedWrenRuntime({ ...(options.packageRoot ? { packageRoot: options.packageRoot } : {}), runtimeRoot: root }); + } catch (error) { if (descriptor !== undefined) cleanupRecognisedStaging(root, digest); if (error instanceof ManagedWrenRuntimeError) throw error; return failure("codex_wren_provision_failed"); + } finally { if (descriptor !== undefined) { try { rmSync(lock, { force: true }); } catch { /* next run reports unavailable */ } } } +} + +export function managedWrenClosureDigest(manifest: ManagedWrenManifest): string { return closureDigest(manifest); } diff --git a/apps/genbi/server/native-sessions.ts b/apps/genbi/server/native-sessions.ts index ac71dd75a..bed0755ab 100644 --- a/apps/genbi/server/native-sessions.ts +++ b/apps/genbi/server/native-sessions.ts @@ -19,6 +19,7 @@ import { createNativeSessionWorkspace, initializeNativeSessionStateBase, nativeS import type { NativeSessionStateBase } from "./native-session-workspace.js"; import { NativeWrenRuntimeError, resolveNativeWrenRuntime } from "./native-wren-runtime.js"; import type { NativeWrenRuntime } from "./native-wren-runtime.js"; +import { ManagedWrenRuntimeError } from "./managed-wren-runtime.js"; import { assertNativeExecutableIdentity, assertNativeRuntimeSpec, attestNativeExecutable, buildNativeChildEnvironment, buildNativeRuntimeSpec, nativeProcessEnvironment, resolveNativeExecutable } from "./native-runtime-spec.js"; import type { NativeChildEnvironment, NativeExecutableIdentity, NativeRuntimeSpec } from "./native-runtime-spec.js"; import { createEmptyCodexWrenHome, materializeCodexWrenHome } from "./native-wren-home.js"; @@ -674,6 +675,8 @@ export async function probeNativeSessionProducer(input: { readonly warbleBin: string; readonly irPaths: Readonly>; readonly wrenShim?: string; + /** Production Codex probes receive the same immutable-record resolver used at launch. */ + readonly resolveManagedCodexWrenRuntime?: () => NativeWrenRuntime; readonly nativeHome?: string; readonly nodeExecutable?: NativeExecutableIdentity; readonly childToolDirectories?: readonly string[]; @@ -725,7 +728,14 @@ export async function probeNativeSessionProducer(input: { const scopeId = `preflight-${purpose}-${vendor}`; const scopePath = path.join(root, "scope.json"); const mcpPath = path.join(root, "mcp.json"); - const wrenRuntime = vendor === "codex" ? resolveNativeWrenRuntime(input.wrenShim) : undefined; + // Production passes the sealed managed record. The legacy resolver is a + // compatibility seam for deterministic fixture tests only; bin.ts does + // not provide a shim and so can never select an editable installation. + const wrenRuntime = vendor === "codex" + ? input.resolveManagedCodexWrenRuntime + ? input.resolveManagedCodexWrenRuntime() + : resolveNativeWrenRuntime(input.wrenShim) + : undefined; const binding = NATIVE_DISPATCH_REGISTRY[purpose].scopeKind === "bound_project" ? { project_identity: "native-preflight-project", generation: "1", revision: "native-preflight-revision" } : undefined; @@ -742,7 +752,7 @@ export async function probeNativeSessionProducer(input: { break; } } catch (error) { - outcome = error instanceof NativeWrenRuntimeError + outcome = error instanceof NativeWrenRuntimeError || error instanceof ManagedWrenRuntimeError ? fail(`result=wren_runtime_unavailable (${error.message})`, error.message) : fail("result=preflight_workspace_unavailable", "the preflight could not create its temporary workspace"); break; @@ -1010,6 +1020,7 @@ export class NativeSessionService { nativeHome: this.nativeHome, nodeExecutable: this.nodeExecutable, childToolDirectories: this.childToolDirectories, + ...(this.options.resolveManagedCodexWrenRuntime ? { resolveManagedCodexWrenRuntime: this.options.resolveManagedCodexWrenRuntime } : {}), ...(this.options.pathValue !== undefined ? { pathValue: this.options.pathValue } : {}), wrenShim: this.wrenShim, }); diff --git a/apps/genbi/test/managed-wren-runtime.test.ts b/apps/genbi/test/managed-wren-runtime.test.ts new file mode 100644 index 000000000..0b77574bd --- /dev/null +++ b/apps/genbi/test/managed-wren-runtime.test.ts @@ -0,0 +1,175 @@ +import { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs"; +import { execFileSync } from "node:child_process"; +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, managedWrenClosureDigest, manifestDigest, provisionManagedWrenRuntime, readManagedWrenManifest, resolveManagedWrenRuntime } from "../server/managed-wren-runtime.js"; + +const roots: string[] = []; +const digest = (value: string | Uint8Array) => createHash("sha256").update(value).digest("hex"); + +function fixture() { + const root = realpathSync(mkdtempSync(path.join(tmpdir(), "genbi-managed-wren-"))); roots.push(root); + const packageRoot = path.join(root, "package"); const runtimeRoot = path.join(root, "runtime"); + mkdirSync(path.join(packageRoot, "managed-wren"), { recursive: true, mode: 0o700 }); mkdirSync(runtimeRoot, { mode: 0o700 }); chmodSync(runtimeRoot, 0o700); + const archiveDigest = digest("python-archive"); const wheelDigest = digest("wheel"); + const manifest = { + schema: 1, activation: "approved", platform: "darwin-arm64", + compatibility: { genbi: "0.0.4", profile: "genbi-native-v4", wren: "0.13.0" }, + python: { implementation: "cpython", version: "3.11.16", upstream: { release: "20260901", url: "https://example.invalid/upstream", sha256: archiveDigest }, mirror: { url: "https://github.com/Canner/WrenAI/releases/download/managed-wren-v0.0.4/python.tar.gz", sha256: archiveDigest }, interpreterPath: "python/install/bin/python3.11" }, + wheels: [{ distribution: "wrenai", version: "0.13.0", filename: "wrenai-0.13.0-py3-none-any.whl", url: "https://github.com/Canner/WrenAI/releases/download/managed-wren-v0.0.4/wrenai-0.13.0-py3-none-any.whl", sourceUrl: "https://files.pythonhosted.org/wrenai-0.13.0-py3-none-any.whl", sha256: wheelDigest }], + runtime: { pythonArchivePath: "python.tar.gz", venvInterpreterPath: "venv/bin/python", launcherPath: "venv/bin/wren", module: "wren.cli:app", packagePath: "venv/lib/python3.11/site-packages/wren", packageTreeSha256: "staged", closureSha256: "staged" }, + licenseApproval: { state: "approved", evidence: "release-evidence" }, + } as const; + const closure = managedWrenClosureDigest(manifest as never); + const packageDigest = digest(`__init__.py\0${digest("__version__ = '0.13.0'\n")}`); + const approved = { ...manifest, runtime: { ...manifest.runtime, packageTreeSha256: packageDigest, closureSha256: closure } }; + writeFileSync(path.join(packageRoot, "managed-wren", "manifest.json"), JSON.stringify(approved)); + const parsed = readManagedWrenManifest(packageRoot); const generation = path.join(runtimeRoot, manifestDigest(parsed)); + for (const directory of [path.join(generation, "python", "install", "bin"), path.join(generation, "venv", "bin"), path.join(generation, "venv", "lib", "python3.11", "site-packages", "wren"), path.join(generation, "wheels")]) mkdirSync(directory, { recursive: true, mode: 0o700 }); + const python = path.join(generation, "python", "install", "bin", "python3.11"); const venvPython = path.join(generation, "venv", "bin", "python"); const launcher = path.join(generation, "venv", "bin", "wren"); + writeFileSync(path.join(generation, "python.tar.gz"), "python-archive", { mode: 0o600 }); + writeFileSync(path.join(generation, "wheels", manifest.wheels[0].filename), "wheel", { mode: 0o600 }); + writeFileSync(python, "#!/bin/sh\nexit 0\n", { mode: 0o700 }); writeFileSync(venvPython, "#!/bin/sh\nexit 0\n", { mode: 0o700 }); writeFileSync(launcher, `#!${venvPython}\nfrom wren.cli import app\n`, { mode: 0o700 }); writeFileSync(path.join(generation, "venv", "lib", "python3.11", "site-packages", "wren", "__init__.py"), "__version__ = '0.13.0'\n", { mode: 0o600 }); + writeFileSync(path.join(generation, ".genbi-managed-wren.json"), JSON.stringify({ manifestDigest: manifestDigest(parsed), packageDigest, closureDigest: closure, interpreterDigest: digest("#!/bin/sh\nexit 0\n"), launcherDigest: digest(`#!${venvPython}\nfrom wren.cli import app\n`) }), { mode: 0o600 }); chmodSync(generation, 0o700); + return { packageRoot, runtimeRoot, generation, launcher, manifest: approved }; +} + +afterEach(() => { vi.unstubAllGlobals(); while (roots.length) rmSync(roots.pop()!, { recursive: true, force: true }); }); + +function provisionFixture(version = "0.13.0") { + const root = realpathSync(mkdtempSync(path.join(tmpdir(), "genbi-managed-wren-provision-"))); roots.push(root); + const packageRoot = path.join(root, "installed-package"); const runtimeRoot = path.join(root, "runtime"); const source = path.join(root, "python-source"); + mkdirSync(path.join(packageRoot, "managed-wren"), { recursive: true, mode: 0o700 }); mkdirSync(runtimeRoot, { mode: 0o700 }); mkdirSync(path.join(source, "python", "install", "bin"), { recursive: true, mode: 0o700 }); chmodSync(runtimeRoot, 0o700); + const python = path.join(source, "python", "install", "bin", "python3.11"); + const fakePython = [ + "#!/bin/sh", + "if [ \"$1\" = \"-m\" ] && [ \"$2\" = \"venv\" ]; then", + " target=\"$3\"", + " /bin/mkdir -p \"$target/bin\" \"$target/lib/python3.11/site-packages/wren\"", + " /bin/cp \"$0\" \"$target/bin/python\"", + " /bin/chmod 700 \"$target/bin/python\"", + " /usr/bin/printf '#!%s\\nfrom wren.cli import app\\n' \"$target/bin/python\" > \"$target/bin/wren\"", + " /bin/chmod 700 \"$target/bin/wren\"", + ` /usr/bin/printf \"__version__ = '${version}'\\n\" > \"$target/lib/python3.11/site-packages/wren/__init__.py\"`, + " /bin/chmod 600 \"$target/lib/python3.11/site-packages/wren/__init__.py\"", + " exit 0", + "fi", + "if [ \"$1\" = \"-m\" ] && [ \"$2\" = \"pip\" ]; then", + " /usr/bin/printf '%s\\n' \"$*\" > \"$HOME/pip-args\"", + " exit 0", + "fi", + "exit 1", + "", + ].join("\n"); + writeFileSync(python, fakePython, { mode: 0o700 }); + const archive = path.join(root, "python.tar.gz"); execFileSync("tar", ["-czf", archive, "-C", source, "."]); + const archiveBytes = readFileSync(archive); const wheelBytes = Buffer.from(`fixture wheel ${version}\n`); + const base = { + schema: 1, activation: "approved", platform: "darwin-arm64", + compatibility: { genbi: "0.0.4", profile: "genbi-native-v4", wren: version }, + python: { implementation: "cpython", version: "3.11.16", upstream: { release: "20260901", url: "https://example.invalid/upstream", sha256: digest(archiveBytes) }, mirror: { url: `https://github.com/Canner/WrenAI/releases/download/fixture-${version}/python.tar.gz`, sha256: digest(archiveBytes) }, interpreterPath: "python/install/bin/python3.11" }, + wheels: [{ distribution: "wrenai", version, filename: `wrenai-${version}-py3-none-any.whl`, url: `https://github.com/Canner/WrenAI/releases/download/fixture-${version}/wrenai-${version}-py3-none-any.whl`, sourceUrl: `https://files.pythonhosted.org/fixture/wrenai-${version}-py3-none-any.whl`, sha256: digest(wheelBytes) }], + runtime: { pythonArchivePath: "python.tar.gz", venvInterpreterPath: "venv/bin/python", launcherPath: "venv/bin/wren", module: "wren.cli:app", packagePath: "venv/lib/python3.11/site-packages/wren", packageTreeSha256: digest(`__init__.py\0${digest(`__version__ = '${version}'\n`)}`), closureSha256: "staged" }, + licenseApproval: { state: "approved", evidence: "fixture" }, + } as const; + const manifest = { ...base, runtime: { ...base.runtime, closureSha256: managedWrenClosureDigest(base as never) } }; + writeFileSync(path.join(packageRoot, "managed-wren", "manifest.json"), JSON.stringify(manifest)); + const requests: string[] = []; + const installFetch = (beforeResponse?: () => Promise) => vi.stubGlobal("fetch", vi.fn(async (input: string | URL) => { + requests.push(String(input)); if (beforeResponse) await beforeResponse(); + if (String(input) === manifest.python.mirror.url) return new Response(archiveBytes); + if (String(input) === manifest.wheels[0].url) return new Response(wheelBytes); + return new Response("not found", { status: 404 }); + })); + return { packageRoot, runtimeRoot, manifest, requests, installFetch }; +} + +describe("managed Wren runtime", () => { + it("accepts only an approved, immutable record and rejects every tampered launch link", () => { + const value = fixture(); + expect(resolveManagedWrenRuntime({ packageRoot: value.packageRoot, runtimeRoot: value.runtimeRoot })).toMatchObject({ generation_root: value.generation, launcher: value.launcher }); + for (const mutation of ["interpreter", "launcher", "package", "marker"] as const) { + const next = fixture(); + if (mutation === "interpreter") writeFileSync(path.join(next.generation, "venv", "bin", "python"), "not executable"); + if (mutation === "launcher") writeFileSync(next.launcher, "#!/wrong/python\n"); + if (mutation === "package") writeFileSync(path.join(next.generation, "venv", "lib", "python3.11", "site-packages", "wren", "__init__.py"), "tampered\n"); + if (mutation === "marker") writeFileSync(path.join(next.generation, ".genbi-managed-wren.json"), "{}", { mode: 0o600 }); + expect(() => resolveManagedWrenRuntime({ packageRoot: next.packageRoot, runtimeRoot: next.runtimeRoot })).toThrow(ManagedWrenRuntimeError); + } + }); + + it("rejects mutable, unapproved, or non-mirror manifest input before provisioning", () => { + const value = fixture(); + const manifestPath = path.join(value.packageRoot, "managed-wren", "manifest.json"); + const original = JSON.parse(JSON.stringify(value.manifest)); + for (const mutate of [ + (m: any) => { m.activation = "staged"; m.licenseApproval.state = "pending"; }, + (m: any) => { m.python.mirror.url = "https://example.com/python.tar.gz"; }, + (m: any) => { m.wheels[0].filename = "../escape.whl"; }, + ]) { + const changed = JSON.parse(JSON.stringify(original)); mutate(changed); writeFileSync(manifestPath, JSON.stringify(changed)); + expect(() => resolveManagedWrenRuntime({ packageRoot: value.packageRoot, runtimeRoot: value.runtimeRoot })).toThrow(ManagedWrenRuntimeError); + } + }); + + 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 }); + expect(first.generation_root).toContain(first.manifest_digest); + expect(value.requests).toEqual([value.manifest.python.mirror.url, value.manifest.wheels[0].url]); + expect(readFileSync(path.join(first.generation_root, "pip-args"), "utf8")).toContain("--no-index --no-deps --require-hashes"); + const requestCount = value.requests.length; + await expect(provisionManagedWrenRuntime({ packageRoot: value.packageRoot, runtimeRoot: value.runtimeRoot })).resolves.toMatchObject({ generation_root: first.generation_root }); + expect(value.requests).toHaveLength(requestCount); + }, 20_000); + + it("waits for a concurrent first-use owner and both callers receive the same generation", async () => { + const value = provisionFixture(); let release!: () => void; const paused = new Promise((resolve) => { release = resolve; }); let firstFetch = true; + value.installFetch(async () => { if (firstFetch) { firstFetch = false; await paused; } }); + const owner = provisionManagedWrenRuntime({ packageRoot: value.packageRoot, runtimeRoot: value.runtimeRoot }); + await new Promise((resolve) => setTimeout(resolve, 30)); + const waiter = provisionManagedWrenRuntime({ packageRoot: value.packageRoot, runtimeRoot: value.runtimeRoot }); + release(); + const [a, b] = await Promise.all([owner, waiter]); + expect(a.generation_root).toBe(b.generation_root); + expect(value.requests).toHaveLength(2); + }, 20_000); + + it("recovers only marked stale staging and preserves untrusted or rollback state", async () => { + const value = provisionFixture(); const digestValue = manifestDigest(readManagedWrenManifest(value.packageRoot)); + const stale = path.join(value.runtimeRoot, `.staging-${digestValue}-stale`); mkdirSync(stale, { mode: 0o700 }); chmodSync(stale, 0o700); writeFileSync(path.join(stale, ".genbi-managed-wren-staging.json"), JSON.stringify({ manifestDigest: digestValue }), { mode: 0o600 }); + const untrusted = path.join(value.runtimeRoot, `.staging-${digestValue}-untrusted`); mkdirSync(untrusted, { mode: 0o700 }); chmodSync(untrusted, 0o700); writeFileSync(path.join(untrusted, "keep"), "keep"); + const rollback = path.join(value.runtimeRoot, "previous-generation"); mkdirSync(rollback, { mode: 0o700 }); chmodSync(rollback, 0o700); writeFileSync(path.join(rollback, "keep"), "keep"); + value.installFetch(); await provisionManagedWrenRuntime({ packageRoot: value.packageRoot, runtimeRoot: value.runtimeRoot }); + expect(existsSync(stale)).toBe(false); expect(readFileSync(path.join(untrusted, "keep"), "utf8")).toBe("keep"); expect(readFileSync(path.join(rollback, "keep"), "utf8")).toBe("keep"); + }); + + it("retains older generations and keeps an active record pinned across an update", async () => { + const old = provisionFixture("0.13.0"); old.installFetch(); const active = await provisionManagedWrenRuntime({ packageRoot: old.packageRoot, runtimeRoot: old.runtimeRoot }); + const next = provisionFixture("0.13.1"); next.installFetch(); const updated = await provisionManagedWrenRuntime({ packageRoot: next.packageRoot, runtimeRoot: old.runtimeRoot }); + expect(updated.generation_root).not.toBe(active.generation_root); + expect(lstatSync(active.generation_root).isDirectory()).toBe(true); + expect(resolveManagedWrenRuntime({ packageRoot: old.packageRoot, runtimeRoot: old.runtimeRoot }).launcher).toBe(active.launcher); + }); + + it("rejects retained archive and wheel tampering after a successful provision", async () => { + for (const target of ["python.tar.gz", "wheel"] as const) { + const next = provisionFixture(); next.installFetch(); const nextRecord = await provisionManagedWrenRuntime({ packageRoot: next.packageRoot, runtimeRoot: next.runtimeRoot }); + writeFileSync(target === "python.tar.gz" ? path.join(nextRecord.generation_root, target) : path.join(nextRecord.generation_root, "wheels", next.manifest.wheels[0].filename), "tampered"); + expect(() => resolveManagedWrenRuntime({ packageRoot: next.packageRoot, runtimeRoot: next.runtimeRoot })).toThrow(ManagedWrenRuntimeError); + } + }); + + it("derives a closed staged candidate manifest from the selected release closure", () => { + const root = realpathSync(mkdtempSync(path.join(tmpdir(), "genbi-managed-wren-release-"))); roots.push(root); + const packagePath = path.join(root, "runtime", "venv", "lib", "python3.11", "site-packages", "wren"); mkdirSync(packagePath, { recursive: true, mode: 0o700 }); writeFileSync(path.join(packagePath, "__init__.py"), "__version__ = '0.13.0'\n", { mode: 0o600 }); + const wheel = { distribution: "wrenai", version: "0.13.0", filename: "wrenai-0.13.0-py3-none-any.whl", sourceUrl: "https://files.pythonhosted.org/fixture/wrenai-0.13.0-py3-none-any.whl", sha256: "b".repeat(64), license: "Apache-2.0" }; + writeFileSync(path.join(root, "wheel-inputs.json"), JSON.stringify([wheel])); + execFileSync(process.execPath, [path.resolve("scripts", "managed-wren-release.mjs"), root, "managed-wren-fixture"], { cwd: path.resolve("."), stdio: "pipe" }); + const candidate = JSON.parse(readFileSync(path.join(root, "managed-wren-manifest.candidate.json"), "utf8")); + expect(candidate).toMatchObject({ activation: "staged", wheels: [{ distribution: wheel.distribution, version: wheel.version, filename: wheel.filename, url: "https://github.com/Canner/WrenAI/releases/download/managed-wren-fixture/wrenai-0.13.0-py3-none-any.whl" }] }); + expect(candidate.runtime.packageTreeSha256).not.toBe("staged"); expect(candidate.runtime.closureSha256).not.toBe("staged"); + }); +}); diff --git a/apps/genbi/test/packaging-contract.test.ts b/apps/genbi/test/packaging-contract.test.ts index 1d72d0cf4..19a29be31 100644 --- a/apps/genbi/test/packaging-contract.test.ts +++ b/apps/genbi/test/packaging-contract.test.ts @@ -17,6 +17,23 @@ describe("installed-package and vendor-contract CI wiring", () => { expect(packageJson.scripts["check:installed-package"]).toBe("node scripts/installed-package-acceptance.mjs"); }); + it("ships the staged managed-Wren manifest but never treats its release input as an active runtime", () => { + expect(packageJson.files).toContain("managed-wren"); + const manifest = JSON.parse(readFileSync(path.join(packageRoot, "managed-wren", "manifest.json"), "utf8")); + expect(manifest).toMatchObject({ schema: 1, platform: "darwin-arm64", activation: "staged", licenseApproval: { state: "pending" } }); + expect(manifest.python.mirror.url).toMatch(/^https:\/\/github\.com\/Canner\/WrenAI\/releases\/download\//); + expect(manifest.wheels[0]).toMatchObject({ distribution: "wrenai", version: "0.13.0" }); + }); + + it("keeps managed-Wren publication and manifest activation behind the protected approval job", () => { + const workflow = readFileSync(path.join(repositoryRoot, ".github", "workflows", "managed-wren-runtime.yml"), "utf8"); + expect(workflow).toContain("managed-wren-license-approved"); + expect(workflow).toContain("if: ${{ inputs.license_approval == 'approved' }}"); + expect(workflow).toContain("managed-wren-manifest.candidate.json"); + expect(workflow).toContain("--no-index --no-deps --require-hashes"); + expect(workflow).toContain("gh release create"); + }); + it("pins the context loader exactly and runs its no-checkout acceptance on the certified macOS target", () => { expect(packageJson.dependencies["@wrenai/context-loader"]).toBe("0.1.0"); const workflow = readFileSync(path.join(repositoryRoot, ".github", "workflows", "genbi-ci.yml"), "utf8");