diff --git a/CHANGELOG.md b/CHANGELOG.md index d7f5db737f5..3b7aacabc9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/docs/plugins/codex-harness.md b/docs/plugins/codex-harness.md index dbd4dc684fc..3ce12bff8b9 100644 --- a/docs/plugins/codex-harness.md +++ b/docs/plugins/codex-harness.md @@ -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: diff --git a/extensions/codex/openclaw.plugin.json b/extensions/codex/openclaw.plugin.json index f424c61f8a1..201474a6a3f 100644 --- a/extensions/codex/openclaw.plugin.json +++ b/extensions/codex/openclaw.plugin.json @@ -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 } } diff --git a/extensions/codex/src/app-server/config.test.ts b/extensions/codex/src/app-server/config.test.ts index 50b2e0cb9b9..0de8e9c3f0c 100644 --- a/extensions/codex/src/app-server/config.test.ts +++ b/extensions/codex/src/app-server/config.test.ts @@ -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({ diff --git a/extensions/codex/src/app-server/config.ts b/extensions/codex/src/app-server/config.ts index 023b348d8ce..c556479a032 100644 --- a/extensions/codex/src/app-server/config.ts +++ b/extensions/codex/src/app-server/config.ts @@ -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; } diff --git a/extensions/codex/src/app-server/protocol.ts b/extensions/codex/src/app-server/protocol.ts index fba166eeee5..80d34c7f159 100644 --- a/extensions/codex/src/app-server/protocol.ts +++ b/extensions/codex/src/app-server/protocol.ts @@ -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; }; diff --git a/extensions/codex/src/app-server/run-attempt.test.ts b/extensions/codex/src/app-server/run-attempt.test.ts index 3ed3f8f32e9..3f573e1eb04 100644 --- a/extensions/codex/src/app-server/run-attempt.test.ts +++ b/extensions/codex/src/app-server/run-attempt.test.ts @@ -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", }), ); });