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
6 changes: 6 additions & 0 deletions apps/server/src/codexModelOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
92 changes: 92 additions & 0 deletions apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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* () {
Expand Down
26 changes: 24 additions & 2 deletions apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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;
Expand All @@ -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 =
Expand All @@ -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,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
);
// MCP-written app context must be quoted user input, not developer instructions.
const appContext = Object.fromEntries(
(input.appContext ?? []).map((entry) => [
Expand Down Expand Up @@ -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 }),
Comment thread
Mezzle marked this conversation as resolved.
Resolved
// Codex supplies its own mode instructions; send both modes to clear Plan on exit.
collaborationMode: {
mode: input.runtimePolicy.interactionMode,
Expand Down Expand Up @@ -1583,7 +1596,10 @@ export type CodexAdapterV2DriverEnv =

export const createCodexAdapterV2 = Effect.fn("CodexAdapterV2.createCodexAdapterV2")(function* (
{ instanceId, environment, enabled, config }: ProviderAdapterDriverCreateInput<CodexSettings>,
hooks: Pick<CodexAdapterV2Options, "onUsageLimits" | "resolveRuntime" | "compatibility"> = {},
hooks: Pick<
CodexAdapterV2Options,
"onUsageLimits" | "resolveRuntime" | "compatibility" | "defaultCyberAccessProgram"
> = {},
) {
const clientFactory = yield* CodexAppServerClientFactory;
const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests;
Expand Down Expand Up @@ -1633,6 +1649,7 @@ export interface CodexAdapterV2Options {
readonly environment: NodeJS.ProcessEnv;
readonly clientFactory: CodexAppServerClientFactoryShape;
readonly onUsageLimits?: ManagedServerProvider["applyUsageLimits"];
readonly defaultCyberAccessProgram?: (model: string) => Effect.Effect<string | undefined>;
readonly resolveRuntime?: Effect.Effect<CodexEffectiveRuntime, ProviderSetupError, Scope.Scope>;
readonly compatibility?: (
version: string,
Expand Down Expand Up @@ -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,
Expand Down
128 changes: 127 additions & 1 deletion apps/server/src/provider/CodexProvider.test.ts
Original file line number Diff line number Diff line change
@@ -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({
Expand Down Expand Up @@ -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"]);
});
Loading
Loading