fix(genbi): fail closed before native Codex launch

This commit is contained in:
Jax Liu
2026-09-04 23:31:01 +08:00
parent f53d757482
commit 35411ed4ae
7 changed files with 274 additions and 48 deletions
+34 -8
View File
@@ -26,6 +26,7 @@ import type { CodexWrenHome } from "./native-wren-home.js";
import { runtimeSettingsCorrection } from "./runtime-binding.js";
import { sealNativeClaudeResumeHandle, sealNativeResumeHandle, unsealNativeResumeHandle } from "./native-session-resume.js";
import { RuntimeHost } from "./runtime-host/local.js";
import { runtimeNotReady } from "./runtime-host/policy.js";
import type { RuntimeReadiness } from "./runtime-host/types.js";
export const NATIVE_VENDORS = ["claude", "codex"] as const;
@@ -52,6 +53,7 @@ const START_SEPARATE_REPLAY_LIMIT = 128;
const CODEX_NATIVE_PERMISSION_HEADER = "[permissions.warble_native_wren.filesystem]";
const PROVIDER_SESSION_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const CODEX_RESUME_HANDLE_FILE = path.join(".warble", "codex-thread-id");
const CODEX_WREN_RUNTIME_UNPROVISIONED = runtimeNotReady("codex-app-server", "unprovisioned", "codex_wren_runtime_unprovisioned");
type NativeLaunchPhase =
| "workspace"
@@ -319,6 +321,13 @@ export interface NativeSessionServiceOptions {
readonly warbleBin: string;
/** Absolute server-configured Wren shim for native Codex sessions; never a request input. */
readonly wrenShim?: string;
/**
* Server-only managed-runtime record resolver. Phase 2A intentionally has
* no production implementation: absence is the fail-closed launch fence.
* A later provisioner must revalidate its immutable record in this callback;
* the editable `wrenShim` resolver is never a launch fallback.
*/
readonly resolveManagedCodexWrenRuntime?: () => NativeWrenRuntime;
/** Fixed native child HOME captured by BFF composition, never request input. */
readonly nativeHome?: string;
/** Decision-17 profile source selected by server setup mode. */
@@ -1378,6 +1387,14 @@ export class NativeSessionService {
const selectedRuntimeUnavailable = runtimeHostReadiness.selectedReadiness.state === "ready"
? undefined
: runtimeHostReadiness.selectedReadiness.message;
const codexBackendReadiness = runtimeHostReadiness.backends["codex-app-server"];
const codexWrenRuntimeUnavailable = this.legacyFixtureMode()
? undefined
: this.options.resolveManagedCodexWrenRuntime === undefined
? CODEX_WREN_RUNTIME_UNPROVISIONED.message
: codexBackendReadiness.state === "ready"
? undefined
: codexBackendReadiness.message;
const mcpReadiness = this.options.artifactService?.readiness();
const runtime = this.options.store.getNativeRuntimeBinding();
const runtimeCorrection = this.options.store.hasExplicitRuntimeSettings()
@@ -1419,7 +1436,8 @@ export class NativeSessionService {
: mcpReadiness && !mcpReadiness.available
? mcpReadiness.reason
: undefined;
const reason = runtimeCorrection ?? selectedRuntimeUnavailable ?? sharedReason;
const vendorRuntimeUnavailable = dispatch?.provider === "codex" ? codexWrenRuntimeUnavailable : undefined;
const reason = runtimeCorrection ?? selectedRuntimeUnavailable ?? vendorRuntimeUnavailable ?? sharedReason;
const executable = dispatch?.provider === "claude" ? "claude" : "codex";
const unavailable = reason ?? (!hostAvailable
? "native terminal host cannot spawn local processes on this machine"
@@ -1431,7 +1449,9 @@ export class NativeSessionService {
// one is unusable and why instead of only that "the producer" is.
if (!legacyFixture && producer) for (const vendor of NATIVE_VENDORS) {
const probe = producer.vendors[vendor];
vendorReadiness[vendor] = probe?.available === false
vendorReadiness[vendor] = vendor === "codex" && codexWrenRuntimeUnavailable
? { available: false, reason: codexWrenRuntimeUnavailable }
: probe?.available === false
? { available: false, reason: probe.reason ?? "native session producer is incompatible" }
: !executableAvailable(vendor)
? { available: false, reason: `the ${vendor === "claude" ? "claude" : "codex"} interactive CLI is not available on this machine` }
@@ -1496,6 +1516,17 @@ export class NativeSessionService {
: capturedRuntime;
const dispatchDefinition = dispatchForPurpose(purpose, legacyRuntime);
if (!dispatchDefinition) throw new InteractiveLaunchError("native sessions require a saved Runtime & authentication binding");
let managedCodexWrenRuntime: NativeWrenRuntime | undefined;
if (dispatchDefinition.provider === "codex" && !this.legacyFixtureMode()) {
const codexReadiness = runtimeReadiness.backends["codex-app-server"];
if (codexReadiness.state !== "ready") throw new InteractiveLaunchError(codexReadiness.message);
if (!this.options.resolveManagedCodexWrenRuntime) throw new InteractiveLaunchError(CODEX_WREN_RUNTIME_UNPROVISIONED.message);
try {
managedCodexWrenRuntime = this.options.resolveManagedCodexWrenRuntime();
} catch {
throw new InteractiveLaunchError(CODEX_WREN_RUNTIME_UNPROVISIONED.message);
}
}
if (purpose !== "setup" && !binding) throw new InteractiveLaunchError("native session requires a current bound project");
const configuredCwd = binding?.path ?? this.options.workspaceRoot;
const configuredIr = this.options.irPaths[purpose];
@@ -1564,12 +1595,7 @@ export class NativeSessionService {
const mcp = artifactService?.issue(row, binding);
if (mcp) this.artifactCredentials.set(id, mcp.credential);
let wrenRuntime: NativeWrenRuntime | undefined;
try {
wrenRuntime = dispatchDefinition.provider === "codex" && !this.legacyFixtureMode() ? resolveNativeWrenRuntime(this.wrenShim) : undefined;
} catch (error) {
if (error instanceof NativeWrenRuntimeError) throw new InteractiveLaunchError("native Wren runtime is unavailable");
throw error;
}
wrenRuntime = managedCodexWrenRuntime;
const vendorExecutable = this.vendorExecutables[dispatchDefinition.provider];
if (!vendorExecutable) throw new InteractiveLaunchError(`the ${dispatchDefinition.provider} interactive CLI is not available on this machine`);
const producerExecutable = producer?.producer
+98 -21
View File
@@ -1,5 +1,6 @@
import { execFileSync } from "node:child_process";
import { chmodSync, existsSync, lstatSync, mkdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
import { createHash } from "node:crypto";
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
import path from "node:path";
import type { NativeWrenRuntime } from "./native-wren-runtime.js";
import { buildNativeChildEnvironment, nativeProcessEnvironment } from "./native-runtime-spec.js";
@@ -103,10 +104,95 @@ export interface CodexWrenHome {
readonly cleanup?: () => void;
}
function validatePrivateFile(file: string): void {
const metadata = lstatSync(file);
if (!metadata.isFile() || metadata.isSymbolicLink() || realpathSync(file) !== file || (metadata.mode & 0o777) !== 0o600) {
throw new InteractiveLaunchError("native Wren session home is unavailable");
interface PrivatePathIdentity {
readonly canonical: string;
readonly dev: number;
readonly ino: number;
readonly mode: number;
}
interface PrivateFileIdentity extends PrivatePathIdentity {
readonly size: number;
readonly mtimeMs: number;
readonly ctimeMs: number;
readonly digest: string;
}
interface WrenHomeIdentity {
readonly directory: PrivatePathIdentity;
readonly files: Readonly<Record<string, PrivateFileIdentity>>;
}
function unavailable(): never {
throw new InteractiveLaunchError("native Wren session home is unavailable");
}
function privateDirectoryIdentity(directory: string): PrivatePathIdentity {
try {
const metadata = lstatSync(directory);
const canonical = realpathSync(directory);
if (!metadata.isDirectory() || metadata.isSymbolicLink() || canonical !== directory || (metadata.mode & 0o777) !== 0o700) return unavailable();
return { canonical, dev: metadata.dev, ino: metadata.ino, mode: metadata.mode & 0o777 };
} catch (error) {
if (error instanceof InteractiveLaunchError) throw error;
return unavailable();
}
}
function privateFileIdentity(file: string): PrivateFileIdentity {
try {
const metadata = lstatSync(file);
const canonical = realpathSync(file);
if (!metadata.isFile() || metadata.isSymbolicLink() || canonical !== file || (metadata.mode & 0o777) !== 0o600) return unavailable();
return {
canonical,
dev: metadata.dev,
ino: metadata.ino,
mode: metadata.mode & 0o777,
size: metadata.size,
mtimeMs: metadata.mtimeMs,
ctimeMs: metadata.ctimeMs,
digest: createHash("sha256").update(readFileSync(file)).digest("hex"),
};
} catch (error) {
if (error instanceof InteractiveLaunchError) throw error;
return unavailable();
}
}
function samePathIdentity(left: PrivatePathIdentity, right: PrivatePathIdentity): boolean {
return left.canonical === right.canonical && left.dev === right.dev && left.ino === right.ino && left.mode === right.mode;
}
function sameFileIdentity(left: PrivateFileIdentity, right: PrivateFileIdentity): boolean {
return samePathIdentity(left, right)
&& left.size === right.size
&& left.mtimeMs === right.mtimeMs
&& left.ctimeMs === right.ctimeMs
&& left.digest === right.digest;
}
function captureWrenHome(destination: string, fileNames: readonly string[]): WrenHomeIdentity {
try {
const actualNames = readdirSync(destination).sort();
const expectedNames = [...fileNames].sort();
if (JSON.stringify(actualNames) !== JSON.stringify(expectedNames)) return unavailable();
return {
directory: privateDirectoryIdentity(destination),
files: Object.freeze(Object.fromEntries(expectedNames.map((name) => [name, privateFileIdentity(path.join(destination, name))]))),
};
} catch (error) {
if (error instanceof InteractiveLaunchError) throw error;
return unavailable();
}
}
function assertWrenHome(destination: string, expected: WrenHomeIdentity): void {
const current = captureWrenHome(destination, Object.keys(expected.files));
if (!samePathIdentity(current.directory, expected.directory)) return unavailable();
for (const [name, identity] of Object.entries(expected.files)) {
const observed = current.files[name];
if (!observed || !sameFileIdentity(observed, identity)) return unavailable();
}
}
@@ -117,7 +203,7 @@ function retirementMarker(cwd: string): string {
function retire(destination: string, marker: string): void {
rmSync(destination, { recursive: true, force: true });
writeFileSync(marker, "retired\n", { encoding: "utf8", mode: 0o600, flag: "wx" });
validatePrivateFile(marker);
privateFileIdentity(marker);
}
/** Setup has no bound profile yet, but still receives a fresh private WREN_HOME. */
@@ -129,17 +215,15 @@ export function createEmptyCodexWrenHome(cwd: string): CodexWrenHome {
if (existsSync(marker)) throw new Error("retired");
mkdirSync(destination, { mode: 0o700 });
chmodSync(destination, 0o700);
const metadata = lstatSync(destination);
if (!metadata.isDirectory() || metadata.isSymbolicLink() || realpathSync(destination) !== destination || (metadata.mode & 0o777) !== 0o700) {
throw new Error("invalid directory");
}
const identity = captureWrenHome(destination, []);
active = true;
return Object.freeze({
home: destination,
dataRoots: Object.freeze([]),
active: () => active,
assertActive: () => {
if (!active || !existsSync(destination)) throw new InteractiveLaunchError("native Wren session home is unavailable");
if (!active) return unavailable();
assertWrenHome(destination, identity);
},
cleanup: () => {
if (!active) return;
@@ -180,19 +264,12 @@ export function materializeCodexWrenHome(input: {
stdio: ["ignore", "pipe", "ignore"],
timeout: 10_000,
});
const canonical = realpathSync(destination);
const metadata = lstatSync(destination);
if (canonical !== destination || !metadata.isDirectory() || metadata.isSymbolicLink() || (metadata.mode & 0o777) !== 0o700) {
throw new InteractiveLaunchError("native Wren session home is unavailable");
}
validatePrivateFile(path.join(destination, "profiles.yml"));
const envFile = path.join(destination, ".env");
if (existsSync(envFile)) validatePrivateFile(envFile);
const identity = captureWrenHome(destination, ["profiles.yml", ...(existsSync(envFile) ? [".env"] : [])]);
active = true;
const assertActive = () => {
if (!active || !existsSync(destination) || !statSync(destination).isDirectory()) {
throw new InteractiveLaunchError("native Wren session home is unavailable");
}
if (!active) return unavailable();
assertWrenHome(destination, identity);
};
return Object.freeze({
home: destination,
+4 -1
View File
@@ -30,7 +30,10 @@ export interface RuntimeHostOptions {
}
const fixedCodexProbe = (): RuntimeBackendProbeResult<"codex-app-server"> => ({
readiness: runtimeNotReady("codex-app-server", "unprovisioned", "codex_app_server_unprovisioned"),
// Phase 2A has no managed-runtime record/provisioner yet. This is the first
// blocking link for every Codex launch, even while the development-local
// PTY host itself is healthy.
readiness: runtimeNotReady("codex-app-server", "unprovisioned", "codex_wren_runtime_unprovisioned"),
diagnostic: { phase: "provisioning" },
});
@@ -12,8 +12,11 @@ import { initializeNativeSessionStateBase } from "../server/native-session-works
import { NativeArtifactService } from "../server/native-artifacts.js";
import { NativeSessionService, probeNativeSessionProducer } from "../server/native-sessions.js";
import { RuntimeHost } from "../server/runtime-host/local.js";
import { runtimeReady } from "../server/runtime-host/policy.js";
import type { PtyFactory } from "../server/interactive-terminal.js";
import { attestNativeExecutable } from "../server/native-runtime-spec.js";
import { createEmptyCodexWrenHome } from "../server/native-wren-home.js";
import { resolveNativeWrenRuntime } from "../server/native-wren-runtime.js";
const dirs: string[] = [];
const HELP = ["dispatch", "--target", "--purpose", "--native-scope", "--native-mcp", "--out"].join("\n");
@@ -103,7 +106,7 @@ function irPaths(dir: string) {
return { analysis: path.join(dir, "analysis.json"), setup: path.join(dir, "setup.json"), context_enrichment: path.join(dir, "context.json") } as const;
}
function productionService(dir: string, producer: string, wrenShim: string, terminalManager?: () => Promise<InteractiveTerminalManager>, subscriptionProvider: "claude" | "codex" = "codex", runtimeHost?: RuntimeHost) {
function productionService(dir: string, producer: string, wrenShim: string, terminalManager?: () => Promise<InteractiveTerminalManager>, subscriptionProvider: "claude" | "codex" = "codex", runtimeHost?: RuntimeHost, managedRuntimeAvailable = true) {
const store = new Store(":memory:");
store.setRuntimeSettings({ ...store.getRuntimeSettings(), subscriptionProvider, subscriptionDriverModel: "driver", tierModels: subscriptionProvider === "claude" ? [{ tier: "cheap", model: "haiku" }, { tier: "strong", model: "sonnet" }] : [{ tier: "cheap", model: "cheap" }, { tier: "strong", model: "strong" }] });
const binding = store.activateEnrichmentBinding(resolveEnrichmentBinding(dir));
@@ -126,8 +129,26 @@ function productionService(dir: string, producer: string, wrenShim: string, term
}
};
visitCodex(codexRoot);
let managedRuntime;
if (managedRuntimeAvailable) {
try { managedRuntime = resolveNativeWrenRuntime(wrenShim); } catch { /* absent/invalid remains unprovisioned */ }
}
const effectiveRuntimeHost = runtimeHost ?? new RuntimeHost({
selected: "local",
deployment: "development",
localAvailable: () => true,
...(managedRuntime ? {
vendorProbes: {
"codex-app-server": async () => ({
readiness: runtimeReady("fixture-managed", ["pty"]),
diagnostic: { phase: "provisioning" as const },
}),
},
} : {}),
});
return {
store,
state,
service: new NativeSessionService({
store,
terminalManager: terminalManager ?? (async () => new InteractiveTerminalManager(pty)),
@@ -137,20 +158,16 @@ function productionService(dir: string, producer: string, wrenShim: string, term
irPaths: irPaths(dir),
warbleBin: producer,
wrenShim,
runtimeHost: effectiveRuntimeHost,
...(managedRuntime ? { resolveManagedCodexWrenRuntime: () => managedRuntime } : {}),
terminalHostAvailable: async () => true,
vendorExecutables: {
claude: attestNativeExecutable("vendor", process.execPath),
codex: attestNativeExecutable("vendor", codexBin),
},
...(runtimeHost ? { runtimeHost } : {}),
executableAvailable: () => true,
artifactService: artifacts,
prepareCodexWrenHome: ({ cwd }) => {
const home = path.join(cwd, ".wren");
mkdirSync(home);
writeFileSync(path.join(home, "profiles.yml"), "active: fixture\nprofiles:\n fixture:\n datasource: duckdb\n");
return { home, dataRoots: [] };
},
prepareCodexWrenHome: ({ cwd }) => createEmptyCodexWrenHome(cwd),
}),
};
}
@@ -183,6 +200,58 @@ describe("native producer compatibility preflight", () => {
store.close();
});
it("keeps production Codex unprovisioned when the local PTY host is ready but no managed Wren record exists", async () => {
const dir = fixtureDir();
const wrenShim = runtimeFixture(dir);
const terminalManager = vi.fn(async () => new InteractiveTerminalManager({ spawn: vi.fn() }));
const { service, store } = productionService(dir, fakeProducer(dir, "compatible"), wrenShim, terminalManager, "codex", undefined, false);
const readiness = await service.readiness();
expect(readiness.runtimeHost).toMatchObject({
selected: "local",
selectedReadiness: { state: "ready" },
backends: { "codex-app-server": { state: "unprovisioned", code: "codex_wren_runtime_unprovisioned" } },
});
expect(readiness.purposes.analysis).toMatchObject({
available: false,
reason: "The managed Wren runtime has not been provisioned.",
vendors: {
codex: { available: false, reason: "The managed Wren runtime has not been provisioned." },
claude: { available: true },
},
});
await expect(service.openOrCreate({ purpose: "analysis" })).rejects.toThrow("The managed Wren runtime has not been provisioned.");
expect(terminalManager).not.toHaveBeenCalled();
expect(store.listNativeSessions()).toEqual([]);
store.close();
});
it("rejects a Codex session home replaced after materialization and before PTY start", async () => {
const dir = fixtureDir();
const wrenShim = runtimeFixture(dir);
const replacement = mkdtempSync(path.join(tmpdir(), "genbi-native-wren-home-replacement-"));
dirs.push(replacement);
const spawn = vi.fn();
let state: ReturnType<typeof initializeNativeSessionStateBase>;
const terminalManager = vi.fn(async () => {
const sessionIds = readdirSync(path.join(state.root, "native"));
expect(sessionIds).toHaveLength(1);
const sessionHome = path.join(state.root, "native", sessionIds[0]!, ".wren");
rmSync(sessionHome, { recursive: true });
symlinkSync(replacement, sessionHome, "dir");
return new InteractiveTerminalManager({ spawn });
});
const fixture = productionService(dir, fakeProducer(dir, "compatible"), wrenShim, terminalManager);
state = fixture.state;
await expect(fixture.service.openOrCreate({ purpose: "analysis" })).rejects.toThrow("native Wren session home is unavailable");
expect(terminalManager).toHaveBeenCalledOnce();
expect(spawn).not.toHaveBeenCalled();
expect(fixture.store.listNativeSessions()).toHaveLength(1);
expect(fixture.store.listNativeSessions()[0]).toMatchObject({ status: "failed", failure: "native Wren session home is unavailable" });
fixture.store.close();
});
it("keeps the preflight-resolved executable when the configured symlink retargets before launch", async () => {
const dir = fixtureDir();
const wrenShim = runtimeFixture(dir);
@@ -281,12 +350,12 @@ describe("native producer compatibility preflight", () => {
purposes: {
analysis: {
available: false,
reason: expect.stringContaining("native Wren runtime is unavailable"),
reason: "The managed Wren runtime has not been provisioned.",
producer: { available: false, category: "native_session_producer_incompatible", diagnostic: expect.stringContaining("wren_runtime_unavailable") },
},
},
});
await expect(service.openOrCreate({ purpose: "analysis" })).rejects.toThrow("native Wren runtime is unavailable");
await expect(service.openOrCreate({ purpose: "analysis" })).rejects.toThrow("The managed Wren runtime has not been provisioned.");
expect(store.listNativeSessions()).toEqual([]);
store.close();
});
@@ -309,9 +378,11 @@ describe("native producer compatibility preflight", () => {
expect(readiness.purposes.analysis.available).toBe(true);
expect(readiness.purposes.analysis.reason).toBeUndefined();
expect(readiness.purposes.analysis.producer).toMatchObject({ available: true });
// The unusable vendor is still reported, with its own cause, rather than
// being hidden because the configured one happens to work.
expect(readiness.purposes.analysis.vendors.codex).toMatchObject({ available: false, reason: expect.stringContaining("native Wren runtime is unavailable") });
// The unusable vendor is still reported through the authoritative managed
// record fence rather than being hidden because the configured one happens
// to work. The lower deterministic probe above retains the exact broken
// editable-chain diagnostic without making it a launch fallback.
expect(readiness.purposes.analysis.vendors.codex).toMatchObject({ available: false, reason: "The managed Wren runtime has not been provisioned." });
expect(readiness.purposes.analysis.vendors.claude).toMatchObject({ available: true });
store.close();
});
+22 -2
View File
@@ -13,11 +13,29 @@ import type { NativeSessionServiceOptions } from "../server/native-sessions.js";
import { NativeArtifactService, NATIVE_MCP_CREDENTIAL_ENV_VAR, NATIVE_MCP_TOOL_NAME } from "../server/native-artifacts.js";
import { InteractiveTerminalManager } from "../server/interactive-terminal.js";
import { RuntimeHost } from "../server/runtime-host/local.js";
import { runtimeReady } from "../server/runtime-host/policy.js";
import type { PtyFactory } from "../server/interactive-terminal.js";
import { attestNativeExecutable, type NativeChildEnvironment } from "../server/native-runtime-spec.js";
import { resolveNativeWrenRuntime } from "../server/native-wren-runtime.js";
const dirs: string[] = [];
const NATIVE_MCP_URL = "http://127.0.0.1:4787/api/native-sessions/mcp";
function managedCodexRuntimeOptions() {
return {
runtimeHost: new RuntimeHost({
selected: "local",
deployment: "development",
localAvailable: () => true,
vendorProbes: {
"codex-app-server": async () => ({
readiness: runtimeReady("fixture-managed", ["pty"]),
diagnostic: { phase: "provisioning" as const },
}),
},
}),
resolveManagedCodexWrenRuntime: () => resolveNativeWrenRuntime(),
} as const;
}
function welcomeFor(purpose: "analysis" | "setup" | "context_enrichment") {
return purpose === "setup"
? "Help me set up this GenBI project. Start by explaining the next setup step and ask what data source I want to connect."
@@ -1444,6 +1462,7 @@ describe("native session persistence", () => {
workspaceRoot: undefined, materializationState: successState, irPaths: { analysis: path.join(success.dir, "analysis.json"), setup: undefined, context_enrichment: undefined },
warbleBin: fakeV4Producer(success.dir), artifactService: successArtifacts,
vendorExecutables: { [vendor]: attestNativeExecutable("vendor", process.execPath) },
...(vendor === "codex" ? managedCodexRuntimeOptions() : {}),
prepareCodexWrenHome: ({ cwd }) => {
const home = path.join(cwd, ".wren");
mkdirSync(home);
@@ -1479,6 +1498,7 @@ describe("native session persistence", () => {
workspaceRoot: undefined, materializationState: failureState, irPaths: { analysis: undefined, setup: undefined, context_enrichment: path.join(failure.dir, "enrich.json") },
warbleBin: fakeV4Producer(failure.dir), artifactService: failureArtifacts,
vendorExecutables: { [vendor]: attestNativeExecutable("vendor", process.execPath) },
...(vendor === "codex" ? managedCodexRuntimeOptions() : {}),
});
await expect(failureService.create({ purpose: "context_enrichment", vendor })).rejects.toThrow(/materialization failed/);
const failedDescriptor = failedIssue.mock.results[0]?.value as { credential: string };
@@ -1702,7 +1722,7 @@ describe("native session persistence", () => {
],
[
new RuntimeHost({ selected: "codex-app-server", deployment: "development", localAvailable: () => true }),
"The Codex app-server runtime has not been provisioned by this GenBI release.",
"The managed Wren runtime has not been provisioned.",
],
[
new RuntimeHost({ selected: "claude-sandbox-runtime", deployment: "development", localAvailable: () => true }),
@@ -1757,7 +1777,7 @@ describe("native session persistence", () => {
const state = materializationState();
writeFileSync(path.join(dir, "missing-ir.json"), "{}");
const store = new Store(":memory:");
const service = new NativeSessionService({ store, terminalManager: async () => { throw new Error("not reached"); }, getBinding: () => binding, workspaceRoot: undefined, materializationState: state, irPaths: { analysis: path.join(dir, "missing-ir.json"), setup: undefined, context_enrichment: undefined }, warbleBin: path.join(dir, "missing-warble") });
const service = new NativeSessionService({ store, terminalManager: async () => { throw new Error("not reached"); }, getBinding: () => binding, workspaceRoot: undefined, materializationState: state, irPaths: { analysis: path.join(dir, "missing-ir.json"), setup: undefined, context_enrichment: undefined }, warbleBin: path.join(dir, "missing-warble"), ...managedCodexRuntimeOptions() });
// The launch still fails closed; it now names which link broke instead of
// reporting the whole producer incompatible.
await expect(service.create({ purpose: "analysis", vendor: "codex" })).rejects.toThrow("the Warble binary could not be resolved");
+30 -1
View File
@@ -1,4 +1,4 @@
import { existsSync, lstatSync, mkdtempSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
import { chmodSync, existsSync, lstatSync, mkdtempSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
import { homedir, tmpdir } from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
@@ -131,4 +131,33 @@ describe("Codex session WREN_HOME", () => {
expect(() => materialize(value)).toThrow(/profile is unavailable/);
expect(existsSync(path.join(value.session, ".wren"))).toBe(false);
});
it.each(["symlink", "directory", "mode", "profile", "secrets"] as const)(
"rejects retained session-home %s replacement or tampering",
(mutation) => {
const value = fixture();
const home = materialize(value);
const destination = home.home;
if (mutation === "symlink") {
const replacement = path.join(value.root, "replacement-home");
mkdirSync(replacement, { mode: 0o700 });
rmSync(destination, { recursive: true });
symlinkSync(replacement, destination, "dir");
} else if (mutation === "directory") {
rmSync(destination, { recursive: true });
mkdirSync(destination, { mode: 0o700 });
writeFileSync(path.join(destination, "profiles.yml"), "active: replacement\n", { mode: 0o600 });
writeFileSync(path.join(destination, ".env"), "DB_FILE=replacement\n", { mode: 0o600 });
} else if (mutation === "mode") {
chmodSync(destination, 0o755);
} else if (mutation === "profile") {
writeFileSync(path.join(destination, "profiles.yml"), "active: replacement\n");
} else {
writeFileSync(path.join(destination, ".env"), "DB_FILE=replacement\n");
}
expect(() => home.assertActive?.()).toThrow("native Wren session home is unavailable");
home.cleanup?.();
},
);
});
+2 -2
View File
@@ -80,8 +80,8 @@ describe("RuntimeHost readiness", () => {
const vendor = new RuntimeHost({ selected: "codex-app-server", deployment: "development", localAvailable: () => true });
await expect(vendor.probe()).resolves.toMatchObject({
backends: { local: { state: "ready" }, "codex-app-server": { state: "unprovisioned", code: "codex_app_server_unprovisioned" } },
selectedReadiness: { state: "unprovisioned", code: "codex_app_server_unprovisioned" },
backends: { local: { state: "ready" }, "codex-app-server": { state: "unprovisioned", code: "codex_wren_runtime_unprovisioned" } },
selectedReadiness: { state: "unprovisioned", code: "codex_wren_runtime_unprovisioned" },
});
});