From d1da9bed6c47e32c7aefe0a648077ee4fb4b511d Mon Sep 17 00:00:00 2001 From: R <53855466+cb8010d6@users.noreply.github.com> Date: Fri, 4 Sep 2026 16:16:44 +0800 Subject: [PATCH 1/3] feat(providers): allow direct encrypted V2 task passthrough --- .../docs/reference/configuration/providers.md | 1 + src/config.ts | 1 + src/server/auth-cors.ts | 1 + src/server/responses/core.ts | 42 +++++++++++++++++-- src/types/provider.ts | 6 +++ tests/server/agent-task-recovery.test.ts | 28 +++++++++++++ 6 files changed, 75 insertions(+), 4 deletions(-) diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index 327cb3416b..b1020d53f9 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -108,6 +108,7 @@ predictions. Explicit provider/model price overrides still take precedence. | `requestPacing?` | `{ enabled, requestsPerMinute?, minIntervalMs?, models? }` | Optional client-side outbound request-start pacing, separate from upstream usage, billing, and rate-limit indicators. RPM is converted to an even interval; `minIntervalMs` may impose a longer interval. Provider limits apply across all models, while `models` entries use exact upstream model IDs (for example `nvidia/llama-3.1-nemotron-ultra-253b-v1`) and can only add delay. Queue waits do not consume the upstream response-header timeout. HTTP, Responses WebSocket, and explicit adapter `fetchResponse`/`runTurn` dispatches are covered. | | `upstreamHttpVersion?` | `"auto" \| "http1.1" \| "h1" \| "http2" \| "h2"` | Pin the HTTP version used for upstream requests to this provider. Defaults to `auto`, which lets Bun negotiate. An explicit pin requires an HTTPS target and fails locally when it cannot be honored. Set `http1.1` when a provider's HTTP/2 SSE stream stalls instead of delivering events — the symptom is a long-running streaming request that produces nothing and eventually times out. For Cursor, `http1.1`/`h1` selects its `RunSSE` + `BidiAppend` compatibility transport for inference and also pins live model discovery. Management `POST`/`PATCH` accept `null` to clear it back to `auto`. | | `responsesPath?` | `string` | Relative resource path for key-auth `openai-responses` requests. It must start with `/` and contain no scheme, query, or fragment. | +| `allowEncryptedV2AgentTasks?` | `boolean` | Disabled by default. Trust a direct key-auth `openai-responses` provider to consume or relay opaque encrypted V2 sub-agent tasks unchanged. Eligible routes skip `agentTaskRecovery`; all other routes keep the existing recovery or fail-closed behavior. OpenCodex does not decrypt, translate, or recover tasks sent through this opt-in. | | `upstreamWebsocket?` | `boolean` | Opt-in upstream Responses WebSocket transport for `openai-responses` requests (default false). When the upstream supports the Responses WebSocket protocol, streaming POST requests to the configured Responses path (default `/v1/responses`) are dialed as WSS over an HTTPS base URL and re-encoded to SSE for the usual pipeline. Forward providers use `{baseUrl}/responses`; key-auth providers use `responsesPath`, or the legacy `/v1/responses` fallback. This mirrors the canonical ChatGPT backend optimization for OpenAI-compatible gateways (for example sub2api) whose WebSocket ingress is measurably faster than its SSE queue. Plain HTTP remains on SSE; non-Responses paths and `openai-chat` requests stay on HTTP. | | `supportsServiceTier?` | `boolean` | Tri-state canonical Fast capability fallback. `true` publishes Fast in the catalog, satisfies service-tier routing requirements, contributes a supported fingerprint, and lets fast mode inject the provider's canonical wire value on a compatible final adapter. `false` strips the field and never injects, and exact model declarations cannot reopen it. Absent leaves the provider unclassified: fast mode does not inject or normalize a canonical caller value, and caller values obey the final wire's forwarding permission (`chatServiceTier` on Chat; passthrough on Responses). The registry classifies canonical OpenAI (`true`), DeepSeek, and Volcengine Ark (`false`); set it explicitly only for custom gateways that genuinely support tiers. | | `modelSupportsServiceTier?` | `Record` | Exact upstream model capability overrides. Exact `true` enables canonical Fast for that model; exact `false` narrows provider defaults. An explicit provider-level `supportsServiceTier: false` remains fail-closed and cannot be reopened. Exact `true` does not authorize foreign caller-tier forwarding on Chat. Undeclared models fall back to provider-wide behavior. Management `PATCH /api/providers` merges entries and accepts `null` to clear one. | diff --git a/src/config.ts b/src/config.ts index 2a1af85020..11da839570 100644 --- a/src/config.ts +++ b/src/config.ts @@ -537,6 +537,7 @@ const providerConfigSchema = z.object({ modelSupportsServiceTier: z.record(z.string().min(1), z.boolean()).optional(), preserveResponsesReasoningContent: z.boolean().optional(), decodesNativeCompactionBlobs: z.boolean().optional(), + allowEncryptedV2AgentTasks: z.boolean().optional(), allowPrivateNetwork: z.boolean().optional(), // The management API accepts `null` as "clear this", so a config written before the POST // canonicalization below can hold one on disk. Rejecting it here would send the operator diff --git a/src/server/auth-cors.ts b/src/server/auth-cors.ts index cc8f342943..c0c7b77fd1 100644 --- a/src/server/auth-cors.ts +++ b/src/server/auth-cors.ts @@ -784,6 +784,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = { modelSupportsServiceTier: "editor", preserveResponsesReasoningContent: "editor", decodesNativeCompactionBlobs: "editor", + allowEncryptedV2AgentTasks: "editor", allowPrivateNetwork: "editor", upstreamHttpVersion: "editor", upstreamWebsocket: "editor", diff --git a/src/server/responses/core.ts b/src/server/responses/core.ts index 0c67a834b1..db58707983 100644 --- a/src/server/responses/core.ts +++ b/src/server/responses/core.ts @@ -1758,6 +1758,31 @@ function unreadableEncryptedAgentTaskResponse(): Response { ); } +/** + * Keep this trust boundary deliberately narrow: only a key-auth Responses route may consume + * opaque child-task ciphertext, and the model's final wire override must still be Responses. + * Callers keep combo attempts on their existing native-only recovery/fail-closed behavior. + */ +function canPassThroughEncryptedV2AgentTask( + route: RouteResult, + inboundWire: InboundWire, +): boolean { + const provider = route.provider; + if ( + inboundWire !== "responses" + || provider.allowEncryptedV2AgentTasks !== true + || provider.adapter !== "openai-responses" + || (provider.authMode ?? "key") !== "key" + ) return false; + + return resolveWireProtocolOverride( + route.providerName, + route.modelId, + provider, + inboundWire, + ).adapter === "openai-responses"; +} + type ResponsesAuthResolution = | { ok: true; authCtx: CodexAuthContext; headers: Headers; substituteMainCredential: boolean } | { ok: false; response: Response }; @@ -3086,7 +3111,8 @@ async function handleResponsesInner( previewSelectionAdmission?.release(); } - // Native fallback can consume ciphertext, so recover only after final route selection. + // Native fallback and explicitly trusted direct Responses routes can consume ciphertext, + // so recover only after final route selection. if ( inboundWire === "responses" && @@ -3095,6 +3121,7 @@ async function handleResponsesInner( && agentTaskRecovery && !isCanonicalOpenAiForwardProvider(route.provider) && !options.comboAttempt + && !canPassThroughEncryptedV2AgentTask(route, inboundWire) ) { let recovered = false; try { @@ -3218,9 +3245,16 @@ async function handleResponsesInner( if (options.abortSignal?.aborted) return clientCancelledResponse(); - // Encrypted child tasks may only reach the canonical native backend. This check - // runs against the FINAL route so native-only fallback can rescue a routed primary. - if (!isCanonicalOpenAiForwardProvider(route.provider) && unreadableEncryptedAgentTask) { + // Encrypted child tasks may reach the canonical native backend or an explicitly trusted + // direct Responses route. This runs against the FINAL route so native-only fallback can + // rescue an incompatible primary without weakening combo behavior. + const finalRouteCanPassThroughEncryptedTask = !options.comboAttempt + && canPassThroughEncryptedV2AgentTask(route, inboundWire); + if ( + !isCanonicalOpenAiForwardProvider(route.provider) + && !finalRouteCanPassThroughEncryptedTask + && unreadableEncryptedAgentTask + ) { return unreadableEncryptedAgentTaskResponse(); } diff --git a/src/types/provider.ts b/src/types/provider.ts index 459634fd16..5e9ada7235 100644 --- a/src/types/provider.ts +++ b/src/types/provider.ts @@ -270,6 +270,12 @@ export interface OcxProviderConfig { * compaction blobs. Absent or false degrades foreign blobs to an opaque note. */ decodesNativeCompactionBlobs?: boolean; + /** + * Trust this direct key-auth Responses provider to consume or relay opaque encrypted + * V2 agent tasks. OpenCodex does not decrypt, translate, or recover an eligible task. + * Absent or false keeps the existing recovery/fail-closed behavior. + */ + allowEncryptedV2AgentTasks?: boolean; /** * Explicit opt-in for non-registry private-network destinations such as localhost, RFC1918, * link-local, or unique-local upstreams. Metadata endpoints remain blocked. diff --git a/tests/server/agent-task-recovery.test.ts b/tests/server/agent-task-recovery.test.ts index e90b2bf988..f8eb2662fa 100644 --- a/tests/server/agent-task-recovery.test.ts +++ b/tests/server/agent-task-recovery.test.ts @@ -146,6 +146,34 @@ describe("agent task recovery (opt-in, default off)", () => { expect(fetchedUrls[0]).toContain("chatgpt.com/backend-api/codex"); }); + test("trusted direct Responses routes bypass recovery and preserve encrypted tasks", async () => { + const config = routedConfig(); + config.providers.relay = { + adapter: "openai-responses", + baseUrl: "https://relay.example.test/v1", + authMode: "key", + apiKey: "test-relay-key", + allowEncryptedV2AgentTasks: true, + }; + const input = encryptedInput(); + const fetchedUrls: string[] = []; + let forwardedInput: unknown; + globalThis.fetch = (async (url, init) => { + fetchedUrls.push(String(url)); + const body = JSON.parse(String(init?.body)) as { input?: unknown }; + forwardedInput = body.input; + return providerResponse(); + }) as typeof fetch; + + const response = await post(config, "relay/gpt-5.6-luna", input, codexHeaders()); + + expect(response.status).toBe(200); + expect(fetchedUrls).toHaveLength(1); + expect(fetchedUrls[0]).toContain("relay.example.test"); + expect(fetchedUrls[0]).not.toContain("chatgpt.com"); + expect(forwardedInput).toEqual(input); + }); + test("authenticated ChatGPT recovery accepts the decrypted payload without a duplicated routing envelope", async () => { const assignment = "Implement the focused regression test."; const fetchedUrls: string[] = []; From 0cc829098d1fb3120d2e475c711ee33d5f30876a Mon Sep 17 00:00:00 2001 From: R <53855466+cb8010d6@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:58:54 +0800 Subject: [PATCH 2/3] fix(providers): honor final Responses adapter for V2 passthrough --- src/server/responses/core.ts | 1 - tests/server/agent-task-recovery.test.ts | 71 ++++++++++++++++++------ 2 files changed, 54 insertions(+), 18 deletions(-) diff --git a/src/server/responses/core.ts b/src/server/responses/core.ts index db58707983..8d0faa12cb 100644 --- a/src/server/responses/core.ts +++ b/src/server/responses/core.ts @@ -1771,7 +1771,6 @@ function canPassThroughEncryptedV2AgentTask( if ( inboundWire !== "responses" || provider.allowEncryptedV2AgentTasks !== true - || provider.adapter !== "openai-responses" || (provider.authMode ?? "key") !== "key" ) return false; diff --git a/tests/server/agent-task-recovery.test.ts b/tests/server/agent-task-recovery.test.ts index f8eb2662fa..f1f11ae06b 100644 --- a/tests/server/agent-task-recovery.test.ts +++ b/tests/server/agent-task-recovery.test.ts @@ -147,31 +147,68 @@ describe("agent task recovery (opt-in, default off)", () => { }); test("trusted direct Responses routes bypass recovery and preserve encrypted tasks", async () => { - const config = routedConfig(); + for (const adapterConfig of [ + { adapter: "openai-responses" as const }, + { + adapter: "openai-chat" as const, + modelAdapters: { "gpt-5.6-luna": "openai-responses" }, + }, + ]) { + const config = routedConfig(); + config.providers.relay = { + ...adapterConfig, + baseUrl: "https://relay.example.test/v1", + authMode: "key", + apiKey: "test-relay-key", + allowEncryptedV2AgentTasks: true, + }; + const input = encryptedInput(); + const fetchedUrls: string[] = []; + let forwardedInput: unknown; + globalThis.fetch = (async (url, init) => { + fetchedUrls.push(String(url)); + const body = JSON.parse(String(init?.body)) as { input?: unknown }; + forwardedInput = body.input; + return providerResponse(); + }) as typeof fetch; + + const response = await post(config, "relay/gpt-5.6-luna", input, codexHeaders()); + + expect(response.status).toBe(200); + expect(fetchedUrls).toHaveLength(1); + expect(fetchedUrls[0]).toContain("relay.example.test"); + expect(fetchedUrls[0]).not.toContain("chatgpt.com"); + expect(forwardedInput).toEqual(input); + } + }); + + test.each([ + ["OAuth authentication", { adapter: "openai-responses" as const, authMode: "oauth" as const }], + ["a Chat Completions adapter", { adapter: "openai-chat" as const }], + ["a model-level Chat override", { + adapter: "openai-responses" as const, + modelAdapters: { "gpt-5.6-luna": "openai-chat" }, + }], + ])("trusted passthrough stays fail closed for %s", async (_case, providerConfig) => { + const config = routedConfig(null); config.providers.relay = { - adapter: "openai-responses", + ...providerConfig, baseUrl: "https://relay.example.test/v1", - authMode: "key", apiKey: "test-relay-key", allowEncryptedV2AgentTasks: true, }; - const input = encryptedInput(); - const fetchedUrls: string[] = []; - let forwardedInput: unknown; - globalThis.fetch = (async (url, init) => { - fetchedUrls.push(String(url)); - const body = JSON.parse(String(init?.body)) as { input?: unknown }; - forwardedInput = body.input; - return providerResponse(); + let fetchCalls = 0; + globalThis.fetch = (async () => { + fetchCalls += 1; + throw new Error("ineligible encrypted tasks must not reach an upstream"); }) as typeof fetch; - const response = await post(config, "relay/gpt-5.6-luna", input, codexHeaders()); + const response = await post(config, "relay/gpt-5.6-luna", encryptedInput(), codexHeaders()); + const json = await response.json() as { error?: { code?: string } }; - expect(response.status).toBe(200); - expect(fetchedUrls).toHaveLength(1); - expect(fetchedUrls[0]).toContain("relay.example.test"); - expect(fetchedUrls[0]).not.toContain("chatgpt.com"); - expect(forwardedInput).toEqual(input); + expect(response.status).toBe(400); + expect(json.error?.code).toBe("unreadable_encrypted_agent_task"); + expect(fetchCalls).toBe(0); }); test("authenticated ChatGPT recovery accepts the decrypted payload without a duplicated routing envelope", async () => { From e2c9a6672694420ebd6d4307a493bef3b7f5e6ab Mon Sep 17 00:00:00 2001 From: R <53855466+cb8010d6@users.noreply.github.com> Date: Sat, 5 Sep 2026 08:31:48 +0800 Subject: [PATCH 3/3] test(providers): keep encrypted passthrough opt-in inert in combos --- .../server/agent-task-recovery-combo.test.ts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/server/agent-task-recovery-combo.test.ts b/tests/server/agent-task-recovery-combo.test.ts index 19a2f63317..5caa7f7691 100644 --- a/tests/server/agent-task-recovery-combo.test.ts +++ b/tests/server/agent-task-recovery-combo.test.ts @@ -172,6 +172,35 @@ describe("combo path encrypted agent task recovery", () => { expect(providerFetches).toBe(1); }); + test("keeps an opted-in Responses target out of encrypted combo dispatch", async () => { + const config = comboConfig([ + { provider: "relay", model: "relay-model" }, + { provider: "openai", model: "gpt-5.5" }, + ]); + config.providers.relay = { + adapter: "openai-responses", + baseUrl: "https://relay.example.test/v1", + authMode: "key", + apiKey: "test-relay-key", + allowEncryptedV2AgentTasks: true, + }; + const fetchedUrls: string[] = []; + const forwardedBodies: string[] = []; + globalThis.fetch = (async (input, init) => { + fetchedUrls.push(String(input)); + forwardedBodies.push(typeof init?.body === "string" ? init.body : ""); + return providerResponse(); + }) as typeof fetch; + + const response = await post(config, "combo/routed", encryptedInput(), codexHeaders()); + + expect(response.status).toBe(200); + expect(fetchedUrls).toEqual(["https://chatgpt.com/backend-api/codex/responses"]); + expect(forwardedBodies).toHaveLength(1); + expect(forwardedBodies[0]).toContain(FERNET_TASK); + expect(forwardedBodies[0]).not.toContain("capture_assignment"); + }); + test("keeps the canonical target bypass in a mixed combo without running recovery", async () => { const forwardedBodies: string[] = []; globalThis.fetch = (async (_input, init) => {