Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import * as Orchestrator from "../Orchestrator.ts";
import * as ProviderReplayHarness from "../testkit/ProviderReplayHarness.ts";
import * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter";
import type * as ProviderContinuationRequests from "@t3tools/provider-core/server/ProviderContinuationRequests";
import { symlinksSupported } from "@t3tools/shared/testing/symlinks";
import * as CodexAdapterV2 from "./CodexAdapterV2.ts";
import { makeReplayServerConfig, withCodexReplayChildMetadata } from "./CodexAdapterV2.testkit.ts";
import * as CodexAdapterV2Testkit from "./CodexAdapterV2.testkit.ts";
Expand Down Expand Up @@ -782,6 +783,169 @@ describe("CodexAdapterV2 process spawning", () => {
}).pipe(Effect.provideService(HostProcess.Platform, "linux")),
);

it.effect.each(["sqlite", "config"] as const)(
"keeps paths in the construction cause, not the public %s error detail",
(entry) =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-codex-safe-detail-" });
const sharedHome = path.join(root, "shared");
const shadowHome = path.join(root, "shadow");
yield* fs.makeDirectory(sharedHome);
yield* fs.makeDirectory(shadowHome);
const name = entry === "sqlite" ? "state_5.sqlite" : "config.toml";
const target = path.join(sharedHome, name);
const link = path.join(shadowHome, name);
yield* fs.writeFileString(target, "shared");
if (entry === "sqlite") yield* fs.symlink(target, link);
else yield* fs.writeFileString(link, "private config");
const error = yield* CodexAdapterV2.createCodexAdapterV2({
instanceId: CodexAdapterV2.CODEX_DEFAULT_INSTANCE_ID,
displayName: undefined,
environment: [],
enabled: true,
config: { ...DEFAULT_CODEX_SETTINGS, homePath: sharedHome, shadowHomePath: shadowHome },
}).pipe(
Effect.provide(
Layer.mergeAll(
CodexAdapterV2.layerAppServerClientFactory,
ServerConfig.layerTest(process.cwd(), { prefix: "t3-codex-safe-detail-config-" }),
),
),
Effect.provideService(
ProviderEventLoggers.ProviderEventLoggers,
ProviderEventLoggers.NoOpProviderEventLoggers,
),
Effect.flip,
);
if (error._tag !== "ProviderAdapterDriverCreateError") return yield* error;
assert.notInclude(error.detail, sharedHome);
assert.notInclude(error.detail, shadowHome);
if (entry === "sqlite") assert.include(error.detail, "Stop all Codex processes");
else assert.equal(error.detail, "Failed to prepare Codex shadow home.");
assert.deepInclude(error.cause, { linkPath: link });
if (entry === "sqlite") assert.equal(yield* fs.readLink(link), target);
else assert.equal(yield* fs.readFileString(link), "private config");
}).pipe(
Effect.provide(
Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer),
),
),
{ skip: !symlinksSupported },
);

it.effect.each([
{
platform: "linux",
key: "CODEX_SQLITE_HOME",
sqliteHome: undefined,
inheritedKey: "CODEX_SQLITE_HOME",
},
{
platform: "linux",
key: "CODEX_SQLITE_HOME",
sqliteHome: "",
inheritedKey: "CODEX_SQLITE_HOME",
},
{
platform: "linux",
key: "CODEX_SQLITE_HOME",
sqliteHome: " \t ",
inheritedKey: "CODEX_SQLITE_HOME",
},
{
platform: "linux",
key: "CODEX_SQLITE_HOME",
sqliteHome: "/explicit/sqlite",
inheritedKey: "CODEX_SQLITE_HOME",
},
{
platform: "win32",
key: "codex_sqlite_home",
sqliteHome: "/explicit/sqlite",
inheritedKey: "CODEX_SQLITE_HOME",
},
{
platform: "win32",
key: "CODEX_SQLITE_HOME",
sqliteHome: "/explicit/sqlite",
inheritedKey: "codex_sqlite_home",
},
] as const)(
"keeps app-server SQLite canonical with $platform $key precedence ($sqliteHome)",
({ platform, key, sqliteHome, inheritedKey }) =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-codex-sqlite-launch-" });
const sharedHome = path.join(root, "shared");
const shadowHome = path.join(root, "shadow");
const environments: Array<NodeJS.ProcessEnv> = [];
const spawner = ChildProcessSpawner.make((command) => {
if (ChildProcess.isStandardCommand(command)) environments.push(command.options.env ?? {});
return Effect.fail(
PlatformError.systemError({
_tag: "NotFound",
module: "ChildProcess",
method: "spawn",
}),
);
});
const adapter = yield* CodexAdapterV2.createCodexAdapterV2({
instanceId: CodexAdapterV2.CODEX_DEFAULT_INSTANCE_ID,
displayName: undefined,
environment:
sqliteHome === undefined ? [] : [{ name: key, value: sqliteHome, sensitive: false }],
enabled: true,
config: { ...DEFAULT_CODEX_SETTINGS, homePath: sharedHome, shadowHomePath: shadowHome },
}).pipe(
Effect.provide(
Layer.mergeAll(
CodexAdapterV2.layerAppServerClientFactory,
ServerConfig.layerTest(process.cwd(), { prefix: "t3-codex-sqlite-" }),
),
),
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
Effect.provideService(
ProviderEventLoggers.ProviderEventLoggers,
ProviderEventLoggers.NoOpProviderEventLoggers,
),
);
yield* adapter
.openSession({
threadId: ThreadId.make("thread-sqlite"),
providerSessionId: ProviderSessionId.make("provider-session-sqlite"),
modelSelection: CODEX_TEST_MODEL_SELECTION,
runtimePolicy: CODEX_TEST_RUNTIME_POLICY,
})
.pipe(Effect.scoped, Effect.exit);
assert.lengthOf(environments, 1);
assert.equal(environments[0]?.CODEX_HOME, shadowHome);
assert.equal(
environments[0]?.CODEX_SQLITE_HOME,
sqliteHome?.trim() ? sqliteHome : sharedHome,
);
assert.lengthOf(
Object.keys(environments[0] ?? {}).filter(
(key) => key.toUpperCase() === "CODEX_SQLITE_HOME",
),
1,
);
}).pipe(
Effect.provide(
Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer),
),
Effect.provideService(
HostProcess.Environment,
sqliteHome ? { [inheritedKey]: "/inherited/sqlite" } : {},
),
Effect.provideService(HostProcess.Platform, platform),
Effect.provideService(SpawnExecutableResolution, () => "codex"),
),
{ skip: !symlinksSupported },
);

it.effect("expands ~ in the configured binary path before spawning", () =>
Effect.gen(function* () {
const spawnedCommands: Array<string> = [];
Expand Down
14 changes: 11 additions & 3 deletions apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,10 @@ import {
toMcpElicitationResponse,
} from "../../provider/CodexMcpElicitation.ts";
import {
CODEX_SQLITE_RECOVERY_GUIDANCE,
materializeCodexShadowHome,
mergeCodexEnvironment,
withCodexSqliteHome,
resolveCodexHomeLayout,
} from "../../provider/Drivers/CodexHomeLayout.ts";
import {
Expand All @@ -114,7 +117,6 @@ import {
} from "../../provider/EventNdjsonLogger.ts";
import * as ProviderEventLoggers from "@t3tools/provider-core/server/ProviderEventLoggers";
import { codexAppServerArgs, resolveCodexLaunchArgs } from "../../provider/codexLaunchArgs.ts";
import { mergeProviderInstanceEnvironment } from "@t3tools/provider-core/server/instanceEnvironment";
import * as McpProviderSession from "@t3tools/provider-core/server/mcpSession";
import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";
import {
Expand Down Expand Up @@ -1585,7 +1587,10 @@ export const createCodexAdapterV2 = (
new ProviderAdapterDriverCreateError({
driver: CODEX_DRIVER_KIND,
instanceId,
detail: "Failed to materialize the Codex shadow home.",
detail:
cause._tag === "CodexShadowHomeSqliteRecoveryRequiredError"
? CODEX_SQLITE_RECOVERY_GUIDANCE
: "Failed to prepare Codex shadow home.",
cause,
}),
),
Expand All @@ -1601,7 +1606,10 @@ export const createCodexAdapterV2 = (
return yield* makeCodexAdapterV2({
instanceId,
settings,
environment: yield* mergeProviderInstanceEnvironment(environment, hostEnvironment),
environment: withCodexSqliteHome(
homeLayout,
yield* mergeCodexEnvironment(environment, hostEnvironment),
),
clientFactory,
crypto,
fileSystem,
Expand Down
29 changes: 26 additions & 3 deletions apps/server/src/provider/CodexManagedRuntime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ it.effect("managed home defaults to the global Codex home and honors configured
ProviderInstanceId.make("codex-work"),
decodeSettings({ homePath: "/custom/shared", shadowHomePath: "/custom/shadow" }),
);
assert.equal(configured.sharedHomePath, "/custom/shared");
assert.equal(configured.effectiveHomePath, "/custom/shadow");
assert.equal(configured.sharedHomePath, path.resolve("/custom/shared"));
assert.equal(configured.effectiveHomePath, path.resolve("/custom/shadow"));
}).pipe(Effect.provide(NodeServices.layer)),
);
it.effect.each(
Expand Down Expand Up @@ -135,10 +135,12 @@ it.effect.each(
if (account === "primary") {
assert.equal(effective.config.homePath, sharedHome);
assert.equal(runtime.homeLayout.mode, "direct");
assert.isUndefined(effective.environment.CODEX_SQLITE_HOME);
} else {
assert.include(effective.config.homePath, instanceId);
assert.include(effective.config.homePath, "userdata/providers/codex");
assert.include(effective.config.homePath, path.join("userdata", "providers", "codex"));
assert.equal(runtime.homeLayout.mode, "authOverlay");
assert.equal(effective.environment.CODEX_SQLITE_HOME, sharedHome);
assert.equal(
yield* fs.readLink(path.join(effective.config.homePath, "sessions")),
path.join(sharedHome, "sessions"),
Expand Down Expand Up @@ -173,6 +175,27 @@ it.effect.each(
assert.strictEqual(ambient.CODEX_HOME, "/user/.codex");
}).pipe(Effect.scoped);
assert.strictEqual(leases, 0);
if (account === "additional") {
yield* fs.writeFileString(path.join(sharedHome, "state_5.sqlite"), "");
yield* fs.symlink(
path.join(sharedHome, "state_5.sqlite"),
path.join(runtime.homePath, "state_5.sqlite"),
);
const error = yield* runtime.resolve.pipe(Effect.scoped, Effect.flip);
assert.equal(error._tag, "ProviderSetupError");
assert.include(error.detail, "Codex SQLite recovery is required");
assert.include(error.detail, "Stop all Codex processes");
assert.notInclude(error.detail, sharedHome);
assert.notInclude(error.detail, runtime.homePath);
assert.notInclude(error.message, sharedHome);
assert.notInclude(error.message, runtime.homePath);
assert.deepInclude(error.cause, {
_tag: "CodexShadowHomeSqliteRecoveryRequiredError",
sharedHomePath: sharedHome,
linkPath: path.join(runtime.homePath, "state_5.sqlite"),
});
assert.strictEqual(leases, 0);
}
yield* runtime.auth.controller.logout(Effect.void);
assert.equal(
yield* fs.readFileString(path.join(sharedHome, "auth.json")),
Expand Down
33 changes: 29 additions & 4 deletions apps/server/src/provider/CodexManagedRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import * as Schema from "effect/Schema";
import * as ServerConfig from "../config.ts";
import * as CodexInstallation from "./CodexInstallation.ts";
import { makeCodexChatGptAuth } from "./CodexChatGptAuth.ts";
import { materializeCodexShadowHome } from "./Drivers/CodexHomeLayout.ts";
import {
CODEX_SQLITE_RECOVERY_GUIDANCE,
inspectCodexShadowSqlite,
materializeCodexShadowHome,
withCodexSqliteHome,
} from "./Drivers/CodexHomeLayout.ts";

export interface CodexEffectiveRuntime {
readonly config: CodexSettings;
Expand Down Expand Up @@ -55,6 +60,22 @@ export const makeCodexManagedRuntime = Effect.fn("makeCodexManagedRuntime")(func
);
const homePath = homeLayout.effectiveHomePath ?? homeLayout.sharedHomePath;
const resolve = Effect.gen(function* () {
yield* inspectCodexShadowSqlite(homeLayout).pipe(
Effect.provideService(FileSystem.FileSystem, fs),
Effect.provideService(Path.Path, path),
Effect.mapError(
(cause) =>
new ProviderSetupError({
instanceId: options.instanceId,
operation: "runtime",
cause,
detail:
cause._tag === "CodexShadowHomeSqliteRecoveryRequiredError"
? CODEX_SQLITE_RECOVERY_GUIDANCE
: "Could not prepare the managed Codex runtime.",
}),
),
);
const executable = yield* installation.acquire().pipe(
Effect.mapError(
() =>
Expand All @@ -74,7 +95,11 @@ export const makeCodexManagedRuntime = Effect.fn("makeCodexManagedRuntime")(func
new ProviderSetupError({
instanceId: options.instanceId,
operation: "runtime",
detail: cause.message,
detail:
cause._tag === "CodexShadowHomeSqliteRecoveryRequiredError"
? CODEX_SQLITE_RECOVERY_GUIDANCE
: "Could not prepare the managed Codex runtime.",
cause,
}),
),
);
Expand All @@ -89,11 +114,11 @@ export const makeCodexManagedRuntime = Effect.fn("makeCodexManagedRuntime")(func
),
);
// Ambient CLI overrides cannot redirect a T3-owned token to a different provider.
const environment: NodeJS.ProcessEnv = {
const environment = withCodexSqliteHome(homeLayout, {
...options.environment,
ACCESS_TOKEN: credentials.accessToken,
CODEX_HOME: homePath,
};
});
delete environment.T3CODE_CODEX_LAUNCH_ARGS;
delete environment.OPENAI_API_KEY;
delete environment.OPENAI_BASE_URL;
Expand Down
Loading
Loading