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
16 changes: 16 additions & 0 deletions docs-site/src/content/docs/guides/sub-agent-surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,22 @@ Recovery options are to select a native ChatGPT child, add a native ChatGPT targ
v1 for heterogeneous-provider delegation, or resend the task as plaintext v2 `agent_message`
content when you control the caller.

An experimental, disabled-by-default `agentTaskRecovery` option can recover this specific native-
to-routed shape through a raw Responses passthrough to the fixed ChatGPT `/responses` endpoint using
the incoming credential shape used by the canonical `openai` provider with `authMode: "forward"`.
Recovery is available only while the proxy is bound to loopback. It never substitutes API-key
authentication, another provider credential, or another Codex account. Only `authorization`, matching
`chatgpt-account-id`, `originator`, and optional `openai-beta`/`user-agent` metadata are forwarded;
`content-type` and `accept` are generated locally, and no other caller headers cross the boundary.
It consumes quota, adds latency, briefly retains recovered plaintext in a bounded in-memory cache,
and depends on undocumented ChatGPT backend behavior. Because a model returns the recovered text,
byte-for-byte fidelity is not guaranteed. It rejects generic/API-key proxy callers and preserves
`unreadable_encrypted_agent_task` on any failure. See
[Agent configuration: Encrypted v2 task recovery](/reference/configuration/agents/#encrypted-v2-task-recovery)
for the full trust boundary and configuration.
Combo routing remains unchanged and continues to consider only canonical native ChatGPT targets for
encrypted tasks.

## Changing the mode

### GUI
Expand Down
2 changes: 2 additions & 0 deletions docs-site/src/content/docs/ja/guides/sub-agent-surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ opencodex は、空のタスクまたは読み取り不可能なタスクを転

回復オプションは、ネイティブ ChatGPT 子の選択、コンボへのネイティブ ChatGPT ターゲットの追加、異種プロバイダーの委任に v1 を使用する、または呼び出し元を制御するときにタスクをプレーンテキスト v2 `agent_message` コンテンツとして再送信することです。

実験的な `agentTaskRecovery` はデフォルトで無効です。明示的に有効にすると、固定された ChatGPT エンドポイントへの追加の認証済みリクエストでこの形式を回復できますが、クォータと待ち時間が増え、非公開のバックエンド動作に依存します。失敗時は従来の `unreadable_encrypted_agent_task` を維持します。詳細は[英語版の設定リファレンス](/reference/configuration/agents/#encrypted-v2-task-recovery)を参照してください。

## モードを変更する

### GUI
Expand Down
2 changes: 2 additions & 0 deletions docs-site/src/content/docs/ko/guides/sub-agent-surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ opencodex는 읽을 수 없거나 빈 작업을 그대로 넘기지 않고 안

복구 방법은 네이티브 ChatGPT 자식을 선택하거나, 콤보에 네이티브 ChatGPT 대상을 추가하거나, 이종 프로바이더 위임에는 v1을 사용하거나, 호출자를 제어할 수 있을 때 작업을 평문 v2 `agent_message` 콘텐츠로 다시 보내는 것입니다.

실험적인 `agentTaskRecovery`는 기본적으로 꺼져 있습니다. 명시적으로 켜면 고정된 ChatGPT 엔드포인트로 인증된 요청을 하나 더 보내 이 형식을 복구할 수 있지만, 할당량과 지연 시간이 늘고 비공개 백엔드 동작에 의존합니다. 실패하면 기존 `unreadable_encrypted_agent_task` 오류를 그대로 유지합니다. 자세한 내용은 [영문 설정 참고 문서](/reference/configuration/agents/#encrypted-v2-task-recovery)를 보세요.

## 모드 변경

### GUI
Expand Down
72 changes: 72 additions & 0 deletions docs-site/src/content/docs/reference/configuration/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ routes, and limits delegated work.
| `subagentModelFallbackPollMs?` | `number` | `60000` | Availability-probe cache interval. Values below 1000 ms fall back to the default. |
| `effortCap?` | `string` | — | Hard ceiling for qualifying v2 main turns and marked spawned-child turns. Accepts `low` through `ultra`. |
| `subagentEffortCap?` | `string` | — | Additional ceiling for spawned-child turns only. When both caps apply, the lower wins. |
| `agentTaskRecovery?` | `object` | — | Experimental opt-in recovery for backend-encrypted v2 tasks sent to routed providers. Disabled unless `enabled: true`; see [Encrypted v2 task recovery](#encrypted-v2-task-recovery). |

Manage the surface with the dashboard or
`ocx v2 status|on|off|mode <v1|default|v2>|threads <n>|mode-hint <text|--clear>`.
Expand Down Expand Up @@ -118,6 +119,77 @@ fails instead of routing unreadable ciphertext elsewhere.
}
```

## Encrypted v2 task recovery

`agentTaskRecovery` is an experimental compatibility path for a native ChatGPT parent spawning a
routed v2 child. It is disabled by default. When explicitly enabled and the final routed child task
contains an otherwise unreadable Fernet payload, opencodex uses a raw Responses passthrough request
to the fixed `https://chatgpt.com/backend-api/codex/responses` endpoint with forward-mode
authentication. ChatGPT returns the plaintext assignment through a forced function call; opencodex
then converts only that task item to a standard user message before routed-provider dispatch.

This is not local decryption and does not fix the Codex wire protocol. It depends on undocumented
ChatGPT backend behavior and may stop working after a backend change. The recovered assignment is
model output, not a cryptographically verified plaintext, so byte-for-byte fidelity is not
guaranteed. A scoped cache miss may add an authenticated ChatGPT request, consume account quota, and
add latency before the routed request. Concurrent requests for the same scoped task share one
recovery request. Startup prints a warning whenever the feature is enabled.

Admission and retention are deliberately narrow:

- recovery is available only while the proxy is bound to loopback;
- only a native Codex caller with a matching ChatGPT bearer/account pair is eligible. This is the
credential shape used by the canonical `openai` provider with `authMode: "forward"`; recovery uses
only the pair on the incoming request and never substitutes API-key authentication, another
provider credential, or another Codex account;
- callers using `x-opencodex-api-key`, `x-api-key`, generic API credentials, or a proxy admission
secret keep the existing `unreadable_encrypted_agent_task` failure;
- raw ChatGPT credentials are sent only to the hard-coded ChatGPT endpoint and are never placed in
the request body, logs, cache keys, or provider request; the in-memory cache scope uses only a
process-random keyed digest of the caller credential and account;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- the recovery request forwards only `authorization`, the matching `chatgpt-account-id`,
`originator`, and optional `openai-beta` and `user-agent` metadata; opencodex sets `content-type`
and `accept` itself, and no other caller headers cross this boundary;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- recovered plaintext is never logged or persisted; the process-local cache is credential-, parent-
thread-, and ciphertext-scoped, expires after 15 minutes, and is bounded by both configured entry
count (200 by default, 512 maximum) and 8 MiB total;
- any malformed envelope, failed recovery, timeout, or validation failure preserves the existing
fail-closed error; client cancellation returns 499. Neither path forwards ciphertext to the
routed provider.

### Threat model

This path assumes the local native Codex caller already holds a valid ChatGPT credential and that
the fixed ChatGPT endpoint is trusted to authenticate it. It protects against generic proxy/API-key
callers using the feature as a plaintext oracle, redirecting credentials to another destination,
cross-account or cross-thread cache reuse, and sensitive-data logging or persistence. Admission
checks token issuer, audience, Codex client, expiry/not-before bounds, and exact account match before
every cache lookup; the endpoint remains the signature authority.

It does not protect against another process running as the same OS user, a compromised ChatGPT
backend or recovery model, prompt injection inside the encrypted task, model transcription errors,
or memory inspection of the running proxy. Recovery output must therefore be treated as untrusted
model output rather than authenticated plaintext.

```json
{
"agentTaskRecovery": {
"enabled": true,
"model": "gpt-5.6-sol",
"timeoutMs": 45000,
"cacheEntries": 200
}
}
```

Enable this only when the additional authenticated request, quota use, plaintext-in-process boundary,
and private-backend dependency are acceptable. Prefer a native ChatGPT child or v1 heterogeneous
delegation when they are not.

This recovery path applies to direct-routed children. At most 32 recovery requests can be active at
once; additional misses fail closed. Combo routing keeps its existing native-only filter for
encrypted tasks and does not invoke recovery.

## Effort caps

Caps apply only to the v2 collaboration feature: a main turn qualifies when its tools expose v2,
Expand Down
6 changes: 6 additions & 0 deletions docs-site/src/content/docs/ru/guides/sub-agent-surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,12 @@ opencodex завершаетcя безопасно и не пересылает
combo, использовать v1 для делегирования между разнородными провайдерами либо повторно отправить
задачу как plaintext в содержимом v2 `agent_message`, если вы управляете вызывающей стороной.

Экспериментальная опция `agentTaskRecovery` по умолчанию выключена. После явного включения она
может восстановить этот формат дополнительным аутентифицированным запросом к фиксированному
endpoint ChatGPT, но расходует квоту, добавляет задержку и зависит от закрытого поведения backend.
При любом сбое сохраняется прежняя ошибка `unreadable_encrypted_agent_task`. Подробности приведены
в [английском справочнике конфигурации](/reference/configuration/agents/#encrypted-v2-task-recovery).

## Как сменить режим

### GUI
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ opencodex 会安全失败,而不是转发空任务或不可读任务:

恢复选项是选择原生 ChatGPT 子级、在 combo 中添加原生 ChatGPT 目标、在异构 provider 委派中使用 v1,或者在你控制调用方时将任务作为明文 v2 `agent_message` 内容重新发送。

实验性的 `agentTaskRecovery` 默认关闭。显式启用后,它可以通过向固定 ChatGPT 端点发送额外的认证请求来恢复这种格式,但会消耗配额、增加延迟,并依赖非公开的后端行为。任何失败都会保留原有的 `unreadable_encrypted_agent_task` 错误。详见[英文配置参考](/reference/configuration/agents/#encrypted-v2-task-recovery)。

## 更改模式

### GUI
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ description: 全域控制 Codex 在所有模型上生成和管理子代理的方
opencodex 允許你為目錄中的所有模型選擇多代理協作介面。儀表板和 Models 頁面中的 **Sub-agent** 開關會全域控制這一設定。

:::note
在 v2 介面(`multi_agent_v2`)上,子代理**預設**繼承父會話的模型:`fork_turns` 預設為 `all`,而全量歷史 fork 會拒絕覆蓋。自 v2.7.2 起,opencodex 注入的指引會教模型如何打破繼承 —— 將 `fork_turns` 設為 `"none"`(或如 `"3"` 的部分 fork)的 `spawn_agent` 呼叫可以傳入 `model` / `reasoning_effort` 引數;即使公開的工具 schema 中看不到這些引數,Codex 執行環境也會解析並應用。已知傳輸限制:當**原生**父代理 spawn 一個路由到**非原生** provider 的子代理時,Codex 用戶端可能只以後端加密的 `encrypted_content` 傳送 `NEW_TASK` 載荷([#92](https://github.com/lidge-jun/opencodex/issues/92))。opencodex 不會把這種無法讀取的任務轉發給外部 provider:直接路由會回傳 HTTP 400 和錯誤碼 `unreadable_encrypted_agent_task`;組合路由則會跳過無法解密的目標,並在存在可用目標時選擇規範的原生 ChatGPT 目標。恢復方法:異構 provider 委派改用 v1、選擇原生 ChatGPT 子代理,或將任務重新作為明文 v2 `agent_message` 內容傳送。
在 v2 介面(`multi_agent_v2`)上,子代理**預設**繼承父會話的模型:`fork_turns` 預設為 `all`,而全量歷史 fork 會拒絕覆蓋。自 v2.7.2 起,opencodex 注入的指引會教模型如何打破繼承 —— 將 `fork_turns` 設為 `"none"`(或如 `"3"` 的部分 fork)的 `spawn_agent` 呼叫可以傳入 `model` / `reasoning_effort` 引數;即使公開的工具 schema 中看不到這些引數,Codex 執行環境也會解析並應用。已知傳輸限制:當**原生**父代理 spawn 一個路由到**非原生** provider 的子代理時,Codex 用戶端可能只以後端加密的 `encrypted_content` 傳送 `NEW_TASK` 載荷([#92](https://github.com/lidge-jun/opencodex/issues/92))。opencodex 不會把這種無法讀取的任務轉發給外部 provider:直接路由會回傳 HTTP 400 和錯誤碼 `unreadable_encrypted_agent_task`;組合路由則會跳過無法解密的目標,並在存在可用目標時選擇規範的原生 ChatGPT 目標。恢復方法:異構 provider 委派改用 v1、選擇原生 ChatGPT 子代理,或將任務重新作為明文 v2 `agent_message` 內容傳送。另有預設停用的實驗性 `agentTaskRecovery`;它會增加 ChatGPT 配額用量與延遲,且依賴非公開後端行為。
:::

## What sub-agents are
Expand Down Expand Up @@ -106,6 +106,10 @@ opencodex 會安全失敗,而不是轉發空或無法讀取的任務:
恢復方法:選擇原生 ChatGPT 子代理、在組合中加入原生 ChatGPT 目標、異構 provider 委派改用 v1,
或在你能控制呼叫方時將任務重新作為明文 v2 `agent_message` 內容傳送。

實驗性的 `agentTaskRecovery` 預設停用。明確啟用後,它可透過固定 ChatGPT 端點的額外已驗證請求
恢復此格式,但會消耗配額、增加延遲,並依賴非公開後端行為。任何失敗都保留原本的
`unreadable_encrypted_agent_task` 錯誤。詳見[英文設定參考](/reference/configuration/agents/#encrypted-v2-task-recovery)。

## 更改模式

### GUI
Expand Down
38 changes: 38 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1130,6 +1130,13 @@ const clientIntegrationsSchema = z.object({
"claude-desktop": z.boolean().optional().catch(undefined),
}).passthrough();

const agentTaskRecoverySchema = z.object({
enabled: z.boolean().optional(),
model: z.string().trim().min(1).optional(),
timeoutMs: z.number().int().min(1_000).max(120_000).optional(),
cacheEntries: z.number().int().min(1).max(512).optional(),
}).strict();

const configSchema = z.object({
port: z.number().int().min(0).max(65535).default(10100),
managementUsageMaxReadBytes: z.number().int().positive().default(64 * 1024 * 1024),
Expand Down Expand Up @@ -1168,6 +1175,8 @@ const configSchema = z.object({
providerContextCaps: z.record(z.string(), z.number().int().positive()).optional(),
contextCapValue: z.number().int().positive().optional(),
multiAgentGuidanceEnabled: z.boolean().optional(),
// Invalid optional recovery config must not discard unrelated provider/account state.
agentTaskRecovery: agentTaskRecoverySchema.optional().catch(undefined),
// These selections pre-date schema validation and used to pass through as
// unknown fields. Invalid hand edits must disable only the optional
// delegation/native-default feature, not reject the whole config and hide
Expand Down Expand Up @@ -1903,6 +1912,20 @@ function warnDegradedUpstreamHostCircuitThreshold(rawParsed: unknown): void {
if (warning) console.warn(`⚠️ config.json ${warning}. Other settings were preserved.`);
}

function malformedAgentTaskRecoveryWarning(rawParsed: unknown): string | null {
const raw = rawConfigRecord(rawParsed);
if (!raw || !Object.hasOwn(raw, "agentTaskRecovery")) return null;
const result = agentTaskRecoverySchema.safeParse(raw.agentTaskRecovery);
if (result.success) return null;
const field = result.error.issues[0]?.path.join(".");
return `agentTaskRecovery${field ? `.${field}` : ""} ignored: invalid experimental recovery configuration`;
}

function warnDegradedAgentTaskRecovery(rawParsed: unknown): void {
const warning = malformedAgentTaskRecoveryWarning(rawParsed);
if (warning) console.warn(`⚠️ config.json ${warning}. Other settings were preserved.`);
}

type NativeSubagentPersistedField = "injectionModel" | "injectionEffort" | "syncCodexSubagentDefaults";

function rawConfigRecord(rawParsed: unknown): Record<string, unknown> | null {
Expand Down Expand Up @@ -2005,6 +2028,7 @@ export function loadConfig(): OcxConfig {
warnDegradedNativeSubagentConfig(parsed, config);
warnDegradedCodexAccountPicker(parsed);
warnDegradedUpstreamHostCircuitThreshold(parsed);
warnDegradedAgentTaskRecovery(parsed);
return withRefreshedCostOverlays(normalizeClaudeSubagentEffort(normalizeNativeSubagentSync(config, parsed), parsed));
}
// Schema validation failed — merge defaults into the raw object instead of
Expand All @@ -2027,6 +2051,7 @@ export function loadConfig(): OcxConfig {
warnDegradedNativeSubagentConfig(parsed, config);
warnDegradedCodexAccountPicker(parsed);
warnDegradedUpstreamHostCircuitThreshold(parsed);
warnDegradedAgentTaskRecovery(parsed);
return withRefreshedCostOverlays(normalizeClaudeSubagentEffort(normalizeNativeSubagentSync(config, parsed), parsed));
}
// Merge couldn't fix it — truly broken config
Expand Down Expand Up @@ -2086,6 +2111,8 @@ function validFileConfigDiagnostics(config: OcxConfig, rawParsed: unknown): Conf
if (pickerWarning) warnings.push(pickerWarning);
const hostCircuitWarning = malformedUpstreamHostCircuitThresholdWarning(rawParsed);
if (hostCircuitWarning) warnings.push(hostCircuitWarning);
const recoveryWarning = malformedAgentTaskRecoveryWarning(rawParsed);
if (recoveryWarning) warnings.push(recoveryWarning);
if (syncDisabledReason) {
warnings.push(`syncCodexSubagentDefaults ignored: ${syncDisabledReason}`);
}
Expand Down Expand Up @@ -2167,6 +2194,16 @@ function upstreamHostCircuitThresholdError(value: unknown): string | null {
return `schema_invalid: upstreamHostCircuitThreshold: must be an integer from 0 to ${UPSTREAM_HOST_CIRCUIT_MAX_THRESHOLD}`;
}

function agentTaskRecoveryError(value: unknown): string | null {
const raw = rawConfigRecord(value);
if (!raw || !Object.hasOwn(raw, "agentTaskRecovery") || raw.agentTaskRecovery === undefined) return null;
const result = agentTaskRecoverySchema.safeParse(raw.agentTaskRecovery);
if (result.success) return null;
const issue = result.error.issues[0];
const field = issue?.path.join(".");
return `schema_invalid: agentTaskRecovery${field ? `.${field}` : ""}: ${issue?.message ?? "invalid configuration"}`;
}

/**
* Same reasoning as {@link blankHostnameError}, and more urgent: the read path degrades a
* malformed selection-order map to undefined, which on a write would drop every entry the
Expand Down Expand Up @@ -2262,6 +2299,7 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
?? claudeSubagentEffortError(value)
?? appOwnedMemoryBudgetError(value)
?? upstreamHostCircuitThresholdError(value)
?? agentTaskRecoveryError(value)
?? googleAntigravityStaticCatalogVersionError(value)
?? codexAccountPrioritiesError(value)
?? codexAccountPickerEnabledError(value)
Expand Down
Loading
Loading