From c9cbeaece3f92db5df7e5e50c116741278ba1126 Mon Sep 17 00:00:00 2001 From: Jon Deibel Date: Wed, 4 Nov 2026 12:01:14 -0500 Subject: [PATCH 1/3] fix(server): Codex children no longer fail to start under a managed sandbox policy Codex can be locked down by managed requirements (requirements.toml or MDM). When those forbid the danger-full-access sandbox, a Codex child delegated from a full-access parent inherits full-access, and Codex refuses the turn with "invalid value for sandbox_mode". T3 never asked Codex which modes it allows, so it reported every runtime mode as supported. The provider probe now reads configRequirements/read and reports only the runtime modes whose sandbox mode and approval policy the requirements allow. The existing runtime-mode resolution then steps an inherited mode down to the broadest allowed one, and an explicit request for a forbidden mode gets a clear error instead of a failed turn. With no requirements configured, nothing changes. Done with Claude Opus 5.5 in Claude Code, inside T3 Code. --- apps/server/src/provider/CodexProvider.ts | 25 ++++++++++- .../codexRuntimeModeRequirements.test.ts | 37 +++++++++++++++++ .../provider/codexRuntimeModeRequirements.ts | 41 +++++++++++++++++++ 3 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 apps/server/src/provider/codexRuntimeModeRequirements.test.ts create mode 100644 apps/server/src/provider/codexRuntimeModeRequirements.ts diff --git a/apps/server/src/provider/CodexProvider.ts b/apps/server/src/provider/CodexProvider.ts index ed9e3d0cde684..7b99c27f50aba 100644 --- a/apps/server/src/provider/CodexProvider.ts +++ b/apps/server/src/provider/CodexProvider.ts @@ -26,6 +26,7 @@ import type { ProviderOptionDescriptor, ServerProviderModel, ServerProviderSkill, + RuntimeMode, } from "@t3tools/contracts"; import { PREFERRED_DEFAULT_CODEX_MODELS, ServerSettingsError } from "@t3tools/contracts"; @@ -36,6 +37,7 @@ import { } from "@t3tools/shared/model"; import { resolveSpawnCommand } from "@t3tools/shared/shell"; import { codexAppServerArgs, resolveCodexLaunchArgs } from "./codexLaunchArgs.ts"; +import { codexRuntimeModesAllowedByRequirements } from "./codexRuntimeModeRequirements.ts"; import { buildServerProvider, COMPACT_SLASH_COMMAND, @@ -93,6 +95,7 @@ export interface CodexAppServerProviderSnapshot { readonly version: string | undefined; readonly models: ReadonlyArray | undefined; readonly serviceTier?: string | undefined; + readonly supportedRuntimeModes?: ReadonlyArray | undefined; readonly skills: ReadonlyArray; } @@ -551,7 +554,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, serviceTier, rateLimits, runtimeModes] = yield* Effect.all( [ withCodexDiscoveryDeadline(client.request("skills/list", { cwds: [input.cwd] }), deadlineMs), accountResponse.account?.type === "chatgpt" @@ -597,6 +600,18 @@ const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(fun ), ), ), + // Managed requirements can forbid a sandbox mode, and Codex then refuses + // to start the turn, so delegation has to know which modes are allowed. + withCodexDiscoveryDeadline( + client.request("configRequirements/read", undefined), + deadlineMs, + CODEX_CONFIG_PROBE_TIMEOUT_MS, + ).pipe( + Effect.map((response) => codexRuntimeModesAllowedByRequirements(response?.requirements)), + Effect.catch(() => + Effect.logDebug("Codex config requirements read failed.").pipe(Effect.as(undefined)), + ), + ), ], { concurrency: "unbounded" }, ); @@ -607,6 +622,7 @@ const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(fun version, models, serviceTier, + supportedRuntimeModes: runtimeModes, skills: skillsResponse ? parseCodexSkillsListResponse(skillsResponse, input.cwd) : [], } satisfies CodexAppServerProviderSnapshot; }); @@ -860,7 +876,12 @@ export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(fu }); return buildServerProvider({ - presentation: CODEX_PRESENTATION, + presentation: { + ...CODEX_PRESENTATION, + ...(snapshot.supportedRuntimeModes + ? { supportedRuntimeModes: snapshot.supportedRuntimeModes } + : {}), + }, enabled: codexSettings.enabled, checkedAt, models: pipe( diff --git a/apps/server/src/provider/codexRuntimeModeRequirements.test.ts b/apps/server/src/provider/codexRuntimeModeRequirements.test.ts new file mode 100644 index 0000000000000..a6f4fd206a827 --- /dev/null +++ b/apps/server/src/provider/codexRuntimeModeRequirements.test.ts @@ -0,0 +1,37 @@ +import * as NodeAssert from "node:assert/strict"; + +import { describe, it } from "vite-plus/test"; + +import { codexRuntimeModesAllowedByRequirements } from "./codexRuntimeModeRequirements.ts"; + +describe("codexRuntimeModesAllowedByRequirements", () => { + it("leaves every mode available when nothing restricts Codex", () => { + NodeAssert.equal(codexRuntimeModesAllowedByRequirements(null), undefined); + NodeAssert.equal(codexRuntimeModesAllowedByRequirements({}), undefined); + }); + + it("drops full access when managed requirements forbid the danger-full-access sandbox", () => { + NodeAssert.deepStrictEqual( + codexRuntimeModesAllowedByRequirements({ + allowedSandboxModes: ["read-only", "workspace-write"], + }), + ["approval-required", "auto-accept-edits", "auto"], + ); + }); + + it("drops modes whose approval policy is forbidden", () => { + NodeAssert.deepStrictEqual( + codexRuntimeModesAllowedByRequirements({ + allowedApprovalPolicies: ["untrusted", "on-request"], + }), + ["approval-required", "auto-accept-edits", "auto"], + ); + NodeAssert.deepStrictEqual( + codexRuntimeModesAllowedByRequirements({ + allowedSandboxModes: ["read-only", "workspace-write", "danger-full-access"], + allowedApprovalPolicies: ["untrusted"], + }), + ["approval-required"], + ); + }); +}); diff --git a/apps/server/src/provider/codexRuntimeModeRequirements.ts b/apps/server/src/provider/codexRuntimeModeRequirements.ts new file mode 100644 index 0000000000000..03dc6cd14434c --- /dev/null +++ b/apps/server/src/provider/codexRuntimeModeRequirements.ts @@ -0,0 +1,41 @@ +import type { RuntimeMode } from "@t3tools/contracts"; +import type * as CodexSchema from "effect-codex-app-server/schema"; + +type CodexRequirements = NonNullable; + +// Mirrors codexRuntimeModeTurnDefaults in CodexAdapterV2, which can't be +// imported here because the adapter already imports this provider. +const CODEX_RUNTIME_MODE_POLICIES: ReadonlyArray<{ + readonly runtimeMode: RuntimeMode; + readonly sandboxMode: CodexSchema.V2ConfigRequirementsReadResponse__SandboxMode; + readonly approvalPolicy: "untrusted" | "on-request" | "never"; +}> = [ + { runtimeMode: "approval-required", sandboxMode: "read-only", approvalPolicy: "untrusted" }, + { + runtimeMode: "auto-accept-edits", + sandboxMode: "workspace-write", + approvalPolicy: "on-request", + }, + { runtimeMode: "auto", sandboxMode: "workspace-write", approvalPolicy: "on-request" }, + { runtimeMode: "full-access", sandboxMode: "danger-full-access", approvalPolicy: "never" }, +]; + +/** + * The runtime modes Codex accepts under its managed requirements + * (requirements.toml or MDM). Undefined when nothing restricts them, so every + * mode stays available. + */ +export function codexRuntimeModesAllowedByRequirements( + requirements: CodexRequirements | null | undefined, +): ReadonlyArray | undefined { + const allowedSandboxModes = requirements?.allowedSandboxModes ?? undefined; + const allowedApprovalPolicies = requirements?.allowedApprovalPolicies ?? undefined; + if (allowedSandboxModes === undefined && allowedApprovalPolicies === undefined) { + return undefined; + } + return CODEX_RUNTIME_MODE_POLICIES.filter( + ({ sandboxMode, approvalPolicy }) => + (allowedSandboxModes === undefined || allowedSandboxModes.includes(sandboxMode)) && + (allowedApprovalPolicies === undefined || allowedApprovalPolicies.includes(approvalPolicy)), + ).map(({ runtimeMode }) => runtimeMode); +} From 9090c6c3a1a6f5ae2dcf6403d250bf4db706d350 Mon Sep 17 00:00:00 2001 From: Jon Deibel Date: Wed, 4 Nov 2026 12:52:14 -0500 Subject: [PATCH 2/3] fix(server): step a Codex turn down to an allowed mode when Supervised is forbidden --- .../orchestration-v2/RuntimePolicy.test.ts | 20 +++++++++++++++++++ .../src/orchestration-v2/RuntimePolicy.ts | 16 ++++++++++++--- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/apps/server/src/orchestration-v2/RuntimePolicy.test.ts b/apps/server/src/orchestration-v2/RuntimePolicy.test.ts index ec610b093f7d3..a2ab866293e22 100644 --- a/apps/server/src/orchestration-v2/RuntimePolicy.test.ts +++ b/apps/server/src/orchestration-v2/RuntimePolicy.test.ts @@ -65,9 +65,11 @@ function makeThread(input: { const grokInstanceId = ProviderInstanceId.make("grok"); const antigravityInstanceId = ProviderInstanceId.make("antigravity"); +const managedCodexInstanceId = ProviderInstanceId.make("codex-managed"); const supportedRuntimeModesByInstance = new Map>([ [grokInstanceId, ["approval-required", "auto", "full-access"]], [antigravityInstanceId, ["approval-required", "auto-accept-edits", "full-access"]], + [managedCodexInstanceId, ["auto-accept-edits", "auto"]], ]); const providerInstanceFor = (instanceId: ProviderInstanceId) => ({ @@ -156,6 +158,24 @@ it.layer(layerTest)("RuntimePolicyV2", (it) => { assert.equal(yield* modeFor(providerInstanceId, "auto-accept-edits"), "auto-accept-edits"); }), ); + + it.effect("steps down to the broadest mode Codex allows when it forbids Supervised", () => + Effect.gen(function* () { + const policy = yield* RuntimePolicy.RuntimePolicyV2; + const now = yield* DateTime.now; + const modeFor = (runtimeMode: RuntimeMode) => + policy + .resolve({ + thread: makeThread({ now, worktreePath: null, runtimeMode }), + modelSelection: { instanceId: managedCodexInstanceId, model: "gpt-5.5" }, + }) + .pipe(Effect.map((resolved) => resolved.runtimeMode)); + + assert.equal(yield* modeFor("full-access"), "auto"); + assert.equal(yield* modeFor("auto-accept-edits"), "auto-accept-edits"); + assert.equal(yield* modeFor("approval-required"), "approval-required"); + }), + ); }); it.layer( diff --git a/apps/server/src/orchestration-v2/RuntimePolicy.ts b/apps/server/src/orchestration-v2/RuntimePolicy.ts index f0d0ae38577d1..83bf4133f6a5a 100644 --- a/apps/server/src/orchestration-v2/RuntimePolicy.ts +++ b/apps/server/src/orchestration-v2/RuntimePolicy.ts @@ -92,11 +92,21 @@ function providerRuntimeMode( runtimeMode: RuntimeMode, supportedRuntimeModes: ReadonlyArray | undefined, ): RuntimeMode { - return supportedRuntimeModes === undefined || + if ( + supportedRuntimeModes === undefined || supportedRuntimeModes.length === 0 || supportedRuntimeModes.includes(runtimeMode) - ? runtimeMode - : "approval-required"; + ) { + return runtimeMode; + } + if (supportedRuntimeModes.includes("approval-required")) return "approval-required"; + // Codex's managed requirements can forbid the read-only sandbox Supervised needs, so step down + // to the broadest mode it still allows rather than one it will refuse to start. + return ( + RuntimeMode.literals.findLast( + (mode) => supportedRuntimeModes.includes(mode) && !isRuntimeModeAbove(mode, runtimeMode), + ) ?? runtimeMode + ); } const refuseAboveCeiling = ( From 0dbec0b553678dcb2925dd014f2004d9a87b4c3e Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 15 Nov 2026 00:57:40 +0000 Subject: [PATCH 3/3] fix(web): composer shows the mode Codex actually steps down to The composer fell back to the first supported mode while the server stepped down to the broadest allowed mode at or below the thread's, so a Full access thread on a Codex limited to auto-accept-edits and auto showed Auto-accept edits while it ran as Auto. Both now use one providerRuntimeMode from contracts. Co-Authored-By: Claude Opus 5.5 --- .../src/orchestration-v2/RuntimePolicy.ts | 22 +------------------ .../src/components/chat/runtimeModeConfig.ts | 3 ++- packages/contracts/src/providerPolicy.ts | 22 +++++++++++++++++++ 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/apps/server/src/orchestration-v2/RuntimePolicy.ts b/apps/server/src/orchestration-v2/RuntimePolicy.ts index 83bf4133f6a5a..65c0bcec326e9 100644 --- a/apps/server/src/orchestration-v2/RuntimePolicy.ts +++ b/apps/server/src/orchestration-v2/RuntimePolicy.ts @@ -1,5 +1,6 @@ import { isRuntimeModeAbove, + providerRuntimeMode, managedRuntimeModeCeilingMessage, ModelSelection, OrchestrationV2AppThread, @@ -88,27 +89,6 @@ export const layer: Layer.Layer = Layer.succeed(RuntimePolicyV2 }), }); -function providerRuntimeMode( - runtimeMode: RuntimeMode, - supportedRuntimeModes: ReadonlyArray | undefined, -): RuntimeMode { - if ( - supportedRuntimeModes === undefined || - supportedRuntimeModes.length === 0 || - supportedRuntimeModes.includes(runtimeMode) - ) { - return runtimeMode; - } - if (supportedRuntimeModes.includes("approval-required")) return "approval-required"; - // Codex's managed requirements can forbid the read-only sandbox Supervised needs, so step down - // to the broadest mode it still allows rather than one it will refuse to start. - return ( - RuntimeMode.literals.findLast( - (mode) => supportedRuntimeModes.includes(mode) && !isRuntimeModeAbove(mode, runtimeMode), - ) ?? runtimeMode - ); -} - const refuseAboveCeiling = ( managedPolicy: ServerSettings.ServerSettingsService["Service"]["managedPolicy"], thread: OrchestrationV2AppThread, diff --git a/apps/web/src/components/chat/runtimeModeConfig.ts b/apps/web/src/components/chat/runtimeModeConfig.ts index 3702641e816f9..9437558aed760 100644 --- a/apps/web/src/components/chat/runtimeModeConfig.ts +++ b/apps/web/src/components/chat/runtimeModeConfig.ts @@ -1,6 +1,7 @@ import { managedRuntimeModeDenial, type ManagedPolicySummary, + providerRuntimeMode, type RuntimeMode, } from "@t3tools/contracts"; import { @@ -49,7 +50,7 @@ export function resolveCompatibleRuntimeMode( supportedRuntimeModes && supportedRuntimeModes.length > 0 ? runtimeModeOptions.filter((mode) => supportedRuntimeModes.includes(mode)) : runtimeModeOptions; - return { modes, mode: modes.includes(runtimeMode) ? runtimeMode : (modes[0] ?? runtimeMode) }; + return { modes, mode: providerRuntimeMode(runtimeMode, supportedRuntimeModes) }; } export function resolveRuntimeModeOptions( diff --git a/packages/contracts/src/providerPolicy.ts b/packages/contracts/src/providerPolicy.ts index 1b94f43aabd5c..c0c07ac6e2371 100644 --- a/packages/contracts/src/providerPolicy.ts +++ b/packages/contracts/src/providerPolicy.ts @@ -42,6 +42,28 @@ const RUNTIME_MODE_RANK: Record = { export const isRuntimeModeAbove = (mode: RuntimeMode, ceiling: RuntimeMode | undefined): boolean => ceiling !== undefined && RUNTIME_MODE_RANK[mode] > RUNTIME_MODE_RANK[ceiling]; +/** The mode a provider actually runs a thread in, given the modes it supports (all when none). */ +export function providerRuntimeMode( + runtimeMode: RuntimeMode, + supportedRuntimeModes: ReadonlyArray | undefined, +): RuntimeMode { + if ( + supportedRuntimeModes === undefined || + supportedRuntimeModes.length === 0 || + supportedRuntimeModes.includes(runtimeMode) + ) { + return runtimeMode; + } + if (supportedRuntimeModes.includes("approval-required")) return "approval-required"; + // Codex's managed requirements can forbid the read-only sandbox Supervised needs, so step down + // to the broadest mode it still allows rather than one it will refuse to start. + return ( + RuntimeMode.literals.findLast( + (mode) => supportedRuntimeModes.includes(mode) && !isRuntimeModeAbove(mode, runtimeMode), + ) ?? runtimeMode + ); +} + export const ProviderInteractionMode = Schema.Literals(["default", "plan"]); export type ProviderInteractionMode = typeof ProviderInteractionMode.Type; export const DEFAULT_PROVIDER_INTERACTION_MODE: ProviderInteractionMode = "default";