diff --git a/apps/server/src/codexModelOptions.ts b/apps/server/src/codexModelOptions.ts index 43b8a3c184f5..54ba4b100e15 100644 --- a/apps/server/src/codexModelOptions.ts +++ b/apps/server/src/codexModelOptions.ts @@ -12,3 +12,9 @@ export function getCodexServiceTierOptionValue( (getModelSelectionBooleanOptionValue(modelSelection, "fastMode") === true ? "fast" : undefined) ); } + +export function getCodexCyberAccessProgramOptionValue( + modelSelection: ModelSelection | null | undefined, +): string | undefined { + return getModelSelectionStringOptionValue(modelSelection, "cyberAccessProgram"); +} diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts index 2d14fa9518de..da47b7b53f1a 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts @@ -222,6 +222,30 @@ describe("CodexAdapterV2 runtime policy", () => { }), ); + it.effect("sends the chosen cyber access program, else the model's default", () => + Effect.gen(function* () { + const build = (value?: string, defaultCyberAccessProgram?: string) => + CodexAdapterV2.buildCodexTurnStartParams({ + nativeThreadId: "native-cyber", + codexInput: [{ type: "text", text: "test" }], + runtimePolicy: { runtimeMode: "full-access", interactionMode: "default", cwd: null }, + modelSelection: { + instanceId: ProviderInstanceId.make("codex"), + model: "gpt-5.4", + ...(value === undefined ? {} : { options: [{ id: "cyberAccessProgram", value }] }), + }, + defaultCyberAccessProgram, + }); + + // Custom models have no catalog entry, so Codex keeps its own treatment. + assert.notProperty(yield* build(), "cyberAccessProgram"); + assert.equal((yield* build(undefined, "standard")).cyberAccessProgram, "standard"); + assert.equal((yield* build("automatic", "standard")).cyberAccessProgram, "standard"); + assert.equal((yield* build("daybreakBlue", "standard")).cyberAccessProgram, "daybreakBlue"); + assert.equal((yield* build("standard", "daybreakBlue")).cyberAccessProgram, "standard"); + }), + ); + it.effect("sends MCP app model context as untrusted Codex context", () => Effect.gen(function* () { const policy = { runtimeMode: "full-access", interactionMode: "default", cwd: null } as const; @@ -2072,6 +2096,74 @@ describe("CodexAdapterV2 post-settle continuation", () => { ), ); + it.effect("sends the selected cyber access program on the wire", () => + Effect.scoped( + Effect.gen(function* () { + const nativeThreadId = "cyber-thread"; + const nativeTurnId = "cyber-turn"; + const entries = codexReplayPreamble({ nativeThreadId, nativeTurnId, prompt: "review" }); + const transcript = makeCodexReplayTranscript({ + scenario: "cyber-access-program", + entries: [ + ...entries.slice(0, 5), + { + type: "expect_outbound", + label: "turn/start", + frame: { + id: 3, + method: "turn/start", + params: { + threadId: nativeThreadId, + input: [{ type: "text", text: "review" }], + cwd: "/workspace", + model: "gpt-5.4", + approvalPolicy: "never", + approvalsReviewer: "user", + sandboxPolicy: { type: "dangerFullAccess" }, + summary: "detailed", + cyberAccessProgram: "daybreakBlue", + collaborationMode: { mode: "default", settings: { model: "gpt-5.4" } }, + }, + }, + }, + ...entries.slice(6), + { + type: "emit_inbound", + label: "done", + frame: { + method: "turn/completed", + params: { + threadId: nativeThreadId, + turn: makeCodexReplayTurn({ id: nativeTurnId, status: "completed" }), + }, + }, + }, + ], + }); + const harness = yield* makeCodexReplayHarness(transcript); + yield* harness.runtime.startTurn({ + ...makeCodexTestTurnInput({ + threadId: harness.threadId, + providerThread: harness.providerThread, + now: yield* DateTime.now, + attemptId: RunAttemptId.make("cyber-attempt"), + text: "review", + }), + modelSelection: { + ...CODEX_TEST_MODEL_SELECTION, + options: [{ id: "cyberAccessProgram", value: "daybreakBlue" }], + }, + }); + yield* harness.firstTerminal; + assert.equal(harness.terminalEvents()[0]?.status, "completed"); + }).pipe( + Effect.provide( + Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer), + ), + ), + ), + ); + it.effect("preserves T3 context on the wire and restores it after compaction", () => Effect.scoped( Effect.gen(function* () { diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts index f81a3809b35f..1b2cd95358af 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts @@ -102,7 +102,10 @@ import { ChildProcess, ChildProcessSpawner } from "effect/process"; import { resolveAttachmentPath, resolveAttachmentPathById } from "../../attachmentStore.ts"; import * as PortScanner from "../../preview/PortScanner.ts"; -import { getCodexServiceTierOptionValue } from "../../codexModelOptions.ts"; +import { + getCodexCyberAccessProgramOptionValue, + getCodexServiceTierOptionValue, +} from "../../codexModelOptions.ts"; import { ServerConfig } from "../../config.ts"; import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion"; import { buildCodexAdditionalContext } from "../../provider/CodexDeveloperInstructions.ts"; @@ -685,6 +688,7 @@ const CodexTurnStartParamsWithCollaborationMode = CodexSchema.V2TurnStartParams. additionalContext: Schema.optionalKey( Schema.Record(Schema.String, CodexSchema.V2TurnStartParams__AdditionalContextEntry), ), + cyberAccessProgram: Schema.optionalKey(CodexSchema.V2TurnStartParams__CyberAccessProgram), }), ); type CodexTurnStartParamsWithCollaborationMode = @@ -737,6 +741,8 @@ function codexRuntimeModeTurnDefaults(runtimeMode: RuntimeMode): { } } +const isCodexCyberAccessProgram = Schema.is(CodexSchema.V2TurnStartParams__CyberAccessProgram); + export const buildCodexTurnStartParams = Effect.fn("CodexAdapterV2.buildCodexTurnStartParams")( function* (input: { readonly nativeThreadId: string; @@ -749,6 +755,7 @@ export const buildCodexTurnStartParams = Effect.fn("CodexAdapterV2.buildCodexTur /** ChatGPT token sharing does not accept service tiers. */ readonly omitServiceTier?: boolean; readonly appContext?: ProviderAdapter.ProviderAdapterV2TurnInput["appContext"]; + readonly defaultCyberAccessProgram?: string | undefined; }) { const runtimeModeDefaults = codexRuntimeModeTurnDefaults(input.runtimePolicy.runtimeMode); const approvalPolicy = @@ -768,6 +775,10 @@ export const buildCodexTurnStartParams = Effect.fn("CodexAdapterV2.buildCodexTur input.omitServiceTier === true ? undefined : getCodexServiceTierOptionValue(input.modelSelection); + const selectedCyberAccessProgram = getCodexCyberAccessProgramOptionValue(input.modelSelection); + const cyberAccessProgram = [selectedCyberAccessProgram, input.defaultCyberAccessProgram].find( + isCodexCyberAccessProgram, + ); // MCP-written app context must be quoted user input, not developer instructions. const appContext = Object.fromEntries( (input.appContext ?? []).map((entry) => [ @@ -803,6 +814,8 @@ export const buildCodexTurnStartParams = Effect.fn("CodexAdapterV2.buildCodexTur ...(sandboxPolicy === undefined ? {} : { sandboxPolicy }), ...(effort === undefined ? {} : { effort }), ...(serviceTier === undefined ? {} : { serviceTier }), + // Codex keeps no per-thread choice for this, so every turn must carry it. + ...(cyberAccessProgram === undefined ? {} : { cyberAccessProgram }), // Codex supplies its own mode instructions; send both modes to clear Plan on exit. collaborationMode: { mode: input.runtimePolicy.interactionMode, @@ -1583,7 +1596,10 @@ export type CodexAdapterV2DriverEnv = export const createCodexAdapterV2 = Effect.fn("CodexAdapterV2.createCodexAdapterV2")(function* ( { instanceId, environment, enabled, config }: ProviderAdapterDriverCreateInput, - hooks: Pick = {}, + hooks: Pick< + CodexAdapterV2Options, + "onUsageLimits" | "resolveRuntime" | "compatibility" | "defaultCyberAccessProgram" + > = {}, ) { const clientFactory = yield* CodexAppServerClientFactory; const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests; @@ -1633,6 +1649,7 @@ export interface CodexAdapterV2Options { readonly environment: NodeJS.ProcessEnv; readonly clientFactory: CodexAppServerClientFactoryShape; readonly onUsageLimits?: ManagedServerProvider["applyUsageLimits"]; + readonly defaultCyberAccessProgram?: (model: string) => Effect.Effect; readonly resolveRuntime?: Effect.Effect; readonly compatibility?: ( version: string, @@ -6703,7 +6720,12 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* ( ) { const threadId = yield* getNativeThreadId(turnInput.providerThread); const mcpSession = yield* mcpSessions.read(turnInput.threadId); + const defaultCyberAccessProgram = + adapterOptions.defaultCyberAccessProgram === undefined + ? undefined + : yield* adapterOptions.defaultCyberAccessProgram(turnInput.modelSelection.model); const turnStartParams = yield* buildCodexTurnStartParams({ + defaultCyberAccessProgram, nativeThreadId: threadId, codexInput, runtimePolicy: turnInput.runtimePolicy, diff --git a/apps/server/src/provider/CodexProvider.test.ts b/apps/server/src/provider/CodexProvider.test.ts index 30f63b21f318..9a5383b7c89e 100644 --- a/apps/server/src/provider/CodexProvider.test.ts +++ b/apps/server/src/provider/CodexProvider.test.ts @@ -1,6 +1,12 @@ import { assert, it } from "@effect/vitest"; -import { applyPreferredCodexDefaultModel, mapCodexModelCapabilities } from "./CodexProvider.ts"; +import { + appendCustomCodexModels, + applyPreferredCodexDefaultModel, + applyConfiguredCodexCyberAccess, + codexDefaultCyberAccessProgram, + mapCodexModelCapabilities, +} from "./CodexProvider.ts"; it("maps current Codex model capability fields", () => { const capabilities = mapCodexModelCapabilities({ @@ -153,3 +159,123 @@ it("ignores custom models that shadow a preferred slug", () => { assert.deepStrictEqual(models.find((model) => model.isDefault)?.slug, "gpt-5.4"); }); + +it("defaults cyber access to Standard when the catalog offers it", () => { + const model = { + additionalSpeedTiers: [], + defaultReasoningEffort: "medium", + description: "Test model", + displayName: "GPT Test", + hidden: false, + id: "gpt-test", + isDefault: true, + model: "gpt-test", + supportedReasoningEfforts: [], + }; + const cyber = (programs?: ReadonlyArray<"standard" | "daybreakBlue">) => + mapCodexModelCapabilities({ + ...model, + ...(programs ? { availableAccessPrograms: { cyber: [...programs] } } : {}), + }).optionDescriptors?.[0]; + + assert.strictEqual(cyber(), undefined); + assert.deepStrictEqual(cyber(["standard", "daybreakBlue"]), { + id: "cyberAccessProgram", + label: "Cyber Access", + type: "select", + options: [ + { id: "standard", label: "Standard", isDefault: true }, + { id: "daybreakBlue", label: "Daybreak Blue" }, + ], + currentValue: "standard", + }); + assert.deepInclude(cyber(["standard"]), { currentValue: "standard" }); + assert.deepInclude(cyber(["daybreakBlue"]), { currentValue: "daybreakBlue" }); +}); + +it("finds the default cyber access program for a model", () => { + const capabilities = mapCodexModelCapabilities({ + additionalSpeedTiers: [], + availableAccessPrograms: { cyber: ["standard", "daybreakBlue"] }, + defaultReasoningEffort: "medium", + description: "Test model", + displayName: "GPT Test", + hidden: false, + id: "gpt-test", + isDefault: true, + model: "gpt-test", + supportedReasoningEfforts: [], + }); + const models = [{ slug: "gpt-test", name: "GPT Test", isCustom: false, capabilities }]; + assert.strictEqual(codexDefaultCyberAccessProgram(models, "gpt-test"), "standard"); + assert.strictEqual(codexDefaultCyberAccessProgram(models, "my-model"), undefined); +}); + +it("follows Codex's daybreak setting for the default program", () => { + const capabilities = (cyber: ReadonlyArray<"standard" | "daybreakBlue">) => + mapCodexModelCapabilities({ + additionalSpeedTiers: [], + availableAccessPrograms: { cyber: [...cyber] }, + defaultReasoningEffort: "medium", + description: "Test model", + displayName: "GPT Test", + hidden: false, + id: "gpt-test", + isDefault: true, + model: "gpt-test", + supportedReasoningEfforts: [], + }); + const models = [ + { + slug: "both", + name: "Both", + isCustom: false, + capabilities: capabilities(["standard", "daybreakBlue"]), + }, + { + slug: "standard", + name: "Standard", + isCustom: false, + capabilities: capabilities(["standard"]), + }, + ]; + + const on = applyConfiguredCodexCyberAccess(models, { + daybreakEnabled: true, + apiKeyProgramsDisabled: false, + }); + assert.strictEqual(codexDefaultCyberAccessProgram(on, "both"), "daybreakBlue"); + assert.strictEqual(codexDefaultCyberAccessProgram(on, "standard"), "standard"); + + const disabled = applyConfiguredCodexCyberAccess(models, { + daybreakEnabled: false, + apiKeyProgramsDisabled: true, + }); + assert.strictEqual(codexDefaultCyberAccessProgram(disabled, "both"), undefined); +}); + +it("keeps Daybreak access off custom models that borrow built-in options", () => { + const capabilities = mapCodexModelCapabilities({ + additionalSpeedTiers: [], + availableAccessPrograms: { cyber: ["standard", "daybreakBlue"] }, + defaultReasoningEffort: "medium", + description: "Test model", + displayName: "GPT Test", + hidden: false, + id: "gpt-test", + isDefault: true, + model: "gpt-test", + supportedReasoningEfforts: [{ reasoningEffort: "medium", description: "Medium" }], + }); + const models = appendCustomCodexModels( + [{ slug: "gpt-test", name: "GPT Test", isCustom: false, capabilities }], + [{ slug: "my-model" }], + ); + + const optionIds = (slug: string) => + models + .find((model) => model.slug === slug) + ?.capabilities?.optionDescriptors?.map((descriptor) => descriptor.id); + assert.deepStrictEqual(optionIds("gpt-test"), ["reasoningEffort", "cyberAccessProgram"]); + assert.deepStrictEqual(optionIds("my-model"), ["reasoningEffort"]); +}); diff --git a/apps/server/src/provider/CodexProvider.ts b/apps/server/src/provider/CodexProvider.ts index ed9e3d0cde68..062f0313218d 100644 --- a/apps/server/src/provider/CodexProvider.ts +++ b/apps/server/src/provider/CodexProvider.ts @@ -93,6 +93,8 @@ export interface CodexAppServerProviderSnapshot { readonly version: string | undefined; readonly models: ReadonlyArray | undefined; readonly serviceTier?: string | undefined; + readonly daybreakEnabled?: boolean | undefined; + readonly apiKeyCyberAccessProgramsDisabled?: boolean | undefined; readonly skills: ReadonlyArray; } @@ -113,6 +115,12 @@ const SERVICE_TIER_DESCRIPTIONS: Readonly> = { ultrafast: "Even faster, more expensive", }; +const CYBER_ACCESS_PROGRAM_LABELS: Readonly> = { + standard: "Standard", + daybreakBlue: "Daybreak Blue", + daybreakRed: "Daybreak Red", +}; + function reasoningEffortLabel(reasoningEffort: string): string { return REASONING_EFFORT_LABELS[reasoningEffort] ?? reasoningEffort; } @@ -241,6 +249,24 @@ export function mapCodexModelCapabilities( currentValue: defaultServiceTier, }); } + const cyberAccessPrograms = model.availableAccessPrograms?.cyber ?? []; + if (cyberAccessPrograms.length > 0) { + // Codex's own clients request Standard unless Daybreak is on; omitting the field does not. + const defaultProgram = cyberAccessPrograms.includes("standard") + ? "standard" + : cyberAccessPrograms[0]; + optionDescriptors.push({ + id: "cyberAccessProgram", + label: "Cyber Access", + type: "select", + options: cyberAccessPrograms.map((program) => ({ + id: program, + label: CYBER_ACCESS_PROGRAM_LABELS[program] ?? program, + ...(program === defaultProgram ? { isDefault: true } : {}), + })), + ...(defaultProgram === undefined ? {} : { currentValue: defaultProgram }), + }); + } return createModelCapabilities({ optionDescriptors, @@ -280,6 +306,58 @@ export function applyPreferredCodexDefaultModel( }); } +/** Mirrors Codex: `daybreak = true` prefers a Daybreak program, Blue before Red. */ +export function applyConfiguredCodexCyberAccess( + models: ReadonlyArray, + input: { + readonly daybreakEnabled: boolean; + readonly apiKeyProgramsDisabled: boolean; + }, +): ReadonlyArray { + if (!input.daybreakEnabled && !input.apiKeyProgramsDisabled) return models; + return models.map((model) => { + const descriptors = model.capabilities?.optionDescriptors; + if (!model.capabilities || !descriptors) return model; + return { + ...model, + capabilities: { + ...model.capabilities, + // Codex rejects explicit programs in this session, so none can be offered or sent. + optionDescriptors: (input.apiKeyProgramsDisabled + ? descriptors.filter((descriptor) => descriptor.id !== "cyberAccessProgram") + : descriptors + ).map((descriptor) => { + if (descriptor.id !== "cyberAccessProgram" || descriptor.type !== "select") { + return descriptor; + } + const value = ["daybreakBlue", "daybreakRed"].find((program) => + descriptor.options.some((option) => option.id === program), + ); + if (!value) return descriptor; + return { + ...descriptor, + currentValue: value, + options: descriptor.options.map(({ isDefault: _isDefault, ...option }) => + option.id === value ? { ...option, isDefault: true } : option, + ), + }; + }), + }, + }; + }); +} + +/** Applies when a selection names no program, such as older threads and agent-started turns. */ +export function codexDefaultCyberAccessProgram( + models: ReadonlyArray, + slug: string, +): string | undefined { + const descriptor = models + .find((model) => model.slug === slug) + ?.capabilities?.optionDescriptors?.find(({ id }) => id === "cyberAccessProgram"); + return descriptor?.type === "select" ? descriptor.currentValue : undefined; +} + function applyConfiguredCodexServiceTier( models: ReadonlyArray, serviceTier: string | undefined, @@ -314,13 +392,19 @@ function applyConfiguredCodexServiceTier( /** * Codex has no static default capability set, so a bare custom slug borrows * the first built-in's descriptors; an entry with its own capabilities keeps - * them. + * them. Daybreak access is per model, so a borrowed slug never inherits it. */ -function appendCustomCodexModels( +export function appendCustomCodexModels( models: ReadonlyArray, customModels: ReadonlyArray, ): ReadonlyArray { - const fallbackCapabilities = models.find((model) => model.capabilities)?.capabilities ?? null; + const borrowed = models.find((model) => model.capabilities)?.capabilities ?? null; + const fallbackCapabilities = borrowed && { + ...borrowed, + optionDescriptors: borrowed.optionDescriptors?.filter( + (descriptor) => descriptor.id !== "cyberAccessProgram", + ), + }; return providerModelsFromSettings(models, customModels, fallbackCapabilities); } @@ -551,7 +635,7 @@ const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(fun // Optional discovery must finish before the outer timeout can discard the account result. const deadlineMs = spawnedAtMs + CODEX_AUTH_PROBE_TIMEOUT_MS - CODEX_DISCOVERY_MARGIN_MS; - const [skillsResponse, models, serviceTier, rateLimits] = yield* Effect.all( + const [skillsResponse, models, config, rateLimits] = yield* Effect.all( [ withCodexDiscoveryDeadline(client.request("skills/list", { cwds: [input.cwd] }), deadlineMs), accountResponse.account?.type === "chatgpt" @@ -568,7 +652,7 @@ const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(fun deadlineMs, CODEX_CONFIG_PROBE_TIMEOUT_MS, ).pipe( - Effect.map((response) => response?.config.service_tier ?? undefined), + Effect.map((response) => response?.config), Effect.catch(() => Effect.logDebug("Codex config read failed.").pipe(Effect.as(undefined))), ), // Usage is an enrichment: a failure or a slow answer degrades to "no @@ -606,7 +690,12 @@ const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(fun ...(rateLimits ? { rateLimits } : {}), version, models, - serviceTier, + serviceTier: config?.service_tier ?? undefined, + daybreakEnabled: config?.["daybreak"] === true, + apiKeyCyberAccessProgramsDisabled: + accountResponse.account?.type === "apiKey" && + (config?.["features"] as { readonly api_key_cyber_access_programs?: unknown } | undefined) + ?.api_key_cyber_access_programs === false, skills: skillsResponse ? parseCodexSkillsListResponse(skillsResponse, input.cwd) : [], } satisfies CodexAppServerProviderSnapshot; }); @@ -867,6 +956,11 @@ export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(fu appendCustomCodexModels(snapshot.models ?? [], codexSettings.customModels), applyPreferredCodexDefaultModel, (models) => applyConfiguredCodexServiceTier(models, snapshot.serviceTier), + (models) => + applyConfiguredCodexCyberAccess(models, { + daybreakEnabled: snapshot.daybreakEnabled === true, + apiKeyProgramsDisabled: snapshot.apiKeyCyberAccessProgramsDisabled === true, + }), ), skills: snapshot.skills, slashCommands: [ diff --git a/apps/server/src/provider/Drivers/CodexDriver.ts b/apps/server/src/provider/Drivers/CodexDriver.ts index 61f26c3f8cc7..fcf59fd7eb81 100644 --- a/apps/server/src/provider/Drivers/CodexDriver.ts +++ b/apps/server/src/provider/Drivers/CodexDriver.ts @@ -25,6 +25,7 @@ import { makePendingCodexProvider, probeCodexSkillsForCwd, withCodexAppServerClient, + codexDefaultCyberAccessProgram, } from "../CodexProvider.ts"; import { resolveCodexLaunchArgs } from "../codexLaunchArgs.ts"; import { makeManagedServerProvider } from "@t3tools/provider-core/server/managedProvider"; @@ -210,6 +211,10 @@ export const CodexDriver: ProviderDriver< }, { onUsageLimits: (update) => snapshot.applyUsageLimits(update), + defaultCyberAccessProgram: (model) => + snapshot.getSnapshot.pipe( + Effect.map((value) => codexDefaultCyberAccessProgram(value.models, model)), + ), compatibility: installation.compatibility, }, ).pipe( diff --git a/apps/server/src/provider/Drivers/CodexManagedProvider.ts b/apps/server/src/provider/Drivers/CodexManagedProvider.ts index f9728f9a2497..6f9332a0da08 100644 --- a/apps/server/src/provider/Drivers/CodexManagedProvider.ts +++ b/apps/server/src/provider/Drivers/CodexManagedProvider.ts @@ -20,6 +20,7 @@ import { checkCodexProviderStatus, makePendingCodexProvider, probeCodexSkillsForCwd, + codexDefaultCyberAccessProgram, } from "../CodexProvider.ts"; import { makeManagedServerProvider } from "@t3tools/provider-core/server/managedProvider"; import { mergeProviderInstanceEnvironment } from "@t3tools/provider-core/server/instanceEnvironment"; @@ -244,6 +245,10 @@ export const makeManagedCodexProvider = Effect.fn("makeManagedCodexProvider")(fu // already wraps openSession in withAccess, so resolve without re-entering it. const orchestrationAdapter = yield* createCodexAdapterV2(input, { onUsageLimits: (update) => snapshot.applyUsageLimits(update), + defaultCyberAccessProgram: (model) => + snapshot.getSnapshot.pipe( + Effect.map((value) => codexDefaultCyberAccessProgram(value.models, model)), + ), resolveRuntime: runtime.resolve, }).pipe( Effect.mapError( diff --git a/docs/user/providers-codex.md b/docs/user/providers-codex.md index bdc87902a63b..1c44c8bc647e 100644 --- a/docs/user/providers-codex.md +++ b/docs/user/providers-codex.md @@ -92,6 +92,19 @@ in the thread on web, desktop, or mobile. Some tools offer access for one reques the current session, or permanently. See [Permission modes](./permission-modes.md) for command and file approvals. +## Use Daybreak + +Models that list cyber access programs show a Cyber Access choice in the model +options next to the composer. T3 Code sends it with every turn. As in the Codex +CLI, the default is Standard unless you set `daybreak = true` in Codex's +`config.toml`. Your last choice stays selected for new threads until you change +it, so check it before starting unrelated work. Models that only offer Daybreak +stay on Daybreak. Custom models do not show the choice. + +If Codex reports that you are not logged in with a Daybreak key, sign in to +Codex again using the appropriate Daybreak credentials for your account, then +retry the turn. Use the same Codex home as the provider instance in T3 Code. + ## Codex says I hit a usage limit When Codex stops on a usage limit, the thread names the window that ran out and diff --git a/packages/effect-codex-app-server/src/client.ts b/packages/effect-codex-app-server/src/client.ts index 28f677e03ee0..4899c055f6f6 100644 --- a/packages/effect-codex-app-server/src/client.ts +++ b/packages/effect-codex-app-server/src/client.ts @@ -92,6 +92,7 @@ const V2TurnStartParamsWithCollaborationMode = CodexSchema.V2TurnStartParams.pip additionalContext: Schema.optionalKey( Schema.Record(Schema.String, CodexSchema.V2TurnStartParams__AdditionalContextEntry), ), + cyberAccessProgram: Schema.optionalKey(CodexSchema.V2TurnStartParams__CyberAccessProgram), }), );