Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
0605c85
feat(codex): choose a cyber access program for Daybreak-eligible models
Mezzle Oct 29, 2026
819036b
fix(codex): custom models do not inherit Daybreak access
Mezzle Oct 29, 2026
44db3e0
docs(codex): explain Daybreak login errors
Mezzle Oct 31, 2026
5085584
fix(codex): default cyber access to Standard, matching Codex
Mezzle Oct 31, 2026
df7b215
fix(codex): send only a cyber access program the model offers
Mezzle Nov 6, 2026
ffe1a1b
test(codex): an unread config disables API-key cyber access programs
Mezzle Nov 6, 2026
ec2d965
fix(codex): send the chosen cyber access program even when the model …
Mezzle Nov 6, 2026
c55ba0f
Merge remote-tracking branch 'origin/main' into fix/pr-27006-review-f…
juliusmarminge Nov 10, 2026
20fe5c1
test(codex): drop a cyber access test the Daybreak case already covers
juliusmarminge Nov 10, 2026
29f5fef
fix(codex): keep Standard selectable on Daybreak-only models
juliusmarminge Nov 10, 2026
3895f99
Merge remote-tracking branch 'origin/main' into takeover/27928
juliusmarminge Nov 11, 2026
dfb2be6
test(codex): keep the cyber access tests to the behaviors the PR claims
juliusmarminge Nov 11, 2026
7aa6fac
refactor(codex): resolve the cyber access default from the provider s…
juliusmarminge Nov 11, 2026
15ec9d2
fix(codex): send a cyber access program only for models that offer one
juliusmarminge Nov 11, 2026
b965de7
fix(codex): an unread Codex config keeps an explicit cyber access choice
juliusmarminge Nov 11, 2026
5843208
Merge remote-tracking branch 'origin/main' into fix/pr-27006-review-f…
juliusmarminge Nov 11, 2026
11ff273
Merge remote-tracking branch 'origin/main' into fix/pr-27006-review-f…
juliusmarminge Nov 11, 2026
fd32162
test(codex): leave the wire check to the replayed turn/start frame
juliusmarminge Nov 11, 2026
f09dec3
fix(codex): delegated tasks keep the parent's cyber access program
juliusmarminge Nov 11, 2026
f256fe8
Merge remote-tracking branch 'origin/main' into fix/pr-27006-review-f…
juliusmarminge Nov 12, 2026
2577cc3
Merge remote-tracking branch 'origin/main' into fix/pr-27006-review-f…
juliusmarminge Nov 12, 2026
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
67 changes: 63 additions & 4 deletions apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
RunId,
ThreadId,
TurnItemId,
type ServerProviderModel,
} from "@t3tools/contracts";
import { assert, describe, it } from "@effect/vitest";
import * as CodexClient from "effect-codex-app-server/client";
Expand Down Expand Up @@ -230,6 +231,34 @@ describe("CodexAdapterV2 runtime policy", () => {
}),
);

it.effect("sends the chosen cyber access program, else the model's default", () =>
Effect.gen(function* () {
const build = (value?: string, cyberAccess?: { defaultProgram: string | undefined }) =>
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 }] }),
},
cyberAccess,
});

// Custom models offer no Cyber Access, so even an explicit choice is dropped.
assert.notProperty(yield* build(), "cyberAccessProgram");
assert.notProperty(yield* build("daybreakBlue"), "cyberAccessProgram");
const daybreak = { defaultProgram: "daybreakBlue" };
assert.equal((yield* build(undefined, daybreak)).cyberAccessProgram, "daybreakBlue");
assert.equal((yield* build("standard", daybreak)).cyberAccessProgram, "standard");
// An unread config leaves no default, but an explicit Standard still goes.
const unknown = { defaultProgram: undefined };
assert.notProperty(yield* build(undefined, unknown), "cyberAccessProgram");
assert.equal((yield* build("standard", unknown)).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 @@ -1302,6 +1331,7 @@ describe("CodexAdapterV2 post-settle continuation", () => {
onUsageLimits?: CodexAdapterV2.CodexAdapterV2Options["onUsageLimits"],
promptSuggestions: Effect.Effect<boolean> = Effect.succeed(false),
onNotification: (method: string) => Effect.Effect<void> = () => Effect.void,
models?: CodexAdapterV2.CodexAdapterV2Options["models"],
) =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
Expand Down Expand Up @@ -1364,6 +1394,7 @@ describe("CodexAdapterV2 post-settle continuation", () => {
environment: {},
clientFactory,
...(onUsageLimits ? { onUsageLimits } : {}),
...(models ? { models } : {}),
digest: yield* Digest.Digest,
fileSystem,
idAllocator,
Expand Down Expand Up @@ -2705,12 +2736,18 @@ describe("CodexAdapterV2 post-settle continuation", () => {
Effect.gen(function* () {
const nativeThreadId = "context-thread";
const nativeTurnId = "context-turn";
// An explicit Standard must reach Codex, or Codex falls back to Daybreak.
const modelSelection = {
...CODEX_TEST_MODEL_SELECTION,
options: [{ id: "cyberAccessProgram", value: "standard" }],
};
const params = yield* CodexAdapterV2.buildCodexTurnStartParams({
nativeThreadId,
codexInput: [{ type: "text", text: "work" }],
runtimePolicy: CODEX_TEST_RUNTIME_POLICY,
modelSelection: CODEX_TEST_MODEL_SELECTION,
modelSelection,
hasT3Mcp: true,
cyberAccess: { defaultProgram: "daybreakBlue" },
});
assert.include(
params.additionalContext?.t3_code_orchestration_01?.value ?? "",
Expand Down Expand Up @@ -2790,6 +2827,27 @@ describe("CodexAdapterV2 post-settle continuation", () => {
if (method === "item/completed" || method === "turn/completed")
wireOrder.push(method);
}),
Effect.succeed<ReadonlyArray<ServerProviderModel>>([
{
slug: modelSelection.model,
name: modelSelection.model,
isCustom: false,
capabilities: {
optionDescriptors: [
{
id: "cyberAccessProgram",
label: "Cyber Access",
type: "select",
options: [
{ id: "standard", label: "Standard" },
{ id: "daybreakBlue", label: "Daybreak Blue", isDefault: true },
],
currentValue: "daybreakBlue",
},
],
},
},
]),
);
yield* (yield* McpProviderSessions.McpProviderSessions).set({
environmentId: EnvironmentId.make("test"),
Expand All @@ -2800,15 +2858,16 @@ describe("CodexAdapterV2 post-settle continuation", () => {
authorizationHeader: "Bearer test",
browserToolsAvailable: true,
});
yield* harness.runtime.startTurn(
makeCodexTestTurnInput({
yield* harness.runtime.startTurn({
...makeCodexTestTurnInput({
threadId: harness.threadId,
providerThread: harness.providerThread,
now: yield* DateTime.now,
attemptId: RunAttemptId.make("context-attempt"),
text: "work",
}),
);
modelSelection,
});
yield* harness.firstTerminal;
yield* Deferred.await(restoreRequested);
assert.equal(harness.terminalEvents()[0]?.status, "completed");
Expand Down
56 changes: 37 additions & 19 deletions apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
ProviderDriverKind,
ProviderSetupError,
type ServerProviderCompatibilityAdvisory,
type ServerProviderModel,
isDelegatedAppThread,
} from "@t3tools/contracts";
import { SKILL_MENTION_PATTERN } from "@t3tools/shared/composerInlineTokens";
Expand Down Expand Up @@ -712,19 +713,8 @@ const decodeTurnReasoningEffort = Schema.decodeUnknownEffect(
Schema.Union([CodexSchema.V2TurnStartParams__ReasoningEffort, Schema.Null]),
);

const CodexTurnStartParamsWithCollaborationMode = CodexSchema.V2TurnStartParams.pipe(
Schema.fieldsAssign({
collaborationMode: Schema.optionalKey(CodexSchema.ClientRequest__CollaborationMode),
additionalContext: Schema.optionalKey(
Schema.Record(Schema.String, CodexSchema.V2TurnStartParams__AdditionalContextEntry),
),
}),
);
type CodexTurnStartParamsWithCollaborationMode =
typeof CodexTurnStartParamsWithCollaborationMode.Type;
const decodeCodexTurnStartParamsWithCollaborationMode = Schema.decodeUnknownEffect(
CodexTurnStartParamsWithCollaborationMode,
);
type CodexTurnStartParams = typeof CodexClient.V2TurnStartParams.Type;
const decodeCodexTurnStartParams = Schema.decodeUnknownEffect(CodexClient.V2TurnStartParams);
const isProviderAdapterRuntimeRequestResponseError = Schema.is(
ProviderAdapter.ProviderAdapterRuntimeRequestResponseError,
);
Expand Down Expand Up @@ -770,6 +760,18 @@ function codexRuntimeModeTurnDefaults(runtimeMode: RuntimeMode): {
}
}

const isCodexCyberAccessProgram = Schema.is(CodexSchema.V2TurnStartParams__CyberAccessProgram);

function codexCyberAccess(
models: ReadonlyArray<ServerProviderModel>,
model: string,
): { readonly defaultProgram: string | undefined } | undefined {
const descriptor = models
.find((candidate) => candidate.slug === model)
?.capabilities?.optionDescriptors?.find((candidate) => candidate.id === "cyberAccessProgram");
return descriptor?.type === "select" ? { defaultProgram: descriptor.currentValue } : undefined;
}

export const buildCodexTurnStartParams = Effect.fn("CodexAdapterV2.buildCodexTurnStartParams")(
function* (input: {
readonly nativeThreadId: string;
Expand All @@ -782,6 +784,8 @@ export const buildCodexTurnStartParams = Effect.fn("CodexAdapterV2.buildCodexTur
/** ChatGPT token sharing does not accept service tiers. */
readonly omitServiceTier?: boolean;
readonly appContext?: ProviderAdapter.ProviderAdapterV2TurnInput["appContext"];
/** Set only for a model that offers Cyber Access; custom models never do. */
readonly cyberAccess?: { readonly defaultProgram: string | undefined } | undefined;
/** The user's `model_reasoning_summary` from the Codex config, when set. */
readonly configuredReasoningSummary?: CodexSchema.V2TurnStartParams__ReasoningSummary;
}) {
Expand All @@ -803,6 +807,14 @@ export const buildCodexTurnStartParams = Effect.fn("CodexAdapterV2.buildCodexTur
input.omitServiceTier === true
? undefined
: getCodexServiceTierOptionValue(input.modelSelection);
// A chosen program goes as is, even one the model lacks, so Codex rejects it visibly.
const cyberAccessProgram =
input.cyberAccess === undefined
? undefined
: [
getModelSelectionStringOptionValue(input.modelSelection, "cyberAccessProgram"),
input.cyberAccess.defaultProgram,
].find(isCodexCyberAccessProgram);
// MCP-written app context must be quoted user input, not developer instructions.
const appContext = Object.fromEntries(
(input.appContext ?? []).map((entry) => [
Expand All @@ -824,7 +836,7 @@ export const buildCodexTurnStartParams = Effect.fn("CodexAdapterV2.buildCodexTur
t3Context === undefined && Object.keys(appContext).length === 0
? undefined
: { ...t3Context, ...appContext };
return yield* decodeCodexTurnStartParamsWithCollaborationMode({
return yield* decodeCodexTurnStartParams({
threadId: input.nativeThreadId,
input: input.codexInput,
...(additionalContext ? { additionalContext } : {}),
Expand All @@ -839,6 +851,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 @@ -1683,7 +1697,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" | "models"
> = {},
) {
const clientFactory = yield* CodexAppServerClientFactory;
const continuationRequests = yield* ProviderContinuationRequests.ProviderContinuationRequests;
Expand Down Expand Up @@ -1733,6 +1750,8 @@ export interface CodexAdapterV2Options {
readonly environment: NodeJS.ProcessEnv;
readonly clientFactory: CodexAppServerClientFactoryShape;
readonly onUsageLimits?: ManagedServerProvider["applyUsageLimits"];
/** The provider's published models, read per turn for catalog defaults. */
readonly models?: Effect.Effect<ReadonlyArray<ServerProviderModel>>;
readonly resolveRuntime?: Effect.Effect<CodexEffectiveRuntime, ProviderSetupError, Scope.Scope>;
readonly compatibility?: (
version: string,
Expand Down Expand Up @@ -1792,10 +1811,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* (
environment: resolvedRuntime?.environment ?? adapterOptions.environment,
});
const additionalContextByThread = yield* Ref.make(
new Map<
string,
NonNullable<CodexTurnStartParamsWithCollaborationMode["additionalContext"]>
>(),
new Map<string, NonNullable<CodexTurnStartParams["additionalContext"]>>(),
);
const threadsAwaitingContextRestore = yield* Ref.make(new Set<string>());
// Codex drops client developer messages during compaction but only resends additionalContext
Expand Down Expand Up @@ -7240,6 +7256,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* (
) {
const threadId = yield* getNativeThreadId(turnInput.providerThread);
const mcpSession = yield* mcpSessions.read(turnInput.threadId);
const models = adapterOptions.models === undefined ? [] : yield* adapterOptions.models;
const configuredReasoningSummary = yield* client
.request("config/read", { cwd: turnInput.runtimePolicy.cwd, includeLayers: false })
.pipe(
Expand All @@ -7249,6 +7266,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* (
),
);
const turnStartParams = yield* buildCodexTurnStartParams({
cyberAccess: codexCyberAccess(models, turnInput.modelSelection.model),
nativeThreadId: threadId,
codexInput,
runtimePolicy: turnInput.runtimePolicy,
Expand Down
24 changes: 18 additions & 6 deletions apps/server/src/peer/RemoteDelegation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
CommandId,
EventId,
MessageId,
type ModelSelection,
NodeId,
type OrchestrationV2Run,
ProjectId,
Expand Down Expand Up @@ -744,7 +745,10 @@ const parentScope: McpInvocationContext.McpInvocationScope = {
};

/** The laptop's parent thread, mid-turn when its agent delegates. */
const seedParent = (laptopEnv: Effect.Success<ReturnType<typeof makeLaptop>>) =>
const seedParent = (
laptopEnv: Effect.Success<ReturnType<typeof makeLaptop>>,
parentModelSelection: ModelSelection = modelSelection,
) =>
Effect.gen(function* () {
const now = yield* DateTime.now;
const providerThreadId = ProviderThreadId.make("provider-thread:laptop-parent");
Expand All @@ -756,7 +760,7 @@ const seedParent = (laptopEnv: Effect.Success<ReturnType<typeof makeLaptop>>) =>
threadId: parentThreadId,
projectId: ProjectId.make("project:laptop"),
title: "Laptop parent",
modelSelection,
modelSelection: parentModelSelection,
runtimeMode: "full-access",
interactionMode: "default",
branch: null,
Expand Down Expand Up @@ -830,7 +834,7 @@ const seedParent = (laptopEnv: Effect.Success<ReturnType<typeof makeLaptop>>) =>
threadId: parentThreadId,
ordinal: 1,
providerInstanceId: instanceId,
modelSelection,
modelSelection: parentModelSelection,
providerThreadId,
userMessageId: MessageId.make("message:seed-user:laptop-parent"),
rootNodeId: parentRootNode,
Expand Down Expand Up @@ -1191,16 +1195,24 @@ it.effect("a task named only by environment inherits the parent's model, within
const a = yield* makeLaptop(database, offers);
// The parent runs with full access; the link allows less.
yield* linkTo(a.links, b, "auto-accept-edits");
yield* seedParent(a);
// An explicit Standard must survive, or a Daybreak default there would replace it.
const parentModelSelection = {
...modelSelection,
options: [
{ id: "reasoningEffort", value: "high" },
{ id: "cyberAccessProgram", value: "standard" },
],
};
yield* seedParent(a, parentModelSelection);

const result = yield* a.call("delegate_task", {
task: "Run the suite on the box.",
target: { environmentId: box.environmentId },
});
expect(result.isError, JSON.stringify(result.content)).toBe(false);
// The parent's model, and the link's modes rather than the parent's.
// The parent's model and options, and the link's modes rather than the parent's.
expect(yield* Ref.get(thread.launchedWith)).toEqual({
modelSelection,
modelSelection: parentModelSelection,
runtimeMode: "auto-accept-edits",
interactionMode: "default",
});
Expand Down
11 changes: 11 additions & 0 deletions apps/server/src/provider/CodexProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ it("maps current Codex model capability fields", () => {
id: "gpt-test",
isDefault: true,
model: "gpt-test",
availableAccessPrograms: { cyber: ["standard", "daybreakBlue"] },
defaultServiceTier: "flex",
serviceTiers: [
{
Expand Down Expand Up @@ -65,6 +66,16 @@ it("maps current Codex model capability fields", () => {
],
currentValue: "flex",
},
{
id: "cyberAccessProgram",
label: "Cyber Access",
type: "select",
options: [
{ id: "standard", label: "Standard", isDefault: true },
{ id: "daybreakBlue", label: "Daybreak Blue" },
],
currentValue: "standard",
},
]);
});

Expand Down
Loading
Loading