Client or integration
Codex App
Area
Proxy and routing
Summary
A routed sub-agent returned an ordinary 195-character string through collaboration.send_message.message. It began with gAAAA... and used only Base64-compatible characters, but it was model-generated text rather than backend-minted ciphertext. The Codex client later replayed that value inside agent_message.content[].encrypted_content.
On the next native ChatGPT Responses request, OpenCodex preserved the value because looksLikeBackendCiphertext() currently accepts any string whose length is at least 64 and whose characters match /^[A-Za-z0-9+/=_-]+$/. The ChatGPT backend rejected the unauthentic value with Encrypted function output content could not be decrypted or decoded. The existing opaque-blob recovery did not sanitize and replay this WebSocket failure (sendCount=1, recoveryKinds=[]), so a later turn in the same thread failed again.
Expected: agent-originated plaintext must not be preserved as backend ciphertext solely from its appearance. If an unverifiable agent-message ciphertext still reaches the backend, the exact decrypt rejection should trigger one bounded sanitize-and-retry attempt instead of leaving the thread poisoned.
Observed on installed OpenCodex 2.59.0. The relevant sanitizer and recovery logic is still present in development version 2.60.0 at commit e64d699.
Reproduction
Deterministic unit-level reproduction
Construct an agent_message with a deliberately invalid 195-character Base64-like payload:
const fake = `gAAAAA${"A".repeat(189)}`;
const input = [{
type: "agent_message",
author: "/root/test",
content: [{
type: "encrypted_content",
encrypted_content: fake,
}],
}];
expect(fake.length).toBe(195);
expect(looksLikeBackendCiphertext(fake)).toBe(true);
// Current behavior: the part is preserved as ciphertext.
expect(sanitizeEncryptedContentInPlace(input)).toBe(0);
expect(input[0].content[0].type).toBe("encrypted_content");
The existing key-independent Fernet validator rejects this value because it is not a canonical Fernet wire token. Authenticity still cannot be established from structure alone, so provenance should be the primary trust boundary.
Observed integration path
- Run OpenCodex 2.59.0 with Codex App, a native ChatGPT parent (
gpt-5.6-sol), and a routed openai-chat child.
- Have the routed child call
collaboration.send_message with an ordinary long alphanumeric message beginning with gAAAA....
- The Codex client persists/replays that message as
agent_message.content[].encrypted_content.
- Send the next turn through the native ChatGPT Responses route.
- Observe HTTP 502 / stream termination with
Encrypted function output content could not be decrypted or decoded.
- Observe no opaque-blob recovery (
sendCount=1, recoveryKinds=[]).
- Send another user turn in the same thread and observe the same failure because the invalid part remains in replay history.
No real ciphertext, account identifier, token, API key, private prompt, or request credential is required for this reproduction.
Version
Installed: 2.59.0; also inspected dev 2.60.0 at e64d699
Operating system
macOS 15.8 (24H23)
Provider and model
Native openai / gpt-5.6-sol parent; routed openai-chat / Grok 4.6 child
Logs or error output
HTTP 502
stream disconnected before completion: Encrypted function output content could not be decrypted or decoded.
sendCount=1
recoveryKinds=[]
Screenshots and supporting files
Relevant current code:
src/server/responses/encrypted-payload.ts:100-102: looksLikeBackendCiphertext() accepts any sufficiently long Base64-like string.
src/server/responses/encrypted-payload.ts:483-545: sanitizeEncryptedContentInPlace() preserves a matching encrypted_content part.
src/server/responses/request-prepare.ts:272-285: the sanitizer runs before native passthrough and intentionally preserves values classified as backend ciphertext.
src/server/responses/core-opaque-recovery.ts:180-198: the exact 502 is eligible only when the outbound body is detected as carrying encrypted function/agent output.
src/server/responses/passthrough-dispatch.ts:1448-1500: streamed recovery watches error, response.failed, and response.incomplete; the observed WebSocket failure did not enter this recovery path.
Related but not duplicate:
- #3021 fixed genuine encrypted sub-agent
MESSAGE content being surfaced as visible gAAAA... text after adapter_eof. This report is the inverse direction: agent-generated plaintext is mistaken for ciphertext and sent to the native backend.
- #4454 covered mixed encrypted
agent_message history reaching xAI and being rejected as an unknown item type. This report fails on the native ChatGPT path with a decrypt/decode 502.
- #3661 concerns genuine native ciphertext that a routed V2 worker cannot recover. This report concerns unauthentic agent text sent to the native backend.
- #4191 tracks generic mid-turn WebSocket fallback. It does not cover ciphertext classification or poisoned
agent_message history.
- PR #3535 targeted the same exact rejection message with sanitize-and-rebuild recovery, but it was closed without merge and did not address this false-positive classification trigger.
Suggested regression coverage:
- A 195-character Base64-like
agent_message payload remains plaintext or is omitted; it is never trusted as ciphertext from shape alone.
- Genuine backend-minted ciphertext from trusted provenance remains byte-identical.
- A WebSocket
error, server_error, response.failed, or terminal close carrying the exact decrypt rejection triggers at most one sanitize-and-retry.
- A sanitized retry cannot loop indefinitely.
- Subsequent turns do not resend the rejected part as ciphertext.
Redacted configuration
{
"defaultProvider": "openai",
"providers": {
"routed-child": {
"adapter": "openai-chat",
"baseUrl": "<redacted>",
"apiKey": "<redacted>"
}
}
}
Checks
Client or integration
Codex App
Area
Proxy and routing
Summary
A routed sub-agent returned an ordinary 195-character string through
collaboration.send_message.message. It began withgAAAA...and used only Base64-compatible characters, but it was model-generated text rather than backend-minted ciphertext. The Codex client later replayed that value insideagent_message.content[].encrypted_content.On the next native ChatGPT Responses request, OpenCodex preserved the value because
looksLikeBackendCiphertext()currently accepts any string whose length is at least 64 and whose characters match/^[A-Za-z0-9+/=_-]+$/. The ChatGPT backend rejected the unauthentic value withEncrypted function output content could not be decrypted or decoded.The existing opaque-blob recovery did not sanitize and replay this WebSocket failure (sendCount=1,recoveryKinds=[]), so a later turn in the same thread failed again.Expected: agent-originated plaintext must not be preserved as backend ciphertext solely from its appearance. If an unverifiable agent-message ciphertext still reaches the backend, the exact decrypt rejection should trigger one bounded sanitize-and-retry attempt instead of leaving the thread poisoned.
Observed on installed OpenCodex 2.59.0. The relevant sanitizer and recovery logic is still present in development version 2.60.0 at commit
e64d699.Reproduction
Deterministic unit-level reproduction
Construct an
agent_messagewith a deliberately invalid 195-character Base64-like payload:The existing key-independent Fernet validator rejects this value because it is not a canonical Fernet wire token. Authenticity still cannot be established from structure alone, so provenance should be the primary trust boundary.
Observed integration path
gpt-5.6-sol), and a routedopenai-chatchild.collaboration.send_messagewith an ordinary long alphanumeric message beginning withgAAAA....agent_message.content[].encrypted_content.Encrypted function output content could not be decrypted or decoded.sendCount=1,recoveryKinds=[]).No real ciphertext, account identifier, token, API key, private prompt, or request credential is required for this reproduction.
Version
Installed: 2.59.0; also inspected dev 2.60.0 at e64d699
Operating system
macOS 15.8 (24H23)
Provider and model
Native openai / gpt-5.6-sol parent; routed openai-chat / Grok 4.6 child
Logs or error output
Screenshots and supporting files
Relevant current code:
src/server/responses/encrypted-payload.ts:100-102:looksLikeBackendCiphertext()accepts any sufficiently long Base64-like string.src/server/responses/encrypted-payload.ts:483-545:sanitizeEncryptedContentInPlace()preserves a matchingencrypted_contentpart.src/server/responses/request-prepare.ts:272-285: the sanitizer runs before native passthrough and intentionally preserves values classified as backend ciphertext.src/server/responses/core-opaque-recovery.ts:180-198: the exact 502 is eligible only when the outbound body is detected as carrying encrypted function/agent output.src/server/responses/passthrough-dispatch.ts:1448-1500: streamed recovery watcheserror,response.failed, andresponse.incomplete; the observed WebSocket failure did not enter this recovery path.Related but not duplicate:
MESSAGEcontent being surfaced as visiblegAAAA...text afteradapter_eof. This report is the inverse direction: agent-generated plaintext is mistaken for ciphertext and sent to the native backend.agent_messagehistory reaching xAI and being rejected as an unknown item type. This report fails on the native ChatGPT path with a decrypt/decode 502.agent_messagehistory.Suggested regression coverage:
agent_messagepayload remains plaintext or is omitted; it is never trusted as ciphertext from shape alone.error,server_error,response.failed, or terminal close carrying the exact decrypt rejection triggers at most one sanitize-and-retry.Redacted configuration
{ "defaultProvider": "openai", "providers": { "routed-child": { "adapter": "openai-chat", "baseUrl": "<redacted>", "apiKey": "<redacted>" } } }Checks