fix: drop invalid Codex app-server service tiers

This commit is contained in:
Peter Steinberger
2026-04-23 01:23:37 +01:00
parent 9f358456db
commit fa43cbfcba
7 changed files with 96 additions and 34 deletions

View File

@@ -29,6 +29,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Codex harness: ignore dynamic tool descriptions when deciding whether to reuse a native app-server thread while still fingerprinting tool schemas, so channel-specific copy changes no longer reset otherwise compatible Codex conversations. (#69976) Thanks @chen-zhang-cs-code.
- Codex harness: drop invalid legacy app-server `serviceTier` values such as `"priority"` before native thread and turn requests, while keeping supported Codex tiers limited to `"fast"` and `"flex"`. Fixes #64815.
- Codex harness: show bounded, sanitized permission target samples in app-server approval prompts, so native permission requests keep their specific hosts, roots, and paths visible without leaking home usernames or URL credentials. (#70340) Thanks @Lucenx9.
- Docs/Codex harness: narrow native compaction docs to the current start/completion signals, without promising a readable summary or kept-entry audit list yet. (#69612) Thanks @91wan.
- Providers/Amazon Bedrock: use known context-window metadata for discovered models while keeping the unknown-model fallback conservative, so compaction and overflow handling improve for newer Bedrock models without overstating unlisted model limits. Thanks @wirjo.

View File

@@ -289,7 +289,7 @@ To opt in to Codex guardian-reviewed approvals, set `appServer.mode:
config: {
appServer: {
mode: "guardian",
serviceTier: "priority",
serviceTier: "fast",
},
},
},
@@ -361,20 +361,20 @@ For an already-running app-server, use WebSocket transport:
Supported `appServer` fields:
| Field | Default | Meaning |
| ------------------- | ---------------------------------------- | --------------------------------------------------------------- |
| `transport` | `"stdio"` | `"stdio"` spawns Codex; `"websocket"` connects to `url`. |
| `command` | `"codex"` | Executable for stdio transport. |
| `args` | `["app-server", "--listen", "stdio://"]` | Arguments for stdio transport. |
| `url` | unset | WebSocket app-server URL. |
| `authToken` | unset | Bearer token for WebSocket transport. |
| `headers` | `{}` | Extra WebSocket headers. |
| `requestTimeoutMs` | `60000` | Timeout for app-server control-plane calls. |
| `mode` | `"yolo"` | Preset for YOLO or guardian-reviewed execution. |
| `approvalPolicy` | `"never"` | Native Codex approval policy sent to thread start/resume/turn. |
| `sandbox` | `"danger-full-access"` | Native Codex sandbox mode sent to thread start/resume. |
| `approvalsReviewer` | `"user"` | Use `"guardian_subagent"` to let Codex Guardian review prompts. |
| `serviceTier` | unset | Optional Codex service tier, for example `"priority"`. |
| Field | Default | Meaning |
| ------------------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `transport` | `"stdio"` | `"stdio"` spawns Codex; `"websocket"` connects to `url`. |
| `command` | `"codex"` | Executable for stdio transport. |
| `args` | `["app-server", "--listen", "stdio://"]` | Arguments for stdio transport. |
| `url` | unset | WebSocket app-server URL. |
| `authToken` | unset | Bearer token for WebSocket transport. |
| `headers` | `{}` | Extra WebSocket headers. |
| `requestTimeoutMs` | `60000` | Timeout for app-server control-plane calls. |
| `mode` | `"yolo"` | Preset for YOLO or guardian-reviewed execution. |
| `approvalPolicy` | `"never"` | Native Codex approval policy sent to thread start/resume/turn. |
| `sandbox` | `"danger-full-access"` | Native Codex sandbox mode sent to thread start/resume. |
| `approvalsReviewer` | `"user"` | Use `"guardian_subagent"` to let Codex Guardian review prompts. |
| `serviceTier` | unset | Optional Codex app-server service tier: `"fast"`, `"flex"`, or `null`. Invalid legacy values are ignored. |
The older environment variables still work as fallbacks for local testing when
the matching config field is unset:

View File

@@ -83,7 +83,7 @@
"enum": ["user", "guardian_subagent"],
"default": "user"
},
"serviceTier": { "type": "string" }
"serviceTier": { "type": ["string", "null"], "enum": ["fast", "flex", null] }
}
}
}
@@ -165,7 +165,7 @@
},
"appServer.serviceTier": {
"label": "Service Tier",
"help": "Optional Codex service tier passed when starting or resuming threads.",
"help": "Optional Codex app-server service tier. Use fast, flex, or null.",
"advanced": true
}
}

View File

@@ -19,7 +19,7 @@ describe("Codex app-server config", () => {
approvalPolicy: "on-request",
sandbox: "danger-full-access",
approvalsReviewer: "guardian_subagent",
serviceTier: "priority",
serviceTier: "flex",
},
},
env: {
@@ -33,7 +33,7 @@ describe("Codex app-server config", () => {
approvalPolicy: "on-request",
sandbox: "danger-full-access",
approvalsReviewer: "guardian_subagent",
serviceTier: "priority",
serviceTier: "flex",
start: expect.objectContaining({
transport: "websocket",
url: "ws://127.0.0.1:39175",
@@ -43,6 +43,29 @@ describe("Codex app-server config", () => {
);
});
it("drops invalid legacy service tiers without discarding the rest of the config", () => {
const runtime = resolveCodexAppServerRuntimeOptions({
pluginConfig: {
appServer: {
mode: "guardian",
approvalPolicy: "on-request",
sandbox: "read-only",
serviceTier: "priority",
},
},
env: {},
});
expect(runtime).toEqual(
expect.objectContaining({
approvalPolicy: "on-request",
sandbox: "read-only",
approvalsReviewer: "guardian_subagent",
}),
);
expect(runtime).not.toHaveProperty("serviceTier");
});
it("rejects malformed plugin config instead of treating freeform strings as control values", () => {
expect(
readCodexPluginConfig({

View File

@@ -1,5 +1,6 @@
import { createHash } from "node:crypto";
import { z } from "zod";
import type { CodexServiceTier } from "./protocol.js";
export type CodexAppServerTransportMode = "stdio" | "websocket";
export type CodexAppServerPolicyMode = "yolo" | "guardian";
@@ -24,7 +25,7 @@ export type CodexAppServerRuntimeOptions = {
approvalPolicy: CodexAppServerApprovalPolicy;
sandbox: CodexAppServerSandboxMode;
approvalsReviewer: CodexAppServerApprovalsReviewer;
serviceTier?: string;
serviceTier?: CodexServiceTier;
};
export type CodexPluginConfig = {
@@ -44,7 +45,7 @@ export type CodexPluginConfig = {
approvalPolicy?: CodexAppServerApprovalPolicy;
sandbox?: CodexAppServerSandboxMode;
approvalsReviewer?: CodexAppServerApprovalsReviewer;
serviceTier?: string;
serviceTier?: CodexServiceTier | null;
};
};
@@ -73,6 +74,10 @@ const codexAppServerApprovalPolicySchema = z.enum([
]);
const codexAppServerSandboxSchema = z.enum(["read-only", "workspace-write", "danger-full-access"]);
const codexAppServerApprovalsReviewerSchema = z.enum(["user", "guardian_subagent"]);
const codexAppServerServiceTierSchema = z.preprocess(
(value) => (value === null ? null : resolveServiceTier(value)),
z.enum(["fast", "flex"]).nullable().optional(),
);
const codexPluginConfigSchema = z
.object({
@@ -96,7 +101,7 @@ const codexPluginConfigSchema = z
approvalPolicy: codexAppServerApprovalPolicySchema.optional(),
sandbox: codexAppServerSandboxSchema.optional(),
approvalsReviewer: codexAppServerApprovalsReviewerSchema.optional(),
serviceTier: z.string().optional(),
serviceTier: codexAppServerServiceTierSchema,
})
.strict()
.optional(),
@@ -127,6 +132,7 @@ export function resolveCodexAppServerRuntimeOptions(
resolvePolicyMode(config.mode) ??
resolvePolicyMode(env.OPENCLAW_CODEX_APP_SERVER_MODE) ??
"yolo";
const serviceTier = resolveServiceTier(config.serviceTier);
if (transport === "websocket" && !url) {
throw new Error(
"plugins.entries.codex.config.appServer.url is required when appServer.transport is websocket",
@@ -154,9 +160,7 @@ export function resolveCodexAppServerRuntimeOptions(
approvalsReviewer:
resolveApprovalsReviewer(config.approvalsReviewer) ??
(policyMode === "guardian" ? "guardian_subagent" : "user"),
...(readNonEmptyString(config.serviceTier)
? { serviceTier: readNonEmptyString(config.serviceTier) }
: {}),
...(serviceTier ? { serviceTier } : {}),
};
}
@@ -202,6 +206,10 @@ function resolveApprovalsReviewer(value: unknown): CodexAppServerApprovalsReview
return value === "guardian_subagent" || value === "user" ? value : undefined;
}
function resolveServiceTier(value: unknown): CodexServiceTier | undefined {
return value === "fast" || value === "flex" ? value : undefined;
}
function normalizePositiveNumber(value: unknown, fallback: number): number {
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
}

View File

@@ -1,6 +1,7 @@
export type JsonPrimitive = null | boolean | number | string;
export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
export type JsonObject = { [key: string]: JsonValue };
export type CodexServiceTier = "fast" | "flex";
export type RpcRequest = {
id?: number | string;
@@ -55,7 +56,7 @@ export type CodexThreadStartParams = {
approvalPolicy?: "never" | "on-request" | "on-failure" | "untrusted";
approvalsReviewer?: "user" | "guardian_subagent";
sandbox?: "read-only" | "workspace-write" | "danger-full-access";
serviceTier?: string | null;
serviceTier?: CodexServiceTier | null;
config?: JsonObject | null;
serviceName?: string | null;
baseInstructions?: string | null;
@@ -73,7 +74,7 @@ export type CodexThreadResumeParams = {
approvalPolicy?: "never" | "on-request" | "on-failure" | "untrusted";
approvalsReviewer?: "user" | "guardian_subagent";
sandbox?: "read-only" | "workspace-write" | "danger-full-access";
serviceTier?: string | null;
serviceTier?: CodexServiceTier | null;
baseInstructions?: string | null;
developerInstructions?: string | null;
persistExtendedHistory?: boolean;
@@ -94,7 +95,7 @@ export type CodexTurnStartParams = {
approvalPolicy?: "never" | "on-request" | "on-failure" | "untrusted";
approvalsReviewer?: "user" | "guardian_subagent";
model?: string | null;
serviceTier?: string | null;
serviceTier?: CodexServiceTier | null;
effort?: "minimal" | "low" | "medium" | "high" | "xhigh" | null;
};

View File

@@ -851,7 +851,7 @@ describe("runCodexAppServerAttempt", () => {
approvalPolicy: "on-request",
approvalsReviewer: "guardian_subagent",
sandbox: "danger-full-access",
serviceTier: "priority",
serviceTier: "fast",
},
},
});
@@ -866,7 +866,7 @@ describe("runCodexAppServerAttempt", () => {
approvalPolicy: "on-request",
approvalsReviewer: "guardian_subagent",
sandbox: "danger-full-access",
serviceTier: "priority",
serviceTier: "fast",
developerInstructions: expect.stringContaining(CODEX_GPT5_BEHAVIOR_CONTRACT),
persistExtendedHistory: true,
});
@@ -877,7 +877,7 @@ describe("runCodexAppServerAttempt", () => {
params: expect.objectContaining({
approvalPolicy: "on-request",
approvalsReviewer: "guardian_subagent",
serviceTier: "priority",
serviceTier: "fast",
model: "gpt-5.4-codex",
}),
},
@@ -885,6 +885,35 @@ describe("runCodexAppServerAttempt", () => {
);
});
it("drops invalid legacy service tiers before app-server resume and turn requests", async () => {
const sessionFile = path.join(tempDir, "session.jsonl");
const workspaceDir = path.join(tempDir, "workspace");
await writeExistingBinding(sessionFile, workspaceDir, { model: "gpt-5.2" });
const { requests, waitForMethod, completeTurn } = createResumeHarness();
const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir), {
pluginConfig: {
appServer: {
approvalPolicy: "on-request",
sandbox: "danger-full-access",
serviceTier: "priority",
},
},
});
await waitForMethod("turn/start");
await completeTurn({ threadId: "thread-existing", turnId: "turn-1" });
await run;
const resumeRequest = requests.find((request) => request.method === "thread/resume");
expect(resumeRequest?.params).toEqual(
expect.not.objectContaining({ serviceTier: expect.anything() }),
);
const turnRequest = requests.find((request) => request.method === "turn/start");
expect(turnRequest?.params).toEqual(
expect.not.objectContaining({ serviceTier: expect.anything() }),
);
});
it("builds resume and turn params from the currently selected OpenClaw model", () => {
const params = createParams("/tmp/session.jsonl", "/tmp/workspace");
const appServer = {
@@ -898,7 +927,7 @@ describe("runCodexAppServerAttempt", () => {
approvalPolicy: "on-request" as const,
approvalsReviewer: "guardian_subagent" as const,
sandbox: "danger-full-access" as const,
serviceTier: "priority",
serviceTier: "flex" as const,
};
expect(buildThreadResumeParams(params, { threadId: "thread-1", appServer })).toEqual({
@@ -908,7 +937,7 @@ describe("runCodexAppServerAttempt", () => {
approvalPolicy: "on-request",
approvalsReviewer: "guardian_subagent",
sandbox: "danger-full-access",
serviceTier: "priority",
serviceTier: "flex",
developerInstructions: expect.stringContaining(CODEX_GPT5_BEHAVIOR_CONTRACT),
persistExtendedHistory: true,
});
@@ -921,7 +950,7 @@ describe("runCodexAppServerAttempt", () => {
model: "gpt-5.4-codex",
approvalPolicy: "on-request",
approvalsReviewer: "guardian_subagent",
serviceTier: "priority",
serviceTier: "flex",
}),
);
});