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
20 changes: 20 additions & 0 deletions apps/server/src/orchestration-v2/RuntimePolicy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProviderInstanceId, ReadonlyArray<RuntimeMode>>([
[grokInstanceId, ["approval-required", "auto", "full-access"]],
[antigravityInstanceId, ["approval-required", "auto-accept-edits", "full-access"]],
[managedCodexInstanceId, ["auto-accept-edits", "auto"]],
]);
const providerInstanceFor = (instanceId: ProviderInstanceId) =>
({
Expand Down Expand Up @@ -157,6 +159,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(
Expand Down
12 changes: 1 addition & 11 deletions apps/server/src/orchestration-v2/RuntimePolicy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
isRuntimeModeAbove,
providerRuntimeMode,
managedRuntimeModeCeilingMessage,
ModelSelection,
OrchestrationV2AppThread,
Expand Down Expand Up @@ -88,17 +89,6 @@ export const layer: Layer.Layer<RuntimePolicyV2> = Layer.succeed(RuntimePolicyV2
}),
});

function providerRuntimeMode(
runtimeMode: RuntimeMode,
supportedRuntimeModes: ReadonlyArray<RuntimeMode> | undefined,
): RuntimeMode {
return supportedRuntimeModes === undefined ||
supportedRuntimeModes.length === 0 ||
supportedRuntimeModes.includes(runtimeMode)
? runtimeMode
: "approval-required";
}

const refuseAboveCeiling = (
managedPolicy: ServerSettings.ServerSettingsService["Service"]["managedPolicy"],
thread: OrchestrationV2AppThread,
Expand Down
107 changes: 67 additions & 40 deletions apps/server/src/provider/CodexProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import type {
ProviderOptionDescriptor,
ServerProviderModel,
ServerProviderSkill,
RuntimeMode,
} from "@t3tools/contracts";
import { PREFERRED_DEFAULT_CODEX_MODELS, ServerSettingsError } from "@t3tools/contracts";

Expand All @@ -37,6 +38,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,
Expand Down Expand Up @@ -159,6 +161,7 @@ export interface CodexAppServerProviderSnapshot {
readonly version: string | undefined;
readonly models: ReadonlyArray<ServerProviderModel> | undefined;
readonly serviceTier?: string | undefined;
readonly supportedRuntimeModes?: ReadonlyArray<RuntimeMode> | undefined;
/** Undefined when `config/read` failed, so T3 cannot know Codex's default program. */
readonly daybreakEnabled?: boolean | undefined;
readonly cyberAccessProgramsUnavailable?: boolean | undefined;
Expand Down Expand Up @@ -728,45 +731,63 @@ const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(fun

// Optional discovery must finish before the outer timeout can discard the account result.
const deadlineMs = spawnedAtMs + input.probeTimeoutMs - CODEX_DISCOVERY_MARGIN_MS;
const [skillsResponse, models, { config, nativeUsage, rateLimits }] = yield* Effect.all(
[
withCodexDiscoveryDeadline(client.request("skills/list", { cwds: [input.cwd] }), deadlineMs),
accountResponse.account?.type === "chatgpt"
? requestCodexChatGptModels({
client,
codexHome: initialize.codexHome,
spawnedAtMs,
deadlineMs,
})
: withCodexDiscoveryDeadline(requestAllCodexModels(client), deadlineMs),
// The shared catalog must exclude config from the server's launch project.
withCodexDiscoveryDeadline(
client.request("config/read", { includeLayers: false }),
deadlineMs,
CODEX_CONFIG_PROBE_TIMEOUT_MS,
).pipe(
Effect.catch(() => Effect.logDebug("Codex config read failed.").pipe(Effect.as(undefined))),
Effect.flatMap((config) => {
// Only a confirmed native route may probe or publish the login's quota.
const nativeUsage =
!input.skipNativeUsage &&
config !== undefined &&
codexRoutesSpendNativeQuota(accountResponse, config.config);
const readUsage: Effect.Effect<CodexRateLimitsProbe | undefined> = nativeUsage
? readCodexRateLimits(client, deadlineMs)
: Effect.undefined;
return readUsage.pipe(
Effect.map((rateLimits) => ({
config: config?.config,
nativeUsage,
rateLimits,
})),
);
}),
),
],
{ concurrency: "unbounded" },
);
const [skillsResponse, models, { config, nativeUsage, rateLimits }, supportedRuntimeModes] =
yield* Effect.all(
[
withCodexDiscoveryDeadline(
client.request("skills/list", { cwds: [input.cwd] }),
deadlineMs,
),
accountResponse.account?.type === "chatgpt"
? requestCodexChatGptModels({
client,
codexHome: initialize.codexHome,
spawnedAtMs,
deadlineMs,
})
: withCodexDiscoveryDeadline(requestAllCodexModels(client), deadlineMs),
// The shared catalog must exclude config from the server's launch project.
withCodexDiscoveryDeadline(
client.request("config/read", { includeLayers: false }),
deadlineMs,
CODEX_CONFIG_PROBE_TIMEOUT_MS,
).pipe(
Effect.catch(() =>
Effect.logDebug("Codex config read failed.").pipe(Effect.as(undefined)),
),
Effect.flatMap((config) => {
// Only a confirmed native route may probe or publish the login's quota.
const nativeUsage =
!input.skipNativeUsage &&
config !== undefined &&
codexRoutesSpendNativeQuota(accountResponse, config.config);
const readUsage: Effect.Effect<CodexRateLimitsProbe | undefined> = nativeUsage
? readCodexRateLimits(client, deadlineMs)
: Effect.undefined;
return readUsage.pipe(
Effect.map((rateLimits) => ({
config: config?.config,
nativeUsage,
rateLimits,
})),
);
}),
),
// 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" },
);

const cyberAccess = Option.getOrUndefined(decodeCodexCyberAccessConfig(config));
return {
Expand All @@ -777,6 +798,7 @@ const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(fun
version,
models,
serviceTier: config?.service_tier ?? undefined,
supportedRuntimeModes,
daybreakEnabled: config === undefined ? undefined : cyberAccess?.daybreak === true,
// An unread config cannot rule out an API key with the programs turned off.
cyberAccessProgramsUnavailable:
Expand Down Expand Up @@ -1063,7 +1085,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(
Expand Down
37 changes: 37 additions & 0 deletions apps/server/src/provider/codexRuntimeModeRequirements.test.ts
Original file line number Diff line number Diff line change
@@ -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"],
);
});
});
41 changes: 41 additions & 0 deletions apps/server/src/provider/codexRuntimeModeRequirements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { RuntimeMode } from "@t3tools/contracts";
import type * as CodexSchema from "effect-codex-app-server/schema";

type CodexRequirements = NonNullable<CodexSchema.V2ConfigRequirementsReadResponse["requirements"]>;

// 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<RuntimeMode> | 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);
}
3 changes: 2 additions & 1 deletion apps/web/src/components/chat/runtimeModeConfig.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
managedRuntimeModeDenial,
type ManagedPolicySummary,
providerRuntimeMode,
type RuntimeMode,
} from "@t3tools/contracts";
import {
Expand Down Expand Up @@ -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(
Expand Down
22 changes: 22 additions & 0 deletions packages/contracts/src/providerPolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,28 @@ const RUNTIME_MODE_RANK: Record<RuntimeMode, number> = {
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<RuntimeMode> | 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";
Expand Down
Loading