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,
Comment on lines +173 to +179

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pass Codex launch args to skill probes

When a Codex instance is configured with launchArgs or T3CODE_CODEX_LAUNCH_ARGS (for example --strict-config, --config, or feature flags), status checks and real sessions resolve and pass those args, but this new server.listProviderSkills path starts a plain app-server without them. In that configuration the $ menu can discover a different skill set or fail even though the provider itself works with the configured app-server; pass the resolved launch args into probeCodexAppServerProvider here as the other Codex app-server callers do.

Useful? React with 👍 / 👎.

}).pipe(
Comment on lines +173 to +180

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium Drivers/CodexDriver.ts:173

listSkills calls probeCodexAppServerProvider without passing effectiveConfig.launchArgs, so instances that require custom Codex launch arguments (e.g. --config flags) are probed with a different app-server configuration than their normal snapshot and sessions. Skill discovery can return the wrong skill set or fail even though the provider itself works. Pass launchArgs: resolveCodexLaunchArgs(effectiveConfig.launchArgs, processEnv) as the status probe already does.

Suggested change
return yield* probeCodexAppServerProvider({
binaryPath: effectiveConfig.binaryPath,
homePath: effectiveConfig.homePath,
cwd,
customModels: [],
environment: processEnv,
includeModels: false,
}).pipe(
return yield* probeCodexAppServerProvider({
binaryPath: effectiveConfig.binaryPath,
homePath: effectiveConfig.homePath,
launchArgs: resolveCodexLaunchArgs(effectiveConfig.launchArgs, processEnv),
cwd,
customModels: [],
environment: processEnv,
includeModels: false,
}).pipe(
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/provider/Drivers/CodexDriver.ts around lines 173-180:

`listSkills` calls `probeCodexAppServerProvider` without passing `effectiveConfig.launchArgs`, so instances that require custom Codex launch arguments (e.g. `--config` flags) are probed with a different app-server configuration than their normal snapshot and sessions. Skill discovery can return the wrong skill set or fail even though the provider itself works. Pass `launchArgs: resolveCodexLaunchArgs(effectiveConfig.launchArgs, processEnv)` as the status probe already does.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex skills omit launch args

Medium Severity

The CodexDriver.listSkills function omits launchArgs when probing the app server, unlike checkCodexProviderStatus and the Codex adapter. This causes configured Codex launch arguments (including T3CODE_CODEX_LAUNCH_ARGS) to be ignored during skill discovery, which can lead to skills being missed or mis-resolved in the Composer's $ menu.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ecda648. Configure here.

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 @@ -106,10 +107,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 @@ -159,6 +184,7 @@ export const GrokDriver: ProviderDriver<GrokSettings, GrokDriverEnv> = {
snapshot,
adapter,
textGeneration,
listSkills,
} satisfies ProviderInstance;
}),
};
171 changes: 88 additions & 83 deletions apps/server/src/provider/Layers/CodexProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,99 +287,104 @@ export function buildCodexInitializeParams(): CodexSchema.V1InitializeParams {
};
}

const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(function* (input: {
readonly binaryPath: string;
readonly homePath?: string;
readonly launchArgs?: 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,
codexAppServerArgs(input.launchArgs),
{
env: environment,
extendEnv: true,
},
);
const child = yield* spawner
.spawn(
ChildProcess.make(spawnCommand.command, spawnCommand.args, {
cwd: input.cwd,
export const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(
function* (input: {
readonly binaryPath: string;
readonly homePath?: string;
readonly launchArgs?: 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,
codexAppServerArgs(input.launchArgs),
{
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 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