Skip to content
Open
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
45 changes: 43 additions & 2 deletions apps/server/src/provider/Drivers/CodexDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,15 @@ import { ServerConfig } from "../../config.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import { ProviderDriverError } from "../Errors.ts";
import { makeCodexAdapter } from "../Layers/CodexAdapter.ts";
import { checkCodexProviderStatus, makePendingCodexProvider } from "../Layers/CodexProvider.ts";
import {
checkCodexProviderStatus,
makePendingCodexProvider,
probeCodexAppServerProvider,
} from "../Layers/CodexProvider.ts";
import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts";
import { makeManagedServerProvider } from "../makeManagedServerProvider.ts";
import type { ProviderDriver, ProviderInstance } from "../ProviderDriver.ts";
import type { ServerProviderDraft } from "../providerSnapshot.ts";
import { AUTH_PROBE_TIMEOUT_MS, type ServerProviderDraft } from "../providerSnapshot.ts";
import { mergeProviderInstanceEnvironment } from "../ProviderInstanceEnvironment.ts";
import {
enrichProviderSnapshotWithVersionAdvisory,
Expand Down Expand Up @@ -161,6 +165,42 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = {
...(eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}),
});
const textGeneration = yield* makeCodexTextGeneration(effectiveConfig, processEnv);
const listSkills = Effect.fn("CodexDriver.listSkills")(function* (cwd: string) {
if (!effectiveConfig.enabled) {
return [];
}

return yield* probeCodexAppServerProvider({
binaryPath: effectiveConfig.binaryPath,
homePath: effectiveConfig.homePath,
cwd,
customModels: [],
environment: processEnv,
includeModels: false,
}).pipe(
Effect.scoped,
Effect.map((result) => result.skills),
Effect.mapError(
(cause) =>
new ProviderDriverError({
driver: DRIVER_KIND,
instanceId,
detail: `Failed to list Codex skills: ${cause.message}`,
cause,
}),
),
Effect.timeoutOrElse({
duration: Duration.millis(AUTH_PROBE_TIMEOUT_MS),
orElse: () =>
new ProviderDriverError({
driver: DRIVER_KIND,
instanceId,
detail: `Timed out listing Codex skills after ${AUTH_PROBE_TIMEOUT_MS}ms`,
}),
}),
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
);
});

// Build a managed snapshot whose settings never change — mutations come
// in as instance rebuilds from the registry rather than in-place
Expand Down Expand Up @@ -209,6 +249,7 @@ export const CodexDriver: ProviderDriver<CodexSettings, CodexDriverEnv> = {
snapshot,
adapter,
textGeneration,
listSkills,
} satisfies ProviderInstance;
}),
};
26 changes: 26 additions & 0 deletions apps/server/src/provider/Drivers/GrokDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
checkGrokProviderStatus,
enrichGrokSnapshot,
} from "../Layers/GrokProvider.ts";
import { listGrokSkills } from "../Layers/GrokSkillDiscovery.ts";
import { ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts";
import { makeManagedServerProvider } from "../makeManagedServerProvider.ts";
import {
Expand Down Expand Up @@ -105,10 +106,34 @@ export const GrokDriver: ProviderDriver<GrokSettings, GrokDriverEnv> = {
env: processEnv,
});

const listSkills = Effect.fn("GrokDriver.listSkills")(function* (cwd: string) {
if (!effectiveConfig.enabled) {
return [];
}

return yield* listGrokSkills({
settings: effectiveConfig,
cwd,
environment: processEnv,
}).pipe(
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
Effect.mapError(
(cause) =>
new ProviderDriverError({
driver: DRIVER_KIND,
instanceId,
detail: cause.message,
cause,
}),
),
);
});

const adapter = yield* makeGrokAdapter(effectiveConfig, {
environment: processEnv,
...(eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}),
instanceId,
listSkills,
});
const textGeneration = yield* makeGrokTextGeneration(effectiveConfig, processEnv);

Expand Down Expand Up @@ -157,6 +182,7 @@ export const GrokDriver: ProviderDriver<GrokSettings, GrokDriverEnv> = {
snapshot,
adapter,
textGeneration,
listSkills,
} satisfies ProviderInstance;
}),
};
165 changes: 85 additions & 80 deletions apps/server/src/provider/Layers/CodexProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,94 +286,99 @@ export function buildCodexInitializeParams(): CodexSchema.V1InitializeParams {
};
}

const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(function* (input: {
readonly binaryPath: string;
readonly homePath?: string;
readonly cwd: string;
readonly customModels?: ReadonlyArray<string>;
readonly environment?: NodeJS.ProcessEnv;
}) {
// `~` is not shell-expanded when env vars are set via `child_process.spawn`,
// so `CODEX_HOME=~/.codex_work` would reach codex verbatim and trip
// "CODEX_HOME points to '~/.codex_work', but that path does not exist".
// Expand here for parity with `CodexTextGeneration`/`CodexSessionRuntime`.
const resolvedHomePath = input.homePath ? expandHomePath(input.homePath) : undefined;
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const environment = {
...input.environment,
...(resolvedHomePath ? { CODEX_HOME: resolvedHomePath } : {}),
};
const spawnCommand = yield* resolveSpawnCommand(input.binaryPath, ["app-server"], {
env: environment,
extendEnv: true,
});
const child = yield* spawner
.spawn(
ChildProcess.make(spawnCommand.command, spawnCommand.args, {
cwd: input.cwd,
env: environment,
extendEnv: true,
forceKillAfter: CODEX_APP_SERVER_PROBE_FORCE_KILL_AFTER,
shell: spawnCommand.shell,
}),
)
.pipe(
Effect.mapError(
(cause) =>
new CodexErrors.CodexAppServerSpawnError({
command: `${input.binaryPath} app-server`,
cause,
}),
),
export const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(
function* (input: {
readonly binaryPath: string;
readonly homePath?: string;
readonly cwd: string;
readonly customModels?: ReadonlyArray<string>;
readonly environment?: NodeJS.ProcessEnv;
readonly includeModels?: boolean;
}) {
// `~` is not shell-expanded when env vars are set via `child_process.spawn`,
// so `CODEX_HOME=~/.codex_work` would reach codex verbatim and trip
// "CODEX_HOME points to '~/.codex_work', but that path does not exist".
// Expand here for parity with `CodexTextGeneration`/`CodexSessionRuntime`.
const resolvedHomePath = input.homePath ? expandHomePath(input.homePath) : undefined;
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const environment = {
...input.environment,
...(resolvedHomePath ? { CODEX_HOME: resolvedHomePath } : {}),
};
const spawnCommand = yield* resolveSpawnCommand(input.binaryPath, ["app-server"], {
env: environment,
extendEnv: true,
});
const child = yield* spawner
.spawn(
ChildProcess.make(spawnCommand.command, spawnCommand.args, {
cwd: input.cwd,
env: environment,
extendEnv: true,
forceKillAfter: CODEX_APP_SERVER_PROBE_FORCE_KILL_AFTER,
shell: spawnCommand.shell,
}),
)
.pipe(
Effect.mapError(
(cause) =>
new CodexErrors.CodexAppServerSpawnError({
command: `${input.binaryPath} app-server`,
cause,
}),
),
);
const clientContext = yield* Layer.build(CodexClient.layerChildProcess(child));
const client = yield* Effect.service(CodexClient.CodexAppServerClient).pipe(
Effect.provide(clientContext),
);
const clientContext = yield* Layer.build(CodexClient.layerChildProcess(child));
const client = yield* Effect.service(CodexClient.CodexAppServerClient).pipe(
Effect.provide(clientContext),
);

const initialize = yield* client.request("initialize", {
clientInfo: {
name: "t3code_desktop",
title: "T3 Code Desktop",
version: "0.1.0",
},
capabilities: {
experimentalApi: true,
},
});
yield* client.notify("initialized", undefined);
const initialize = yield* client.request("initialize", {
clientInfo: {
name: "t3code_desktop",
title: "T3 Code Desktop",
version: "0.1.0",
},
capabilities: {
experimentalApi: true,
},
});
yield* client.notify("initialized", undefined);

// Extract the version string after the first '/' in userAgent, up to the next space or the end
const versionMatch = initialize.userAgent.match(/\/([^\s]+)/);
const version = versionMatch ? versionMatch[1] : undefined;

const accountResponse = yield* client.request("account/read", {});
if (!accountResponse.account && accountResponse.requiresOpenaiAuth) {
return {
account: accountResponse,
version,
models: appendCustomCodexModels([], input.customModels ?? []),
skills: [],
} satisfies CodexAppServerProviderSnapshot;
}

// Extract the version string after the first '/' in userAgent, up to the next space or the end
const versionMatch = initialize.userAgent.match(/\/([^\s]+)/);
const version = versionMatch ? versionMatch[1] : undefined;
let skillsResponse: CodexSchema.V2SkillsListResponse;
let models: ReadonlyArray<ServerProviderModel>;
if (input.includeModels === false) {
skillsResponse = yield* client.request("skills/list", { cwds: [input.cwd] });
models = [];
} else {
[skillsResponse, models] = yield* Effect.all(
[client.request("skills/list", { cwds: [input.cwd] }), requestAllCodexModels(client)],
{ concurrency: "unbounded" },
);
}

const accountResponse = yield* client.request("account/read", {});
if (!accountResponse.account && accountResponse.requiresOpenaiAuth) {
return {
account: accountResponse,
version,
models: appendCustomCodexModels([], input.customModels ?? []),
skills: [],
models: appendCustomCodexModels(models, input.customModels ?? []),
skills: parseCodexSkillsListResponse(skillsResponse, input.cwd),
} satisfies CodexAppServerProviderSnapshot;
}

const [skillsResponse, models] = yield* Effect.all(
[
client.request("skills/list", {
cwds: [input.cwd],
}),
requestAllCodexModels(client),
],
{ concurrency: "unbounded" },
);

return {
account: accountResponse,
version,
models: appendCustomCodexModels(models, input.customModels ?? []),
skills: parseCodexSkillsListResponse(skillsResponse, input.cwd),
} satisfies CodexAppServerProviderSnapshot;
});
},
);

const emptyCodexModelsFromSettings = (codexSettings: CodexSettings): ServerProvider["models"] => {
const models = new Set<string>();
Expand Down
Loading
Loading