Skip to content
Merged
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
71 changes: 71 additions & 0 deletions src/server/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import {
import { fetchWithResetRetry, fetchWithTransientRetry, applyUpstreamRecoveryInit } from "../lib/upstream-retry";
import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "./auth-cors";
import { listOpenAiForwardSidecarCandidates, resolveFirstUsableOpenAiSidecar, type ResolvedOpenAiForwardSidecar } from "../providers/openai-sidecar";
import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
import { applyOpenAiVirtualModel, resolveOpenAiCompactModel } from "../providers/openai-virtual-models";
import { isUsageDebugEnabled } from "../usage/debug";
import { readJsonRequestBody, DecompressedBodyTooLargeError, UnsupportedContentEncodingError } from "./request-decompress";
Expand Down Expand Up @@ -311,6 +312,61 @@ function looksLikeBackendCiphertext(payload: string): boolean {
* decryptable (backend) nor readable (model) as a whole.
*/
const FERNET_TOKEN_RUN = /gAAAA[A-Za-z0-9_-]{60,}={0,2}/g;
const FERNET_TOKEN_EXACT = /^gAAAA[A-Za-z0-9_-]{60,}={0,2}$/;

/**
* V2 agent messages carry a readable routing envelope before the actual task. The
* envelope alone is not useful to a routed provider when the task remains a ChatGPT-
* backend Fernet token. Keep arbitrary plaintext (including payload text on the same
* line) readable; only strip the exact envelope form emitted by codex-rs.
*/
function readableAgentMessagePayload(content: unknown): string {
const text = typeof content === "string"
? content
: Array.isArray(content)
? content
.flatMap(part => {
if (!part || typeof part !== "object") return [];
const record = part as { type?: unknown; text?: unknown };
return (
(record.type === "input_text" || record.type === "text" || record.type === "output_text")
&& typeof record.text === "string"
) ? [record.text] : [];
})
.join("\n")
: "";
if (!text.trim()) return "";

const envelopeStart = text.search(/(?:^|\n)Message Type\s*:/i);
if (envelopeStart >= 0) {
const marker = /(?:^|\n)Payload\s*:\s*(?:\n|$)/i.exec(text.slice(envelopeStart));
if (marker) return text.slice(envelopeStart + marker.index + marker[0].length).trim();
}
return text.trim();
}

/**
* True only for a V2 `agent_message` whose task contains a real backend-minted
* Fernet token but no provider-readable payload. Restricting inspection to the
* agent-message content means opaque reasoning signatures and compaction blobs are
* never mistaken for worker tasks.
*/
export function hasUnreadableEncryptedAgentTask(input: unknown): boolean {
if (!Array.isArray(input)) return false;
return input.some(item => {
if (!item || typeof item !== "object" || (item as { type?: unknown }).type !== "agent_message") {
return false;
}
const content = (item as { content?: unknown }).content;
const hasFernetTask = Array.isArray(content) && content.some(part => (
part && typeof part === "object"
&& (part as { type?: unknown }).type === "encrypted_content"
&& typeof (part as { encrypted_content?: unknown }).encrypted_content === "string"
&& FERNET_TOKEN_EXACT.test((part as { encrypted_content: string }).encrypted_content)
));
return hasFernetTask && readableAgentMessagePayload(content).length === 0;
});
}

/**
* Split a non-ciphertext encrypted slot into ordered parts: prose becomes input_text,
Expand Down Expand Up @@ -866,6 +922,21 @@ export async function handleResponses(
return formatErrorResponse(404, "invalid_request_error", err instanceof Error ? err.message : String(err));
}

// ChatGPT can decrypt backend-minted V2 task tokens; external routed providers
// cannot. Stop before auth resolution, adapter construction, or network dispatch so
// a deterministic delivery mismatch cannot become a retry/provider-cost storm. The
// response deliberately contains no ciphertext or request-derived text.
const nativeChatGptRoute = isCanonicalOpenAiForwardProvider(route.provider);
if (!nativeChatGptRoute && hasUnreadableEncryptedAgentTask(
(body as { input?: unknown } | undefined)?.input,
)) {
return formatErrorResponse(
400,
"invalid_request_error",
"Routed V2 worker task is encrypted for the native ChatGPT backend and cannot be read by the selected provider. Use plaintext V2 agent-message delivery or select a native ChatGPT model.",
);
}

// Apply the routed model id upstream: routing may strip a "<provider>/" namespace
// (e.g. "opencode-go/deepseek-v4-pro" → "deepseek-v4-pro"). Adapters read parsed.modelId,
// and the passthrough adapter serializes _rawBody, so rewrite both.
Expand Down
145 changes: 145 additions & 0 deletions tests/v2-agent-message-failfast.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { afterEach, describe, expect, test } from "bun:test";
import { handleResponses, hasUnreadableEncryptedAgentTask, sanitizeEncryptedContentInPlace } from "../src/server/responses";
import type { OcxConfig } from "../src/types";

const originalFetch = globalThis.fetch;
const FERNET_TASK = `gAAAA${"Ab1_-".repeat(20)}==`;
const ROUTING_ENVELOPE = [
"Message Type: NEW_TASK",
"Task name: /root/worker",
"Sender: /root",
"Payload:",
"",
].join("\n");

afterEach(() => {
globalThis.fetch = originalFetch;
});

function workerInput(extraContent: Array<Record<string, unknown>> = []): unknown[] {
return [
{
type: "agent_message",
author: "/root",
recipient: "/root/worker",
content: [
{ type: "input_text", text: ROUTING_ENVELOPE },
...extraContent,
{ type: "encrypted_content", encrypted_content: FERNET_TASK },
],
},
];
}

function routedConfig(): OcxConfig {
return {
port: 0,
defaultProvider: "xai",
providers: {
xai: {
adapter: "openai-chat",
baseUrl: "https://api.x.ai/v1",
authMode: "key",
apiKey: "test-xai-key",
},
},
} as OcxConfig;
}

function nativeConfig(): OcxConfig {
return {
port: 0,
defaultProvider: "openai",
providers: {
openai: {
adapter: "openai-responses",
baseUrl: "https://chatgpt.com/backend-api/codex",
authMode: "forward",
codexAccountMode: "direct",
},
},
} as OcxConfig;
}

async function post(config: OcxConfig, model: string, input: unknown[], headers: HeadersInit = {}): Promise<Response> {
return handleResponses(new Request("http://localhost/v1/responses", {
method: "POST",
headers: { "content-type": "application/json", ...Object.fromEntries(new Headers(headers)) },
body: JSON.stringify({ model, input, stream: false }),
}), config, { model: "", provider: "" });
}

describe("V2 routed agent-message ciphertext guard", () => {
test("recognizes Fernet-only task delivery but ignores readable and unrelated encrypted content", () => {
expect(hasUnreadableEncryptedAgentTask(workerInput())).toBe(true);
expect(hasUnreadableEncryptedAgentTask([{
type: "agent_message",
content: [
{ type: "input_text", text: `[CXC-LEAF-GUARD] obey the worker boundary.\n${ROUTING_ENVELOPE}` },
{ type: "encrypted_content", encrypted_content: FERNET_TASK },
],
}])).toBe(true);
expect(hasUnreadableEncryptedAgentTask(workerInput([
{ type: "input_text", text: "Implement the focused regression test." },
]))).toBe(false);
expect(hasUnreadableEncryptedAgentTask([
{ type: "reasoning", encrypted_content: FERNET_TASK, summary: [] },
{ type: "message", role: "user", content: [{ type: "input_text", text: "continue" }] },
])).toBe(false);
});

test("plaintext parked in the encrypted slot still follows the compatibility sanitizer", () => {
const input = [{
type: "agent_message",
content: [
{ type: "input_text", text: ROUTING_ENVELOPE },
{ type: "encrypted_content", encrypted_content: "Build the requested artifact." },
],
}];
expect(sanitizeEncryptedContentInPlace(input)).toBe(1);
expect(hasUnreadableEncryptedAgentTask(input)).toBe(false);
expect(input[0]).toMatchObject({ type: "message", role: "user" });
});

test("routed Fernet-only task fails as a non-retryable 400 before provider dispatch", async () => {
let fetchCalls = 0;
globalThis.fetch = (async () => {
fetchCalls += 1;
throw new Error("provider dispatch must not happen");
}) as typeof fetch;

const response = await post(routedConfig(), "xai/grok-4.5", workerInput());
const raw = await response.text();
const json = JSON.parse(raw) as { error?: { type?: string; code?: string; message?: string } };
expect(response.status).toBe(400);
expect(json.error).toMatchObject({
type: "invalid_request_error",
code: "invalid_request_error",
});
expect(json.error?.message).toContain("plaintext V2 agent-message delivery");
expect(fetchCalls).toBe(0);
expect(raw).not.toContain(FERNET_TASK);
expect(raw).not.toContain("gAAAA");
});

test("native ChatGPT route keeps the ciphertext for backend decryption", async () => {
let forwardedBody = "";
globalThis.fetch = (async (_input, init) => {
forwardedBody = typeof init?.body === "string" ? init.body : "";
return new Response(JSON.stringify({
id: "resp_native",
object: "response",
status: "completed",
model: "gpt-5.5",
output: [],
usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 },
}), { status: 200, headers: { "content-type": "application/json" } });
}) as typeof fetch;

const response = await post(nativeConfig(), "gpt-5.5", workerInput(), {
authorization: "Bearer caller-codex-token",
});
expect(response.status).toBe(200);
expect(forwardedBody).toContain(FERNET_TASK);
});
});
Loading