@wrenai/genbi 0.0.4 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -2
- package/dist/assets/{echarts-LTFC-ysd.js → echarts-DYM4hjvk.js} +2 -2
- package/dist/assets/index-A_Wrq7Ey.css +1 -0
- package/dist/assets/{index-Bo--QMUN.js → index-DsH129tV.js} +18 -17
- package/dist/index.html +2 -2
- package/dist-server/harness/bundle/schema.d.ts +38 -0
- package/dist-server/harness/bundle/schema.js +18 -10
- package/dist-server/harness/bundle/version.js +1 -1
- package/dist-server/harness/cli-args.d.ts +10 -2
- package/dist-server/harness/cli-args.js +24 -8
- package/dist-server/harness/cli.js +75 -9
- package/dist-server/harness/compile/cache.js +4 -4
- package/dist-server/harness/compile/compose-profile.d.ts +51 -14
- package/dist-server/harness/compile/compose-profile.js +85 -19
- package/dist-server/harness/compile/context-loader.d.ts +56 -0
- package/dist-server/harness/compile/context-loader.js +237 -0
- package/dist-server/harness/compile/errors.d.ts +19 -0
- package/dist-server/harness/compile/errors.js +35 -0
- package/dist-server/harness/compile/index.d.ts +4 -3
- package/dist-server/harness/compile/index.js +4 -3
- package/dist-server/harness/compile/pipeline.d.ts +15 -7
- package/dist-server/harness/compile/pipeline.js +62 -14
- package/dist-server/harness/compile/resolve-binary.d.ts +2 -2
- package/dist-server/harness/compile/resolve-binary.js +3 -3
- package/dist-server/harness/compile/types.d.ts +29 -0
- package/dist-server/harness/compile/warble-identity.d.ts +11 -0
- package/dist-server/harness/compile/warble-identity.js +13 -0
- package/dist-server/harness/components/ai-step.d.ts +4 -0
- package/dist-server/harness/components/ai-step.js +40 -0
- package/dist-server/harness/components/broker.d.ts +60 -0
- package/dist-server/harness/components/broker.js +117 -0
- package/dist-server/harness/components/capability-card.d.ts +98 -0
- package/dist-server/harness/components/capability-card.js +169 -0
- package/dist-server/harness/components/context.d.ts +2 -0
- package/dist-server/harness/components/context.js +30 -0
- package/dist-server/harness/components/display.d.ts +6 -0
- package/dist-server/harness/components/display.js +29 -0
- package/dist-server/harness/components/egress-judge.d.ts +12 -0
- package/dist-server/harness/components/egress-judge.js +27 -0
- package/dist-server/harness/components/egress.d.ts +142 -0
- package/dist-server/harness/components/egress.js +469 -0
- package/dist-server/harness/components/in-process.d.ts +7 -0
- package/dist-server/harness/components/in-process.js +245 -0
- package/dist-server/harness/components/normalize.d.ts +35 -0
- package/dist-server/harness/components/normalize.js +245 -0
- package/dist-server/harness/components/plan.d.ts +31 -0
- package/dist-server/harness/components/plan.js +184 -0
- package/dist-server/harness/components/report.d.ts +207 -0
- package/dist-server/harness/components/report.js +309 -0
- package/dist-server/harness/components/runner.d.ts +193 -0
- package/dist-server/harness/components/runner.js +492 -0
- package/dist-server/harness/components/semantic-guards.d.ts +20 -0
- package/dist-server/harness/components/semantic-guards.js +42 -0
- package/dist-server/harness/components/terminal-json.d.ts +18 -0
- package/dist-server/harness/components/terminal-json.js +82 -0
- package/dist-server/harness/components/usage.d.ts +14 -0
- package/dist-server/harness/components/usage.js +19 -0
- package/dist-server/harness/components/wren-access.d.ts +29 -0
- package/dist-server/harness/components/wren-access.js +233 -0
- package/dist-server/harness/components/zone-dry-run.d.ts +43 -0
- package/dist-server/harness/components/zone-dry-run.js +107 -0
- package/dist-server/harness/components/zone-gate.d.ts +76 -0
- package/dist-server/harness/components/zone-gate.js +144 -0
- package/dist-server/harness/events/index.d.ts +1 -1
- package/dist-server/harness/events/types.d.ts +52 -0
- package/dist-server/harness/index.d.ts +9 -2
- package/dist-server/harness/index.js +5 -1
- package/dist-server/harness/loop/executor.d.ts +1 -0
- package/dist-server/harness/loop/executor.js +37 -15
- package/dist-server/harness/npm-package-resolve.d.ts +2 -0
- package/dist-server/harness/npm-package-resolve.js +16 -4
- package/dist-server/harness/providers/adapters/openai-compatible.d.ts +13 -0
- package/dist-server/harness/providers/adapters/openai-compatible.js +17 -0
- package/dist-server/harness/providers/adapters/openai.d.ts +7 -0
- package/dist-server/harness/providers/adapters/openai.js +6 -0
- package/dist-server/harness/providers/adapters/pi-ai.d.ts +48 -0
- package/dist-server/harness/providers/adapters/pi-ai.js +518 -0
- package/dist-server/harness/providers/binding.d.ts +45 -4
- package/dist-server/harness/providers/binding.js +44 -8
- package/dist-server/harness/providers/index.d.ts +9 -3
- package/dist-server/harness/providers/index.js +10 -3
- package/dist-server/harness/providers/zone.d.ts +96 -0
- package/dist-server/harness/providers/zone.js +112 -0
- package/dist-server/harness/render/answering-query.d.ts +29 -0
- package/dist-server/harness/render/answering-query.js +62 -0
- package/dist-server/harness/render/envelope.d.ts +6 -0
- package/dist-server/harness/render/envelope.js +10 -3
- package/dist-server/harness/route/adapter-spec.d.ts +8 -9
- package/dist-server/harness/route/adapter-spec.js +13 -14
- package/dist-server/harness/route/codex-ask-events.js +2 -1
- package/dist-server/harness/route/codex-ask.d.ts +4 -0
- package/dist-server/harness/route/codex-ask.js +69 -30
- package/dist-server/harness/route/codex-host-query.d.ts +52 -0
- package/dist-server/harness/route/codex-host-query.js +191 -0
- package/dist-server/harness/route/codex-local-manifest.d.ts +9 -1
- package/dist-server/harness/route/codex-local-manifest.js +96 -15
- package/dist-server/harness/route/codex-wren-proxy.d.ts +33 -0
- package/dist-server/harness/route/codex-wren-proxy.js +227 -0
- package/dist-server/harness/route/describe.js +45 -6
- package/dist-server/harness/route/in-process.d.ts +22 -1
- package/dist-server/harness/route/in-process.js +54 -4
- package/dist-server/harness/route/route.js +5 -2
- package/dist-server/harness/route/types.d.ts +30 -1
- package/dist-server/harness/session/run.js +30 -10
- package/dist-server/harness/setup/runner.d.ts +2 -0
- package/dist-server/harness/setup/runner.js +38 -17
- package/dist-server/harness/setup/yaml-scalar.d.ts +23 -0
- package/dist-server/harness/setup/yaml-scalar.js +62 -0
- package/dist-server/server/adopt.js +1 -17
- package/dist-server/server/app.d.ts +1 -0
- package/dist-server/server/app.js +163 -14
- package/dist-server/server/auth-choice.d.ts +13 -1
- package/dist-server/server/auth-choice.js +66 -1
- package/dist-server/server/bin.js +177 -25
- package/dist-server/server/codex-local-grounding.d.ts +18 -0
- package/dist-server/server/codex-local-grounding.js +92 -0
- package/dist-server/server/codex-native-session.d.ts +25 -0
- package/dist-server/server/codex-native-session.js +1 -0
- package/dist-server/server/codex-runtime-composition.d.ts +34 -0
- package/dist-server/server/codex-runtime-composition.js +250 -0
- package/dist-server/server/codex-structured-ask.d.ts +23 -0
- package/dist-server/server/codex-structured-ask.js +102 -0
- package/dist-server/server/conn-config.js +4 -16
- package/dist-server/server/conversation-socket.d.ts +16 -0
- package/dist-server/server/conversation-socket.js +65 -0
- package/dist-server/server/db.d.ts +46 -0
- package/dist-server/server/db.js +88 -9
- package/dist-server/server/env-detect.d.ts +6 -0
- package/dist-server/server/env-detect.js +7 -0
- package/dist-server/server/fold.d.ts +22 -4
- package/dist-server/server/fold.js +37 -5
- package/dist-server/server/harness.d.ts +2 -2
- package/dist-server/server/harness.js +33 -7
- package/dist-server/server/interactive-terminal.d.ts +10 -6
- package/dist-server/server/interactive-terminal.js +33 -8
- package/dist-server/server/managed-wren-runtime.d.ts +116 -0
- package/dist-server/server/managed-wren-runtime.js +670 -0
- package/dist-server/server/native-artifacts.d.ts +23 -7
- package/dist-server/server/native-artifacts.js +43 -12
- package/dist-server/server/native-component-context.d.ts +19 -0
- package/dist-server/server/native-component-context.js +76 -0
- package/dist-server/server/native-component-preparation.d.ts +20 -0
- package/dist-server/server/native-component-preparation.js +129 -0
- package/dist-server/server/native-component-runtime.d.ts +36 -0
- package/dist-server/server/native-component-runtime.js +92 -0
- package/dist-server/server/native-components.d.ts +84 -0
- package/dist-server/server/native-components.js +160 -0
- package/dist-server/server/native-dispatch-process.d.ts +8 -0
- package/dist-server/server/native-dispatch-process.js +65 -0
- package/dist-server/server/native-runtime-spec.d.ts +89 -0
- package/dist-server/server/native-runtime-spec.js +169 -0
- package/dist-server/server/native-sessions.d.ts +172 -19
- package/dist-server/server/native-sessions.js +839 -244
- package/dist-server/server/native-shutdown.d.ts +6 -0
- package/dist-server/server/native-shutdown.js +28 -0
- package/dist-server/server/native-wren-home.d.ts +18 -0
- package/dist-server/server/native-wren-home.js +262 -0
- package/dist-server/server/node-pty-host.d.ts +2 -1
- package/dist-server/server/node-pty-host.js +3 -2
- package/dist-server/server/profile-registry.d.ts +139 -0
- package/dist-server/server/profile-registry.js +620 -0
- package/dist-server/server/runtime-binding.d.ts +4 -1
- package/dist-server/server/runtime-binding.js +50 -2
- package/dist-server/server/runtime-host/claude-component-step.d.ts +25 -0
- package/dist-server/server/runtime-host/claude-component-step.js +186 -0
- package/dist-server/server/runtime-host/codex-app-server.d.ts +75 -0
- package/dist-server/server/runtime-host/codex-app-server.js +239 -0
- package/dist-server/server/runtime-host/codex-compatibility.d.ts +110 -0
- package/dist-server/server/runtime-host/codex-compatibility.js +162 -0
- package/dist-server/server/runtime-host/codex-component-step.d.ts +19 -0
- package/dist-server/server/runtime-host/codex-component-step.js +308 -0
- package/dist-server/server/runtime-host/codex-conversation.d.ts +109 -0
- package/dist-server/server/runtime-host/codex-conversation.js +295 -0
- package/dist-server/server/runtime-host/codex-direct-tools.d.ts +23 -0
- package/dist-server/server/runtime-host/codex-direct-tools.js +59 -0
- package/dist-server/server/runtime-host/codex-events.d.ts +647 -0
- package/dist-server/server/runtime-host/codex-events.js +66 -0
- package/dist-server/server/runtime-host/codex-policy.d.ts +16 -0
- package/dist-server/server/runtime-host/codex-policy.js +151 -0
- package/dist-server/server/runtime-host/codex-process.d.ts +8 -0
- package/dist-server/server/runtime-host/codex-process.js +96 -0
- package/dist-server/server/runtime-host/codex-rpc.d.ts +47 -0
- package/dist-server/server/runtime-host/codex-rpc.js +182 -0
- package/dist-server/server/runtime-host/codex-session.d.ts +74 -0
- package/dist-server/server/runtime-host/codex-session.js +492 -0
- package/dist-server/server/runtime-host/codex-terminal-protocol.d.ts +35 -0
- package/dist-server/server/runtime-host/codex-terminal-protocol.js +243 -0
- package/dist-server/server/runtime-host/codex-terminal.d.ts +16 -0
- package/dist-server/server/runtime-host/codex-terminal.js +120 -0
- package/dist-server/server/runtime-host/index.d.ts +5 -0
- package/dist-server/server/runtime-host/index.js +3 -0
- package/dist-server/server/runtime-host/local.d.ts +29 -0
- package/dist-server/server/runtime-host/local.js +90 -0
- package/dist-server/server/runtime-host/policy.d.ts +8 -0
- package/dist-server/server/runtime-host/policy.js +78 -0
- package/dist-server/server/runtime-host/types.d.ts +52 -0
- package/dist-server/server/runtime-host/types.js +62 -0
- package/dist-server/server/structured-query-evidence.d.ts +7 -0
- package/dist-server/server/structured-query-evidence.js +47 -0
- package/dist-server/server/structured-runtime.d.ts +52 -0
- package/dist-server/server/structured-runtime.js +130 -0
- package/dist-server/server/structured-scope.d.ts +4 -0
- package/dist-server/server/structured-scope.js +12 -0
- package/dist-server/server/turn.d.ts +6 -1
- package/dist-server/server/turn.js +82 -25
- package/dist-server/server/wire-types.d.ts +67 -3
- package/dist-server/server/wren-profiles.js +1 -1
- package/managed-wren/README.md +60 -0
- package/managed-wren/dependency-wheels.json +282 -0
- package/managed-wren/manifest.json +373 -0
- package/managed-wren/release-inputs.json +45 -0
- package/managed-wren/runtime-tree.cjs +115 -0
- package/managed-wren/source-inputs.json +40 -0
- package/package.json +15 -6
- package/profiles/genbi-default/context/binding.yml +17 -7
- package/profiles/genbi-default/context/context.json +146 -0
- package/profiles/genbi-default/ir.golden.json +39 -43
- package/profiles/genbi-default/profile.yml +31 -0
- package/profiles/genbi-enrich-context/context/binding.yml +14 -3
- package/profiles/genbi-enrich-context/context/context.json +146 -0
- package/profiles/genbi-enrich-context/ir.golden.json +4 -5
- package/profiles/genbi-monitor/context/binding.yml +13 -5
- package/profiles/genbi-monitor/context/context.json +146 -0
- package/profiles/genbi-monitor/ir.golden.json +13 -7
- package/profiles/genbi-report/context/binding.yml +19 -0
- package/profiles/genbi-report/context/context.json +223 -0
- package/profiles/genbi-report/ir.golden.json +328 -0
- package/profiles/genbi-report/profile.yml +38 -0
- package/profiles/genbi-setup/ir.golden.json +3 -1
- package/dist/assets/index-Xs4q7pNo.css +0 -1
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Immutable, release-managed Wren runtime for native Codex sessions.
|
|
3
|
+
*
|
|
4
|
+
* The packaged manifest is deliberately staged until the release workflow has
|
|
5
|
+
* mirrored every byte and passed its protected licence-approval environment.
|
|
6
|
+
* This module never substitutes a local checkout, PATH Python, user venv, or
|
|
7
|
+
* upstream URL for a missing approved record.
|
|
8
|
+
*/
|
|
9
|
+
import { createRequire } from "node:module";
|
|
10
|
+
import { execFileSync } from "node:child_process";
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { chmodSync, existsSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
13
|
+
import os from "node:os";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
const SHA256 = z.string().regex(/^[a-f0-9]{64}$/);
|
|
18
|
+
const RELATIVE = z.string().min(1).refine((value) => !path.isAbsolute(value) && !value.split(/[\\/]/).includes(".."));
|
|
19
|
+
const URL = z.string().url().refine((value) => value.startsWith("https://github.com/goldmedal/WrenAI/releases/download/"));
|
|
20
|
+
const stagedDigest = z.union([SHA256, z.literal("staged")]);
|
|
21
|
+
const managedWrenManifestSchema = z.object({
|
|
22
|
+
schema: z.literal(1),
|
|
23
|
+
activation: z.enum(["staged", "approved"]),
|
|
24
|
+
platform: z.literal("darwin-arm64"),
|
|
25
|
+
compatibility: z.object({ genbi: z.string().regex(/^\d+\.\d+\.\d+$/), profile: z.literal("genbi-native-v4"), wren: z.string().regex(/^\d+\.\d+\.\d+(?:\+genbi\.[1-9]\d*)?$/) }).strict(),
|
|
26
|
+
python: z.object({
|
|
27
|
+
implementation: z.literal("cpython"), version: z.string().regex(/^3\.11\.\d+$/),
|
|
28
|
+
upstream: z.object({ release: z.string().regex(/^\d{8}$/), url: z.string().url(), sha256: SHA256 }).strict(),
|
|
29
|
+
mirror: z.object({ url: URL, sha256: SHA256 }).strict(), interpreterPath: RELATIVE,
|
|
30
|
+
}).strict(),
|
|
31
|
+
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),
|
|
32
|
+
runtime: z.object({ pythonArchivePath: RELATIVE, venvInterpreterPath: RELATIVE, launcherPath: RELATIVE, module: z.literal("wren.cli:app"), packagePath: RELATIVE, sitePackagesPath: RELATIVE, pythonTreeSha256: stagedDigest, packageTreeSha256: stagedDigest, sitePackagesTreeSha256: stagedDigest, closureSha256: stagedDigest }).strict(),
|
|
33
|
+
licenseApproval: z.object({ state: z.enum(["pending", "approved"]), evidence: z.string().min(1).optional() }).strict(),
|
|
34
|
+
approvedManifest: z.object({ url: URL, sha256: SHA256 }).strict().optional(),
|
|
35
|
+
}).strict().superRefine((value, context) => {
|
|
36
|
+
if (new Set(value.wheels.map((wheel) => wheel.filename)).size !== value.wheels.length)
|
|
37
|
+
context.addIssue({ code: "custom", message: "duplicate wheel filename" });
|
|
38
|
+
if (value.wheels[0]?.distribution !== "wrenai" || value.wheels[0].version !== value.compatibility.wren)
|
|
39
|
+
context.addIssue({ code: "custom", message: "wrenai must be the first exact wheel" });
|
|
40
|
+
const approved = value.activation === "approved";
|
|
41
|
+
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"))) {
|
|
42
|
+
context.addIssue({ code: "custom", message: "activation requires approved licence evidence and attested digests" });
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
export class ManagedWrenRuntimeError extends Error {
|
|
46
|
+
code;
|
|
47
|
+
constructor(code, message = code) {
|
|
48
|
+
super(message);
|
|
49
|
+
this.code = code;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function failure(code) { throw new ManagedWrenRuntimeError(code); }
|
|
53
|
+
function sha256(value) { return createHash("sha256").update(value).digest("hex"); }
|
|
54
|
+
function contained(root, target) { const relative = path.relative(root, target); return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)); }
|
|
55
|
+
function platform() { return process.platform === "darwin" && process.arch === "arm64" ? "darwin-arm64" : undefined; }
|
|
56
|
+
function packageRoot() {
|
|
57
|
+
const parent = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
58
|
+
// Source runs from <package>/server; the packed server runs from
|
|
59
|
+
// <package>/dist-server/server. Both must read the package-owned manifest.
|
|
60
|
+
return path.basename(parent) === "dist-server" ? path.resolve(parent, "..") : parent;
|
|
61
|
+
}
|
|
62
|
+
export function defaultManagedWrenRoot() { return path.join(os.homedir(), "Library", "Application Support", "WrenAI", "genbi", "managed-wren"); }
|
|
63
|
+
export function managedWrenManifestPath(root = packageRoot()) { return path.join(root, "managed-wren", "manifest.json"); }
|
|
64
|
+
export function readManagedWrenManifest(root = packageRoot()) {
|
|
65
|
+
let raw;
|
|
66
|
+
try {
|
|
67
|
+
raw = JSON.parse(readFileSync(managedWrenManifestPath(root), "utf8"));
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return failure("codex_wren_manifest_missing");
|
|
71
|
+
}
|
|
72
|
+
const parsed = managedWrenManifestSchema.safeParse(raw);
|
|
73
|
+
if (!parsed.success)
|
|
74
|
+
return failure("codex_wren_manifest_invalid");
|
|
75
|
+
if (platform() !== parsed.data.platform)
|
|
76
|
+
return failure("codex_wren_platform_unsupported");
|
|
77
|
+
return Object.freeze(parsed.data);
|
|
78
|
+
}
|
|
79
|
+
function stableJson(value) {
|
|
80
|
+
if (Array.isArray(value))
|
|
81
|
+
return `[${value.map(stableJson).join(",")}]`;
|
|
82
|
+
if (value && typeof value === "object") {
|
|
83
|
+
const object = value;
|
|
84
|
+
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableJson(object[key])}`).join(",")}}`;
|
|
85
|
+
}
|
|
86
|
+
return JSON.stringify(value);
|
|
87
|
+
}
|
|
88
|
+
export function manifestDigest(manifest) { return sha256(stableJson(manifest)); }
|
|
89
|
+
function immutableDirectory(root, digest) { return path.join(root, digest); }
|
|
90
|
+
function assertPrivateDirectory(directory) {
|
|
91
|
+
try {
|
|
92
|
+
const entry = lstatSync(directory);
|
|
93
|
+
if (!entry.isDirectory() || entry.isSymbolicLink() || realpathSync(directory) !== directory || (entry.mode & 0o777) !== 0o700)
|
|
94
|
+
failure("codex_wren_runtime_unprovisioned");
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (error instanceof ManagedWrenRuntimeError)
|
|
98
|
+
throw error;
|
|
99
|
+
failure("codex_wren_runtime_unprovisioned");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function assertContainedRealpath(root, target, code) {
|
|
103
|
+
try {
|
|
104
|
+
const canonical = realpathSync(target);
|
|
105
|
+
if (!contained(root, canonical))
|
|
106
|
+
failure(code);
|
|
107
|
+
return canonical;
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (error instanceof ManagedWrenRuntimeError)
|
|
111
|
+
throw error;
|
|
112
|
+
return failure(code);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function regularExecutable(root, target, code) {
|
|
116
|
+
const canonical = assertContainedRealpath(root, target, code);
|
|
117
|
+
const metadata = statSync(canonical);
|
|
118
|
+
if (!metadata.isFile() || (metadata.mode & 0o111) === 0)
|
|
119
|
+
failure(code);
|
|
120
|
+
return canonical;
|
|
121
|
+
}
|
|
122
|
+
function regularFile(root, target, code) {
|
|
123
|
+
const canonical = assertContainedRealpath(root, target, code);
|
|
124
|
+
const metadata = statSync(canonical);
|
|
125
|
+
if (!metadata.isFile())
|
|
126
|
+
failure(code);
|
|
127
|
+
return canonical;
|
|
128
|
+
}
|
|
129
|
+
function assertSecureTree(root, code) {
|
|
130
|
+
try {
|
|
131
|
+
const visit = (directory) => {
|
|
132
|
+
for (const name of readdirSync(directory)) {
|
|
133
|
+
const target = path.join(directory, name);
|
|
134
|
+
const entry = lstatSync(target);
|
|
135
|
+
// Symlink permission bits are not an access-control surface and differ by
|
|
136
|
+
// host filesystem (Linux commonly reports 0777). Validate their resolved
|
|
137
|
+
// target instead; enforce write bits on the actual files/directories.
|
|
138
|
+
if (entry.isSymbolicLink()) {
|
|
139
|
+
assertContainedRealpath(root, target, code);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if ((entry.mode & 0o022) !== 0)
|
|
143
|
+
failure(code);
|
|
144
|
+
if (entry.isDirectory())
|
|
145
|
+
visit(target);
|
|
146
|
+
else if (!entry.isFile())
|
|
147
|
+
failure(code);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
visit(root);
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (error instanceof ManagedWrenRuntimeError)
|
|
154
|
+
throw error;
|
|
155
|
+
failure(code);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const runtimeTree = createRequire(import.meta.url)(path.join(packageRoot(), "managed-wren", "runtime-tree.cjs"));
|
|
159
|
+
export const managedWrenTreeDigest = runtimeTree.runtimeTreeDigest;
|
|
160
|
+
function assertWrenEntryPoint(launcher, venv) {
|
|
161
|
+
try {
|
|
162
|
+
const entry = runtimeTree.entryPoint(launcher, venv);
|
|
163
|
+
if (entry.name !== "python" || !entry.body.includes("from wren.cli import app"))
|
|
164
|
+
failure("codex_wren_launcher_mismatch");
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
failure("codex_wren_launcher_mismatch");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function closureDigest(manifest) { return sha256(manifest.wheels.map((wheel) => `${wheel.filename}\0${wheel.sha256}`).sort().join("\n")); }
|
|
171
|
+
function ownershipMarker(root) { return path.join(root, ".genbi-managed-wren.json"); }
|
|
172
|
+
function approvedManifestCache(root, digest) { return path.join(root, "attestations", `${digest}.json`); }
|
|
173
|
+
function sameReleaseIdentity(staged, approved) {
|
|
174
|
+
return JSON.stringify(staged.compatibility) === JSON.stringify(approved.compatibility) &&
|
|
175
|
+
JSON.stringify(staged.python) === JSON.stringify(approved.python) &&
|
|
176
|
+
staged.wheels.length === approved.wheels.length && staged.wheels.every((wheel, index) => JSON.stringify(wheel) === JSON.stringify(approved.wheels[index]));
|
|
177
|
+
}
|
|
178
|
+
function activatedManifest(base, root) {
|
|
179
|
+
if (base.activation === "approved")
|
|
180
|
+
return base;
|
|
181
|
+
if (!base.approvedManifest)
|
|
182
|
+
return failure("codex_wren_runtime_unprovisioned");
|
|
183
|
+
try {
|
|
184
|
+
const cache = approvedManifestCache(root, base.approvedManifest.sha256);
|
|
185
|
+
assertPrivateDirectory(path.dirname(cache));
|
|
186
|
+
const entry = lstatSync(cache);
|
|
187
|
+
if (!entry.isFile() || entry.isSymbolicLink() || (entry.mode & 0o777) !== 0o600)
|
|
188
|
+
return failure("codex_wren_manifest_invalid");
|
|
189
|
+
const bytes = readFileSync(cache);
|
|
190
|
+
if (sha256(bytes) !== base.approvedManifest.sha256)
|
|
191
|
+
return failure("codex_wren_manifest_invalid");
|
|
192
|
+
const parsed = managedWrenManifestSchema.safeParse(JSON.parse(bytes.toString("utf8")));
|
|
193
|
+
if (!parsed.success || parsed.data.activation !== "approved" || parsed.data.platform !== base.platform || !sameReleaseIdentity(base, parsed.data))
|
|
194
|
+
return failure("codex_wren_manifest_invalid");
|
|
195
|
+
return Object.freeze(parsed.data);
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
if (error instanceof ManagedWrenRuntimeError)
|
|
199
|
+
throw error;
|
|
200
|
+
return failure("codex_wren_runtime_unprovisioned");
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async function cacheApprovedManifest(base, root) {
|
|
204
|
+
if (base.activation === "approved")
|
|
205
|
+
return base;
|
|
206
|
+
// A byte-identical cached record is the first choice. Do not turn a local
|
|
207
|
+
// cache corruption into a network recovery path: the package anchor is the
|
|
208
|
+
// authority, and a bad cache is an integrity failure.
|
|
209
|
+
try {
|
|
210
|
+
return activatedManifest(base, root);
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
if (!(error instanceof ManagedWrenRuntimeError) || error.code !== "codex_wren_runtime_unprovisioned")
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
if (!base.approvedManifest)
|
|
217
|
+
return failure("codex_wren_runtime_unprovisioned");
|
|
218
|
+
const response = await fetch(base.approvedManifest.url);
|
|
219
|
+
if (!response.ok)
|
|
220
|
+
return failure("codex_wren_provision_failed");
|
|
221
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
222
|
+
if (sha256(bytes) !== base.approvedManifest.sha256)
|
|
223
|
+
return failure("codex_wren_manifest_invalid");
|
|
224
|
+
const cache = approvedManifestCache(root, base.approvedManifest.sha256);
|
|
225
|
+
mkdirSync(path.dirname(cache), { recursive: true, mode: 0o700 });
|
|
226
|
+
chmodSync(path.dirname(cache), 0o700);
|
|
227
|
+
assertPrivateDirectory(path.dirname(cache));
|
|
228
|
+
try {
|
|
229
|
+
writeFileSync(cache, bytes, { mode: 0o600, flag: "wx" });
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
// The provision lock prevents this in normal operation. Preserve this
|
|
233
|
+
// compare-and-accept branch for a cross-version contender that already
|
|
234
|
+
// populated the same exact anchor while this process was starting.
|
|
235
|
+
if (error?.code !== "EEXIST")
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
return activatedManifest(base, root);
|
|
239
|
+
}
|
|
240
|
+
function stagingMarker(root) { return path.join(root, ".genbi-managed-wren-staging.json"); }
|
|
241
|
+
function actualClosureDigest(root, manifest) {
|
|
242
|
+
const hashes = [];
|
|
243
|
+
for (const wheel of manifest.wheels) {
|
|
244
|
+
const target = regularFile(root, path.join(root, "wheels", wheel.filename), "codex_wren_closure_mismatch");
|
|
245
|
+
const actual = sha256(readFileSync(target));
|
|
246
|
+
if (actual !== wheel.sha256)
|
|
247
|
+
failure("codex_wren_closure_mismatch");
|
|
248
|
+
hashes.push(`${wheel.filename}\0${actual}`);
|
|
249
|
+
}
|
|
250
|
+
return sha256(hashes.sort().join("\n"));
|
|
251
|
+
}
|
|
252
|
+
function isRecognisedStaging(root, candidate, digest) {
|
|
253
|
+
try {
|
|
254
|
+
const target = path.join(root, candidate);
|
|
255
|
+
const entry = lstatSync(target);
|
|
256
|
+
if (!entry.isDirectory() || entry.isSymbolicLink() || !contained(root, target) || (entry.mode & 0o777) !== 0o700)
|
|
257
|
+
return false;
|
|
258
|
+
const marker = stagingMarker(target);
|
|
259
|
+
const markerEntry = lstatSync(marker);
|
|
260
|
+
if (!markerEntry.isFile() || markerEntry.isSymbolicLink() || (markerEntry.mode & 0o777) !== 0o600)
|
|
261
|
+
return false;
|
|
262
|
+
return JSON.parse(readFileSync(marker, "utf8")).manifestDigest === digest;
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function cleanupRecognisedStaging(root, digest) {
|
|
269
|
+
for (const candidate of readdirSync(root)) {
|
|
270
|
+
if (!candidate.startsWith(`.staging-${digest}-`) || !isRecognisedStaging(root, candidate, digest))
|
|
271
|
+
continue;
|
|
272
|
+
rmSync(path.join(root, candidate), { recursive: true, force: true });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function reclaimStaleLock(lock) {
|
|
276
|
+
try {
|
|
277
|
+
const entry = lstatSync(lock);
|
|
278
|
+
if (!entry.isFile() || entry.isSymbolicLink() || (entry.mode & 0o777) !== 0o600)
|
|
279
|
+
return false;
|
|
280
|
+
const record = JSON.parse(readFileSync(lock, "utf8"));
|
|
281
|
+
if (!Number.isInteger(record.pid) || record.pid <= 0)
|
|
282
|
+
return false;
|
|
283
|
+
try {
|
|
284
|
+
process.kill(record.pid, 0);
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
if (error?.code !== "ESRCH")
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
// O_EXCL elects one reclaimer. It then hard-links the *current* lock before
|
|
292
|
+
// unlinking its public name; another provisioner cannot create a new O_EXCL
|
|
293
|
+
// lock until that unlink, and a live replacement is never renamed/deleted.
|
|
294
|
+
const claim = `${lock}.reclaim`;
|
|
295
|
+
try {
|
|
296
|
+
const old = JSON.parse(readFileSync(claim, "utf8"));
|
|
297
|
+
process.kill(old.pid, 0);
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
if (error?.code === "ESRCH") {
|
|
302
|
+
const guard = `${claim}.recover-${process.pid}`;
|
|
303
|
+
try {
|
|
304
|
+
linkSync(claim, guard);
|
|
305
|
+
if (JSON.parse(readFileSync(guard, "utf8")).pid === JSON.parse(readFileSync(claim, "utf8")).pid)
|
|
306
|
+
unlinkSync(claim);
|
|
307
|
+
}
|
|
308
|
+
catch { }
|
|
309
|
+
finally {
|
|
310
|
+
try {
|
|
311
|
+
unlinkSync(guard);
|
|
312
|
+
}
|
|
313
|
+
catch { }
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
else if (error?.code !== "ENOENT")
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
let claimDescriptor;
|
|
320
|
+
const nonce = sha256(`${process.pid}\0${Date.now()}\0${Math.random()}`);
|
|
321
|
+
try {
|
|
322
|
+
claimDescriptor = openSync(claim, "wx", 0o600);
|
|
323
|
+
writeFileSync(claimDescriptor, JSON.stringify({ pid: process.pid, nonce }) + "\n");
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
if (error?.code === "EEXIST")
|
|
327
|
+
return false;
|
|
328
|
+
throw error;
|
|
329
|
+
}
|
|
330
|
+
const linked = `${claim}.lock`;
|
|
331
|
+
try {
|
|
332
|
+
linkSync(lock, linked);
|
|
333
|
+
const claimed = statSync(linked);
|
|
334
|
+
const current = statSync(lock);
|
|
335
|
+
const claimedRecord = JSON.parse(readFileSync(linked, "utf8"));
|
|
336
|
+
if (claimed.ino !== current.ino || claimedRecord.pid !== record.pid || JSON.parse(readFileSync(claim, "utf8")).nonce !== nonce)
|
|
337
|
+
return false;
|
|
338
|
+
unlinkSync(lock);
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
finally {
|
|
342
|
+
try {
|
|
343
|
+
unlinkSync(linked);
|
|
344
|
+
}
|
|
345
|
+
catch { }
|
|
346
|
+
try {
|
|
347
|
+
const guard = `${claim}.owner-${nonce}`;
|
|
348
|
+
linkSync(claim, guard);
|
|
349
|
+
if (JSON.parse(readFileSync(guard, "utf8")).nonce === nonce)
|
|
350
|
+
unlinkSync(claim);
|
|
351
|
+
unlinkSync(guard);
|
|
352
|
+
}
|
|
353
|
+
catch { }
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function assertSafeArchive(archive) {
|
|
361
|
+
const verifier = "import posixpath,sys,tarfile\ndef safe(value):\n return value not in ('.','') and not value.startswith('/') and value != '..' and not value.startswith('../')\nwith tarfile.open(sys.argv[1],'r:gz') as archive:\n for member in archive.getmembers():\n name=posixpath.normpath(member.name)\n if name == '.': continue\n if not safe(name): raise SystemExit('unsafe archive member')\n if member.issym(): target=posixpath.normpath(posixpath.join(posixpath.dirname(name),member.linkname))\n elif member.islnk(): target=posixpath.normpath(member.linkname)\n else: continue\n if not safe(target): raise SystemExit('unsafe archive link')\n";
|
|
362
|
+
execFileSync("/usr/bin/python3", ["-c", verifier, archive], { stdio: "ignore" });
|
|
363
|
+
}
|
|
364
|
+
function relocateVenvConfig(staging, destination, manifest) {
|
|
365
|
+
const config = path.join(destination, "venv", "pyvenv.cfg");
|
|
366
|
+
const entry = lstatSync(config);
|
|
367
|
+
if (!entry.isFile() || entry.isSymbolicLink())
|
|
368
|
+
failure("codex_wren_interpreter_mismatch");
|
|
369
|
+
const original = readFileSync(config, "utf8");
|
|
370
|
+
if (!/^home\s*=\s*.+$/m.test(original) || !/^executable\s*=\s*.+$/m.test(original))
|
|
371
|
+
failure("codex_wren_interpreter_mismatch");
|
|
372
|
+
const updated = original.split(staging).join(destination);
|
|
373
|
+
if (updated.includes(staging))
|
|
374
|
+
failure("codex_wren_interpreter_mismatch");
|
|
375
|
+
const expected = { home: path.dirname(path.join(destination, manifest.python.interpreterPath)), executable: path.join(destination, manifest.python.interpreterPath) };
|
|
376
|
+
for (const [key, target] of Object.entries(expected))
|
|
377
|
+
if (updated.match(new RegExp(`^${key}\\s*=\\s*(.+)$`, "m"))?.[1]?.trim() !== target)
|
|
378
|
+
failure("codex_wren_interpreter_mismatch");
|
|
379
|
+
writeFileSync(config, updated, { mode: 0o600 });
|
|
380
|
+
}
|
|
381
|
+
function assertRelocatedVenvConfig(destination, manifest) {
|
|
382
|
+
const config = path.join(destination, "venv", "pyvenv.cfg");
|
|
383
|
+
try {
|
|
384
|
+
const entry = lstatSync(config);
|
|
385
|
+
if (!entry.isFile() || entry.isSymbolicLink() || (entry.mode & 0o022) !== 0)
|
|
386
|
+
failure("codex_wren_interpreter_mismatch");
|
|
387
|
+
const text = readFileSync(config, "utf8");
|
|
388
|
+
const expected = { home: path.dirname(path.join(destination, manifest.python.interpreterPath)), executable: path.join(destination, manifest.python.interpreterPath) };
|
|
389
|
+
for (const [key, target] of Object.entries(expected))
|
|
390
|
+
if (text.match(new RegExp(`^${key}\\s*=\\s*(.+)$`, "m"))?.[1]?.trim() !== target)
|
|
391
|
+
failure("codex_wren_interpreter_mismatch");
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
if (error instanceof ManagedWrenRuntimeError)
|
|
395
|
+
throw error;
|
|
396
|
+
failure("codex_wren_interpreter_mismatch");
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Remove only records that were previously returned by this module and that
|
|
401
|
+
* the caller has established are neither active nor required for rollback.
|
|
402
|
+
*
|
|
403
|
+
* Generation selection belongs to the host/session lifecycle, not to a
|
|
404
|
+
* directory sweep: accepting arbitrary paths here would make cleanup an
|
|
405
|
+
* unsafe deletion primitive. A concurrent provisioner conservatively blocks
|
|
406
|
+
* cleanup altogether; it can retry after the writer releases its lock.
|
|
407
|
+
*/
|
|
408
|
+
export function cleanupManagedWrenGenerations(options) {
|
|
409
|
+
const root = options.runtimeRoot ?? defaultManagedWrenRoot();
|
|
410
|
+
try {
|
|
411
|
+
assertPrivateDirectory(root);
|
|
412
|
+
}
|
|
413
|
+
catch {
|
|
414
|
+
return [];
|
|
415
|
+
}
|
|
416
|
+
if (existsSync(path.join(root, ".provision.lock")))
|
|
417
|
+
return [];
|
|
418
|
+
const retained = new Set(options.retainManifestDigests);
|
|
419
|
+
const removed = [];
|
|
420
|
+
for (const record of options.candidates) {
|
|
421
|
+
if (retained.has(record.manifest_digest) || removed.includes(record.manifest_digest))
|
|
422
|
+
continue;
|
|
423
|
+
const generation = immutableDirectory(root, record.manifest_digest);
|
|
424
|
+
if (record.generation_root !== generation)
|
|
425
|
+
continue;
|
|
426
|
+
try {
|
|
427
|
+
assertPrivateDirectory(generation);
|
|
428
|
+
const markerPath = ownershipMarker(generation);
|
|
429
|
+
const markerEntry = lstatSync(markerPath);
|
|
430
|
+
if (!markerEntry.isFile() || markerEntry.isSymbolicLink() || (markerEntry.mode & 0o777) !== 0o600)
|
|
431
|
+
continue;
|
|
432
|
+
const marker = JSON.parse(readFileSync(markerPath, "utf8"));
|
|
433
|
+
if (marker.manifestDigest !== record.manifest_digest || marker.closureDigest !== record.closure_digest || marker.packageDigest !== record.package_digest)
|
|
434
|
+
continue;
|
|
435
|
+
// Re-check every executable/path the record would hand to a session
|
|
436
|
+
// before removing it. This makes a stale or caller-forged record a no-op.
|
|
437
|
+
if (!contained(generation, record.launcher) || !contained(generation, record.interpreter) ||
|
|
438
|
+
!contained(generation, record.venv_python) || !contained(generation, record.source_root))
|
|
439
|
+
continue;
|
|
440
|
+
regularExecutable(generation, record.launcher, "codex_wren_launcher_mismatch");
|
|
441
|
+
regularExecutable(generation, record.interpreter, "codex_wren_interpreter_mismatch");
|
|
442
|
+
regularExecutable(generation, record.venv_python, "codex_wren_interpreter_mismatch");
|
|
443
|
+
assertContainedRealpath(generation, record.source_root, "codex_wren_package_mismatch");
|
|
444
|
+
rmSync(generation, { recursive: true, force: false });
|
|
445
|
+
removed.push(record.manifest_digest);
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
// An unrecognised, changed, or concurrently replaced entry is retained.
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return Object.freeze(removed);
|
|
452
|
+
}
|
|
453
|
+
/** Revalidate a record before every native Codex launch. */
|
|
454
|
+
export function resolveManagedWrenRuntime(options = {}) {
|
|
455
|
+
const base = readManagedWrenManifest(options.packageRoot);
|
|
456
|
+
const root = options.runtimeRoot ?? defaultManagedWrenRoot();
|
|
457
|
+
const manifest = activatedManifest(base, root);
|
|
458
|
+
const digest = manifestDigest(manifest);
|
|
459
|
+
const generation = immutableDirectory(root, digest);
|
|
460
|
+
assertPrivateDirectory(root);
|
|
461
|
+
assertPrivateDirectory(generation);
|
|
462
|
+
assertSecureTree(generation, "codex_wren_closure_mismatch");
|
|
463
|
+
let marker;
|
|
464
|
+
try {
|
|
465
|
+
const markerPath = ownershipMarker(generation);
|
|
466
|
+
const entry = lstatSync(markerPath);
|
|
467
|
+
if (!entry.isFile() || entry.isSymbolicLink() || (entry.mode & 0o777) !== 0o600)
|
|
468
|
+
return failure("codex_wren_runtime_unprovisioned");
|
|
469
|
+
marker = JSON.parse(readFileSync(markerPath, "utf8"));
|
|
470
|
+
}
|
|
471
|
+
catch {
|
|
472
|
+
return failure("codex_wren_runtime_unprovisioned");
|
|
473
|
+
}
|
|
474
|
+
if (marker.manifestDigest !== digest)
|
|
475
|
+
return failure("codex_wren_closure_mismatch");
|
|
476
|
+
const archive = regularFile(generation, path.join(generation, manifest.runtime.pythonArchivePath), "codex_wren_interpreter_mismatch");
|
|
477
|
+
if (sha256(readFileSync(archive)) !== manifest.python.mirror.sha256)
|
|
478
|
+
return failure("codex_wren_interpreter_mismatch");
|
|
479
|
+
if (managedWrenTreeDigest(path.join(generation, "python")) !== manifest.runtime.pythonTreeSha256 || marker.pythonTreeDigest !== manifest.runtime.pythonTreeSha256)
|
|
480
|
+
return failure("codex_wren_interpreter_mismatch");
|
|
481
|
+
const interpreter = regularExecutable(generation, path.join(generation, manifest.runtime.venvInterpreterPath), "codex_wren_interpreter_mismatch");
|
|
482
|
+
assertRelocatedVenvConfig(generation, manifest);
|
|
483
|
+
const launcher = regularExecutable(generation, path.join(generation, manifest.runtime.launcherPath), "codex_wren_launcher_mismatch");
|
|
484
|
+
if (marker.interpreterDigest !== sha256(readFileSync(interpreter)))
|
|
485
|
+
return failure("codex_wren_interpreter_mismatch");
|
|
486
|
+
if (marker.launcherDigest !== sha256(readFileSync(launcher)))
|
|
487
|
+
return failure("codex_wren_launcher_mismatch");
|
|
488
|
+
assertWrenEntryPoint(launcher, path.join(generation, "venv"));
|
|
489
|
+
const packagePath = assertContainedRealpath(generation, path.join(generation, manifest.runtime.packagePath), "codex_wren_package_mismatch");
|
|
490
|
+
const sitePackages = assertContainedRealpath(generation, path.join(generation, manifest.runtime.sitePackagesPath), "codex_wren_package_mismatch");
|
|
491
|
+
let actualPackage;
|
|
492
|
+
try {
|
|
493
|
+
actualPackage = managedWrenTreeDigest(packagePath);
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
return failure("codex_wren_package_mismatch");
|
|
497
|
+
}
|
|
498
|
+
if (actualPackage !== manifest.runtime.packageTreeSha256 || marker.packageDigest !== actualPackage)
|
|
499
|
+
return failure("codex_wren_package_mismatch");
|
|
500
|
+
let actualSitePackages;
|
|
501
|
+
try {
|
|
502
|
+
actualSitePackages = managedWrenTreeDigest(sitePackages);
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
return failure("codex_wren_package_mismatch");
|
|
506
|
+
}
|
|
507
|
+
if (actualSitePackages !== manifest.runtime.sitePackagesTreeSha256 || marker.sitePackagesDigest !== actualSitePackages)
|
|
508
|
+
return failure("codex_wren_package_mismatch");
|
|
509
|
+
const actualClosure = actualClosureDigest(generation, manifest);
|
|
510
|
+
if (actualClosure !== manifest.runtime.closureSha256 || marker.closureDigest !== actualClosure)
|
|
511
|
+
return failure("codex_wren_closure_mismatch");
|
|
512
|
+
const python = regularExecutable(generation, path.join(generation, manifest.python.interpreterPath), "codex_wren_interpreter_mismatch");
|
|
513
|
+
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 });
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Readiness is deliberately resolve-only: checking any RuntimeHost backend
|
|
517
|
+
* must neither create the GenBI runtime root nor fetch/install managed bytes.
|
|
518
|
+
* An explicit future first-use path may call the exported provisioner below.
|
|
519
|
+
*/
|
|
520
|
+
export function managedWrenReadinessFailure(options = {}) {
|
|
521
|
+
try {
|
|
522
|
+
resolveManagedWrenRuntime(options);
|
|
523
|
+
return undefined;
|
|
524
|
+
}
|
|
525
|
+
catch (error) {
|
|
526
|
+
return error instanceof ManagedWrenRuntimeError ? error.code : "codex_wren_runtime_unprovisioned";
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Provision is intentionally unavailable for a staged manifest. A release can
|
|
531
|
+
* only turn this on after the protected licence approval job writes an approved
|
|
532
|
+
* manifest with release-attested digests.
|
|
533
|
+
*/
|
|
534
|
+
export async function provisionManagedWrenRuntime(options = {}) {
|
|
535
|
+
const root = options.runtimeRoot ?? defaultManagedWrenRoot();
|
|
536
|
+
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
537
|
+
chmodSync(root, 0o700);
|
|
538
|
+
assertPrivateDirectory(root);
|
|
539
|
+
const base = readManagedWrenManifest(options.packageRoot);
|
|
540
|
+
// An approved package can avoid the lock entirely. A staged package takes
|
|
541
|
+
// the same lock for cache population and provision, so concurrent first use
|
|
542
|
+
// cannot observe an absent/half-written approved-manifest cache.
|
|
543
|
+
if (base.activation === "approved") {
|
|
544
|
+
try {
|
|
545
|
+
return resolveManagedWrenRuntime({ ...(options.packageRoot ? { packageRoot: options.packageRoot } : {}), runtimeRoot: root });
|
|
546
|
+
}
|
|
547
|
+
catch (error) {
|
|
548
|
+
if (!(error instanceof ManagedWrenRuntimeError) || error.code !== "codex_wren_runtime_unprovisioned")
|
|
549
|
+
throw error;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
const lock = path.join(root, ".provision.lock");
|
|
553
|
+
let descriptor;
|
|
554
|
+
let provisionDigest;
|
|
555
|
+
try {
|
|
556
|
+
// A second first-use caller waits for the owner instead of falling through
|
|
557
|
+
// to a partial directory or declaring a separate runtime valid.
|
|
558
|
+
const deadline = Date.now() + 10_000;
|
|
559
|
+
while (descriptor === undefined && Date.now() < deadline) {
|
|
560
|
+
try {
|
|
561
|
+
descriptor = openSync(lock, "wx", 0o600);
|
|
562
|
+
writeFileSync(descriptor, JSON.stringify({ pid: process.pid, manifestDigest: base.activation === "approved" ? manifestDigest(base) : base.approvedManifest?.sha256 ?? "pending" }) + "\n");
|
|
563
|
+
}
|
|
564
|
+
catch (error) {
|
|
565
|
+
if (!(error && typeof error === "object" && "code" in error && error.code === "EEXIST"))
|
|
566
|
+
throw error;
|
|
567
|
+
if (reclaimStaleLock(lock))
|
|
568
|
+
continue;
|
|
569
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (descriptor === undefined)
|
|
573
|
+
return failure("codex_wren_provision_failed");
|
|
574
|
+
let manifest;
|
|
575
|
+
try {
|
|
576
|
+
manifest = await cacheApprovedManifest(base, root);
|
|
577
|
+
}
|
|
578
|
+
catch (error) {
|
|
579
|
+
if (error instanceof ManagedWrenRuntimeError)
|
|
580
|
+
throw error;
|
|
581
|
+
return failure("codex_wren_provision_failed");
|
|
582
|
+
}
|
|
583
|
+
const digest = manifestDigest(manifest);
|
|
584
|
+
provisionDigest = digest;
|
|
585
|
+
const destination = immutableDirectory(root, digest);
|
|
586
|
+
// A waiter must re-check after it owns the lock: the original owner may
|
|
587
|
+
// have completed while this caller was waiting.
|
|
588
|
+
try {
|
|
589
|
+
return resolveManagedWrenRuntime({ ...(options.packageRoot ? { packageRoot: options.packageRoot } : {}), runtimeRoot: root });
|
|
590
|
+
}
|
|
591
|
+
catch (error) {
|
|
592
|
+
if (!(error instanceof ManagedWrenRuntimeError) || error.code !== "codex_wren_runtime_unprovisioned")
|
|
593
|
+
throw error;
|
|
594
|
+
}
|
|
595
|
+
// Only a marker written by this provisioner is eligible for interrupted-run
|
|
596
|
+
// recovery, after the exclusive lock prevents a live writer race.
|
|
597
|
+
cleanupRecognisedStaging(root, digest);
|
|
598
|
+
const staging = path.join(root, `.staging-${digest}-${process.pid}`);
|
|
599
|
+
mkdirSync(staging, { mode: 0o700 });
|
|
600
|
+
chmodSync(staging, 0o700);
|
|
601
|
+
writeFileSync(stagingMarker(staging), JSON.stringify({ manifestDigest: digest }) + "\n", { mode: 0o600, flag: "wx" });
|
|
602
|
+
// The release workflow makes these assets available as a closed mirror.
|
|
603
|
+
// Fetching any other URL, index, or dependency resolver is intentionally absent.
|
|
604
|
+
const archive = path.join(staging, manifest.runtime.pythonArchivePath);
|
|
605
|
+
const download = async (url, target, expected) => { const response = await fetch(url); if (!response.ok)
|
|
606
|
+
throw new Error("download"); const bytes = Buffer.from(await response.arrayBuffer()); if (sha256(bytes) !== expected)
|
|
607
|
+
throw new Error("digest"); writeFileSync(target, bytes, { mode: 0o600, flag: "wx" }); };
|
|
608
|
+
await download(manifest.python.mirror.url, archive, manifest.python.mirror.sha256);
|
|
609
|
+
assertSafeArchive(archive);
|
|
610
|
+
execFileSync("/usr/bin/tar", ["-xpzf", archive, "-C", staging], { stdio: "ignore" });
|
|
611
|
+
chmodSync(staging, 0o700);
|
|
612
|
+
runtimeTree.preparePythonTree(path.join(staging, "python"));
|
|
613
|
+
assertSecureTree(staging, "codex_wren_interpreter_mismatch");
|
|
614
|
+
const python = path.join(staging, manifest.python.interpreterPath);
|
|
615
|
+
regularExecutable(staging, python, "codex_wren_interpreter_mismatch");
|
|
616
|
+
const wheels = path.join(staging, "wheels");
|
|
617
|
+
mkdirSync(wheels, { mode: 0o700 });
|
|
618
|
+
for (const wheel of manifest.wheels)
|
|
619
|
+
await download(wheel.url, path.join(wheels, wheel.filename), wheel.sha256);
|
|
620
|
+
const requirements = manifest.wheels.map((wheel) => `${wheel.distribution}==${wheel.version} --hash=sha256:${wheel.sha256}`).join("\n") + "\n";
|
|
621
|
+
writeFileSync(path.join(staging, "requirements.txt"), requirements, { mode: 0o600, flag: "wx" });
|
|
622
|
+
const venv = path.join(staging, "venv");
|
|
623
|
+
execFileSync(python, ["-m", "venv", "--copies", venv], { stdio: "ignore", env: { PATH: path.dirname(python), HOME: staging, PYTHONNOUSERSITE: "1" } });
|
|
624
|
+
const venvInterpreter = path.join(venv, "bin", "python");
|
|
625
|
+
// PBS may still emit an absolute staging symlink despite --copies. Replace
|
|
626
|
+
// only that resolved interpreter with a private copy before promotion.
|
|
627
|
+
if (lstatSync(venvInterpreter).isSymbolicLink()) {
|
|
628
|
+
const resolved = realpathSync(venvInterpreter);
|
|
629
|
+
if (!contained(staging, resolved))
|
|
630
|
+
return failure("codex_wren_interpreter_mismatch");
|
|
631
|
+
unlinkSync(venvInterpreter);
|
|
632
|
+
writeFileSync(venvInterpreter, readFileSync(resolved), { mode: 0o700 });
|
|
633
|
+
}
|
|
634
|
+
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" } });
|
|
635
|
+
const packagePath = path.join(staging, manifest.runtime.packagePath);
|
|
636
|
+
const sitePackagesPath = path.join(staging, manifest.runtime.sitePackagesPath);
|
|
637
|
+
const pythonTreeDigest = managedWrenTreeDigest(path.join(staging, "python"));
|
|
638
|
+
const packageDigest = managedWrenTreeDigest(packagePath);
|
|
639
|
+
const sitePackagesDigest = managedWrenTreeDigest(sitePackagesPath);
|
|
640
|
+
const closure = actualClosureDigest(staging, manifest);
|
|
641
|
+
if (pythonTreeDigest !== manifest.runtime.pythonTreeSha256 || packageDigest !== manifest.runtime.packageTreeSha256 || sitePackagesDigest !== manifest.runtime.sitePackagesTreeSha256 || closure !== manifest.runtime.closureSha256)
|
|
642
|
+
throw new Error("attestation");
|
|
643
|
+
const stagedLauncher = path.join(staging, manifest.runtime.launcherPath);
|
|
644
|
+
assertWrenEntryPoint(stagedLauncher, venv);
|
|
645
|
+
renameSync(staging, destination);
|
|
646
|
+
relocateVenvConfig(staging, destination, manifest);
|
|
647
|
+
runtimeTree.relocateEntryPoints(path.join(destination, manifest.runtime.sitePackagesPath), venv, "wren");
|
|
648
|
+
const finalInterpreter = path.join(destination, manifest.runtime.venvInterpreterPath);
|
|
649
|
+
const finalLauncher = path.join(destination, manifest.runtime.launcherPath);
|
|
650
|
+
rmSync(stagingMarker(destination), { force: true });
|
|
651
|
+
writeFileSync(ownershipMarker(destination), JSON.stringify({ manifestDigest: digest, pythonTreeDigest, packageDigest, sitePackagesDigest, closureDigest: closure, interpreterDigest: sha256(readFileSync(finalInterpreter)), launcherDigest: sha256(readFileSync(finalLauncher)) }) + "\n", { mode: 0o600, flag: "wx" });
|
|
652
|
+
return resolveManagedWrenRuntime({ ...(options.packageRoot ? { packageRoot: options.packageRoot } : {}), runtimeRoot: root });
|
|
653
|
+
}
|
|
654
|
+
catch (error) {
|
|
655
|
+
if (descriptor !== undefined && provisionDigest)
|
|
656
|
+
cleanupRecognisedStaging(root, provisionDigest);
|
|
657
|
+
if (error instanceof ManagedWrenRuntimeError)
|
|
658
|
+
throw error;
|
|
659
|
+
return failure("codex_wren_provision_failed");
|
|
660
|
+
}
|
|
661
|
+
finally {
|
|
662
|
+
if (descriptor !== undefined) {
|
|
663
|
+
try {
|
|
664
|
+
rmSync(lock, { force: true });
|
|
665
|
+
}
|
|
666
|
+
catch { /* next run reports unavailable */ }
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
export function managedWrenClosureDigest(manifest) { return closureDigest(manifest); }
|