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
8 changes: 7 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -1324,8 +1324,14 @@ CURSOR_USER_AGENT="Cursor/3.4"
# Approval policy passed to the app-server turn (e.g. never, on-request).
# OMNIROUTE_CODEX_APPSERVER_APPROVAL=never
# Sandbox policy passed to the app-server turn (e.g. read-only,
# workspace-write, danger-full-access).
# workspace-write, danger-full-access). When unset, the executor defaults to
# "workspace-write" (hardened; used to be "danger-full-access").
# OMNIROUTE_CODEX_APPSERVER_SANDBOX=read-only
# Auto-approve the app-server's own approval prompts (command/file/permission
# execution on the host). Defaults to OFF — prompts are auto-denied. Set to
# true/1/yes only when you trust the deployment to run codex-decided host
# commands. Per-connection override: providerSpecificData.codexAppServerAutoApprove.
# OMNIROUTE_CODEX_APPSERVER_AUTO_APPROVE=false

# ═══════════════════════════════════════════════════════════════════════════════
# 13. CLI FINGERPRINT COMPATIBILITY (Anti-Detection)
Expand Down
1 change: 1 addition & 0 deletions changelog.d/fixes/codex-appserver-hardening.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Hardened the Codex app-server transport after the post-merge security review of #11205: approval prompts from the app-server (its own command/file/permission execution — not the harness tool passthrough) are now auto-denied by default, with opt-in auto-approval via `providerSpecificData.codexAppServerAutoApprove` / `OMNIROUTE_CODEX_APPSERVER_AUTO_APPROVE`; the default codex sandbox changed from `danger-full-access` to `workspace-write` (override per connection or env); env-sourced capability tokens are now only sent to env-sourced URLs or operator-local hosts (loopback/RFC1918/link-local/ULA/localhost/single-label LAN names/*.local/*.ts.net/*.internal), so a connection's providerSpecificData URL can no longer exfiltrate the operator's env token; and the `/readyz` health probe no longer follows redirects while carrying the bearer token.
7 changes: 4 additions & 3 deletions config/quality/file-size-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,7 @@
"src/shared/components/analytics/charts.tsx": 1346,
"src/shared/services/cliRuntime.ts": 1459,
"src/sse/handlers/chat.ts": 2493,
"src/sse/services/auth.ts": 3337,
"src/sse/services/auth.ts": 3344,
"_rebaseline_2026_08_23_11186_synced_inventory_routing": "PR #11186 (pacocartones) own growth: src/sse/services/auth.ts 3260->3337 (+77, loadAdvertisedModelsForSelfHostedConnections + the modelNotAdvertised candidate-filter predicate — pins chat routing to the connection whose synced inventory actually advertises the model, fixing spurious model-not-found on multi-host self-hosted setups; at the existing credential-selection chokepoint, not extractable without splitting the selection flow). Covered by tests/unit/chat-routing-synced-inventory-11089.test.ts. Owner pre-authorized baseline bumps 2026-08-22.",
"tests/unit/account-fallback-service.test.ts": 2044,
"tests/unit/provider-validation-specialty.test.ts": 3880,
Expand Down Expand Up @@ -464,14 +464,15 @@
"src/app/(dashboard)/dashboard/providers/[id]/providerPageHelpers.ts": 1014,
"open-sse/config/imageRegistry.ts": 1034,
"src/sse/handlers/chatHelpers.ts": 1019,
"src/shared/middleware/chatBodyAdmission.ts": 1009,
"src/shared/middleware/chatBodyAdmission.ts": 1118,
"_rebaseline_2026_08_22_11020_sigterm_drain": "PR #11020 (RaviTharuma) own growth: chatBodyAdmission.ts 1005->1009 (+4, heavyweight admission leases now increment the SIGTERM drain counter and releaseChatAdmissionWhenDone holds it for the SSE lifetime — closes #11015; +4 are the lease/drain wiring lines at the existing admission chokepoint). Covered by tests/unit/chat-body-admission.test.ts heavyweight-lease cases. Owner pre-authorized baseline bumps 2026-08-22.",
"_rebaseline_2026_08_20_10668_tabitoken_gateway": "#10668 (yawar-aquil) own catalog growth: src/shared/constants/providers/apikey/gateways.ts 1268->1283 (+15, entirely this PR diff -- one new tabitoken gateway entry, data lines only; base moved from 1255 to 1268 via other merges since the PR forked). Not combination drift: reproducible on the PR branch alone, so the WS5.5 release-captain rule does not apply. Extraction is not available -- the file is pure data (own header: \"Pure data; merged by apikey/index.ts via spread\") and already split into 6 family files under apikey/. Same precedent as _rebaseline_2026_08_14_imagetotext_servicekinds (#10275/#10291, gateways.ts 1250->1255, data lines only) and _rebaseline_2026_08_11_v3850_merge_storm_provider_registry (owner-authorized for this same file).",
"open-sse/executors/commandCode.ts": 1059,
"_rebaseline_2026_08_21_10859_vision_bridge_catalog": "#10859 own growth (Vision Bridge fixes #10808/#10809): src/lib/modelCapabilities.ts 1006->1016 (+10, cmd/gpt-5.3-codex* text-only capability resolution) and open-sse/executors/commandCode.ts 988->1023 (+35, Command Code wire-model normalization for bare ids + reasoning field fallback for opencode-routed gateways). Cohesive bug fixes at the existing capability-resolution / executor chokepoints; not extractable mid-fix. Covered by tests/unit/model-capabilities-command-code-codex-textonly-10703.test.ts, tests/unit/command-code-vision.test.ts, tests/unit/opencode-mimo-reasoning-details-nonstream.test.ts. Pushed directly to release (own-session miss: the original rebaseline was made in a throwaway validation worktree and never landed on the PR branch or the release before merge).",
"_rebaseline_2026_08_21_10907_sticky_pin_clear": "#10907 own growth: open-sse/executors/commandCode.ts 1023->1038 (+15, effort-suffix sanitization threading for the sticky-pin-clear fix). Cohesive change at the existing executor chokepoint. Covered by tests/unit/command-code-executor.test.ts.",
"_rebaseline_2026_08_21_10986_reasoning_only_content": "#10986 own growth: open-sse/executors/commandCode.ts 1038->1059 (+21, reasoning-only content fallback — when upstream emits only reasoning-delta events and never a text-delta, surface the reasoning text as message.content in createJsonResponse and emit a synthetic content delta in createStreamResponse). Cohesive bug fix at the existing executor chokepoint (mirrors precedent style of #10907/#10859). Covered by tests/unit/command-code-executor.test.ts (2 new cases: non-stream + streaming).",
"_rebaseline_2026_08_21_11069_m365_har_import": "#11069 own growth: AddApiKeyModal.tsx 1073->1080 (+7 = Import .har file button for the copilot-m365-web credential modal — M365 is the only provider whose credential (access_token+chathubPath) must be extracted from a DevTools HAR WebSocket URL, added as a new modal affordance). Cohesive UI at the existing modal chokepoint; not extractable. Covered by tests/unit/m365-har-import*.test.ts."
"_rebaseline_2026_08_21_11069_m365_har_import": "#11069 own growth: AddApiKeyModal.tsx 1073->1080 (+7 = Import .har file button for the copilot-m365-web credential modal — M365 is the only provider whose credential (access_token+chathubPath) must be extracted from a DevTools HAR WebSocket URL, added as a new modal affordance). Cohesive UI at the existing modal chokepoint; not extractable. Covered by tests/unit/m365-har-import*.test.ts.",
"_rebaseline_2026_08_23_tip_drift_post_batch0823": "Tip drift after the 2026-08-23 merge wave: chatBodyAdmission.ts 1009->1118 (+109, gate count incl. +1) and auth.ts 3337->3344 (+7), both grown by merges already on origin/release/v3.8.50 (verified identical on the pristine tip) — not by the codex-appserver-hardening PR that carries this bump. Owner pre-authorized baseline bumps 2026-08-22."
},
"_rebaseline_base_2026_08_10_proxyfetch": "Base-red fix (green-prs sweep, issue #9985): open-sse/utils/proxyFetch.ts 1207 > cap 1000 — new proxied-TLS fetch helper introduced by the Fal reference-image work. Owner-authorized quick rebaseline to green; structural slim tracked for v3.9.0.",
"_rebaseline_2026_07_27_v3849_train2": "Merge-train 2 (7 PRs) — owner-approved 2026-07-27. Single entry: chatCore.ts 4955->5006 (#8595, Responses multi-turn image compaction before the context hard-reject). Genuine irreducible growth at the existing compaction chokepoint in handleChatCore — the PR adds a last-resort retry against the concrete budget plus the estimateFinalInputTokens helper, both wired at the pre-existing call site rather than a new branch. Covered by tests/unit/8560-responses-image-compaction.test.ts (4 tests).",
Expand Down
3 changes: 2 additions & 1 deletion docs/reference/ENVIRONMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -742,7 +742,8 @@ REQUEST_TIMEOUT_MS (global override)
| `OMNIROUTE_CODEX_APPSERVER_WS_TOKEN_FILE` | _(unset)_ | Path to a file holding the app-server capability token (from `codex app-server --ws-token-file`). Used when `OMNIROUTE_CODEX_APPSERVER_WS_TOKEN` is unset. Per-connection override: `providerSpecificData.codexAppServerTokenFile`. |
| `OMNIROUTE_CODEX_APPSERVER_CWD` | `/tmp` | Working directory the app-server turn runs in. Per-connection override: `providerSpecificData.codexAppServerCwd`. |
| `OMNIROUTE_CODEX_APPSERVER_APPROVAL` | _(unset)_ | Approval policy passed to the app-server turn (e.g. `never`, `on-request`). Per-connection override: `providerSpecificData.codexAppServerApprovalPolicy`. |
| `OMNIROUTE_CODEX_APPSERVER_SANDBOX` | _(unset)_ | Sandbox policy passed to the app-server turn (e.g. `read-only`, `workspace-write`, `danger-full-access`). Per-connection override: `providerSpecificData.codexAppServerSandbox`. |
| `OMNIROUTE_CODEX_APPSERVER_SANDBOX` | _(unset)_ | Sandbox policy passed to the app-server turn (e.g. `read-only`, `workspace-write`, `danger-full-access`). When unset the executor defaults to `workspace-write` (hardened; previously `danger-full-access`). Per-connection override: `providerSpecificData.codexAppServerSandbox`. |
| `OMNIROUTE_CODEX_APPSERVER_AUTO_APPROVE` | `false` | Auto-approve the app-server's own approval prompts (command/file/permission execution on the host). Off by default — prompts are auto-denied; harness tool calls are unaffected (they travel the separate `item/tool/call` passthrough). Accepts `true`/`1`/`yes`. Per-connection override: `providerSpecificData.codexAppServerAutoApprove`. |
| `FETCH_HEADERS_TIMEOUT_MS` | = `FETCH_TIMEOUT_MS` | Time to receive response headers. |
| `OMNIROUTE_DIRECT_HEADERS_TIMEOUT_MS` | `30000` (30s) | Maximum response-start wait (ms) for each direct no-proxy attempt. A timeout retries once on a fresh socket; set `0` to disable the bound and retain the previous behavior. |
| `FETCH_BODY_TIMEOUT_MS` | = `FETCH_TIMEOUT_MS` | Time to receive the full response body. |
Expand Down
32 changes: 19 additions & 13 deletions open-sse/executors/codex-app-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
CodexAppServerClient,
type CodexAppServerClientOptions,
} from "./codex/appServerClient.ts";
import { resolveAppServerConfig, type CodexAppServerConfig } from "./codex/appServerConfig.ts";
import { resolveAppServerConfig, resolveThreadStartPolicy, type CodexAppServerConfig } from "./codex/appServerConfig.ts";
import {
translateNotification,
translateToolCall,
Expand Down Expand Up @@ -232,13 +232,20 @@ export class CodexAppServerExecutor extends BaseExecutor {
"codex_app_server_unconfigured"
);
}
// Turn policy (hardened after the #11205 security review): approvalPolicy
// "never", sandbox "workspace-write", autoApprove off unless the operator
// opted in — see resolveThreadStartPolicy.
const policy = resolveThreadStartPolicy(config, psd);

const promptText = extractPromptText(input.body);
const effort = extractEffort(input.body);
const toolMaps = buildAppServerToolMaps(input.body);
const hasTools = toolMaps.specs.length > 0;
const events = new AsyncEventQueue<AdapterEvent>();
const client = new CodexAppServerClient(this.clientOptions);
const client = new CodexAppServerClient({
...this.clientOptions,
autoApproveApprovals: policy.autoApprove,
});

const run = async () => {
let terminated = false;
Expand Down Expand Up @@ -284,17 +291,16 @@ export class CodexAppServerExecutor extends BaseExecutor {
cwd: config.cwd,
// OmniRoute is a router: the HARNESS that consumes OmniRoute owns tool
// execution and policy. codex must therefore NEVER block a turn waiting
// on its own interactive approval, and its own sandbox must not gate the
// model — the harness decides what actually runs. So we pair
// approvalPolicy:"never" (non-interactive; codex never prompts) with
// sandbox:"danger-full-access" (codex's own sandbox imposes no
// restriction), mirroring codexInstructions.ts:50 ("never +
// danger-full-access = take advantage of it"). Any server→client
// approval request that still arrives is auto-APPROVED by the client
// (see CodexAppServerClient), never denied — denial would sabotage the
// harness's tool calls. Callers can override both via providerSpecificData.
approvalPolicy: config.approvalPolicy ?? "never",
sandbox: config.sandbox ?? "danger-full-access",
// on its own interactive approval (approvalPolicy "never"). Its own
// sandbox defaults to "workspace-write" (hardened after the #11205
// security review; WAS "danger-full-access") so codex-decided host
// commands are confined to the turn's cwd tree — widen only via an
// explicit operator override. Server→client approval prompts (codex's
// own command/file/permission requests, NOT the harness tool
// passthrough) are auto-DENIED by the client unless the operator opted
// into auto-approval (see CodexAppServerClient).
approvalPolicy: policy.approvalPolicy,
sandbox: policy.sandbox,
// INBOUND harness tools → codex. The client tells the app-server which
// function tools are available for the thread via the `dynamicTools`
// field on thread/start (a DynamicToolSpec[] under the experimental API,
Expand Down
45 changes: 33 additions & 12 deletions open-sse/executors/codex/appServerClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@
* command / patch / permission. OmniRoute is a ROUTER — the harness that consumes
* it owns tool execution and policy — so codex must never stall a turn on its own
* interactive approval. Every inbound ServerRequest is always answered: approval
* prompts are auto-APPROVED (so the model's agentic tool calls proceed; the harness
* decides what really runs), and anything else we can't service gets a JSON-RPC
* error so the id is always settled and the turn never hangs.
* prompts are auto-DENIED by default (they gate codex's OWN host execution, not
* the harness's tools; auto-approval is an explicit operator opt-in — hardening
* after the #11205 security review), and anything else we can't service gets a
* JSON-RPC error so the id is always settled and the turn never hangs.
*/

// wreq-js WebSocket surface (mirrors the private type in codex.ts:71-77).
Expand All @@ -37,7 +38,12 @@ interface PendingReq {
}

// The set of ServerRequest methods that are approval prompts (see PROTOCOL-DIGEST
// "Server -> client REQUESTS"). All of these get an auto-denial decision.
// "Server -> client REQUESTS"). All of these get an auto-DENIAL decision unless
// the operator explicitly opted into auto-approval (hardening after the #11205
// security review): these prompts gate codex's OWN command/file/permission
// execution on the host, NOT the harness's dynamic tools (those travel the
// separate item/tool/call passthrough), so denying by default never sabotages
// harness tool calls — it closes a prompt-injection → host-execution path.
const APPROVAL_REQUEST_METHODS = new Set<string>([
"item/commandExecution/requestApproval",
"item/fileChange/requestApproval",
Expand All @@ -47,12 +53,20 @@ const APPROVAL_REQUEST_METHODS = new Set<string>([
]);

const ROUTER_APPROVAL_NOTE = "router: harness-controlled execution";
const ROUTER_DENIAL_NOTE =
"router: denied by default (set codexAppServerAutoApprove to opt in)";

export interface CodexAppServerClientOptions {
/** Transport factory. Defaults to the shared wreq-js websocket() when omitted. */
websocketFn?: CodexAppServerWebsocketFn | null;
/** Default per-request timeout (ms). */
defaultTimeoutMs?: number;
/**
* Auto-APPROVE codex's own approval prompts (command/file/permission).
* Defaults to FALSE — prompts are auto-denied. Enable only when the operator
* trusts the app-server deployment to run codex-decided host commands.
*/
autoApproveApprovals?: boolean;
}

/**
Expand Down Expand Up @@ -89,11 +103,13 @@ export class CodexAppServerClient {
private toolCallHandler: CodexAppServerToolCallHandler | null = null;
private readonly websocketFn: CodexAppServerWebsocketFn | null;
private readonly defaultTimeoutMs: number;
private readonly autoApproveApprovals: boolean;
private closed = false;

constructor(options: CodexAppServerClientOptions = {}) {
this.websocketFn = options.websocketFn ?? null;
this.defaultTimeoutMs = options.defaultTimeoutMs ?? 120_000;
this.autoApproveApprovals = options.autoApproveApprovals === true;
}

/**
Expand Down Expand Up @@ -232,22 +248,27 @@ export class CodexAppServerClient {
}

/**
* Always answer an inbound ServerRequest so its id is settled. Approval prompts
* are auto-APPROVED (OmniRoute is a router; the harness that consumes it owns
* execution policy, so codex's own approval must not block the turn). Anything
* we cannot service gets a JSON-RPC error so the id is still settled.
* Always answer an inbound ServerRequest so its id is settled. Approval
* prompts are auto-DENIED unless the operator opted into auto-approval
* (hardening after the #11205 security review): they gate codex's OWN host
* command/file execution, not the harness's tools. Anything we cannot
* service gets a JSON-RPC error so the id is still settled.
*/
private answerServerRequest(id: number, method: string): void {
if (!this.ws || this.closed) return;
if (APPROVAL_REQUEST_METHODS.has(method)) {
// ReviewDecision "approved" — let the model's agentic action proceed. The
// harness downstream of OmniRoute is the real gate. Note the note field is
// advisory; the decision string is what codex acts on.
// ReviewDecision — "denied" by default; "approved" only with the explicit
// operator opt-in. The note field is advisory; the decision string is
// what codex acts on.
const approved = this.autoApproveApprovals;
this.ws.send(
JSON.stringify({
jsonrpc: "2.0",
id,
result: { decision: "approved", note: ROUTER_APPROVAL_NOTE },
result: {
decision: approved ? "approved" : "denied",
note: approved ? ROUTER_APPROVAL_NOTE : ROUTER_DENIAL_NOTE,
},
})
);
return;
Expand Down
Loading
Loading