codex-app-server-bridge
v0.1.0
Published
TypeScript client / bridge for the local Codex app-server JSON-RPC protocol. Unofficial — does not provide, proxy, store, or share OpenAI credentials.
Maintainers
Readme
codex-app-server-bridge
日本語版は README_jp.md を参照。
TypeScript client / bridge for the local Codex app-server (JSON-RPC).
Unofficial. This project is an unofficial TypeScript client for local Codex app-server. It does not provide, proxy, store, or share OpenAI credentials. Each user must run and authenticate their own local Codex installation. Do not expose your Codex app-server or credentials to other users.
What this is
A TypeScript bridge that drives a local Codex CLI
process (codex app-server) over its experimental JSON-RPC protocol. It
provides:
- Low-level
CodexJsonRpcClient— direct method access (request / notification / server-initiated request handling, timeout, AbortSignal). - Higher-level
CodexBridge— drives theinitializehandshake, normalizes Codex notifications into a small event union, and routes the seven approval-request flavors through anApprovalControllerwith a kind-aware default-deny response factory. - Approval policy callback (
onApprovalRequested) — pass a deterministic policy callback to the bridge to auto-accept routine operations and escalate only the genuinely uncertain ones. This is the design pivot that keeps the LLM out of the yes/no decision path and preserves a structured approval flow. - Transports — stdio and WebSocket (Unix-socket is a stub and not yet implemented).
- Type-safe wrappers for
initialize,thread/{start,resume,fork,list,read,archive},turn/{start,interrupt}, plus a genericrequest<T>(method, params)escape hatch for everything else. - Full generated protocol types (
src/protocol/generated/v2/) so any Codex method or notification can be addressed by name.
What this is not
- Not a hosted Codex backend.
- Not a credential proxy. The package never reads, stores, or transmits
your OpenAI tokens — each user runs and authenticates their own local
codexCLI. - Not a reimplementation of Codex. We only speak the documented
app-serverprotocol. - Not an agent framework. Codex already exposes a complete agent
(including internal
AgentControlsub-agent spawning); a higher-level framework abstraction on top is redundant, so this package stays at the bridge layer.
Requirements
- Node.js ≥ 22.13.0
- Codex CLI ≥ 0.128, authenticated via
the official
codex loginflow. - pnpm 10+ for local development.
Installation (planned for npm)
pnpm add codex-app-server-bridgeThe package has a single runtime dependency (ws for WebSocket transport).
No peer dependencies; nothing to install beyond the package itself.
Quick start
import { CodexBridge, StdioTransport } from "codex-app-server-bridge";
const bridge = new CodexBridge({
transport: new StdioTransport({ command: "codex", args: ["app-server"] }),
});
await bridge.connect();
const threadResult = (await bridge.startThread({ cwd: process.cwd() } as never)) as {
thread: { id: string };
};
const threadId = threadResult.thread.id;
for await (const event of bridge.startTurn({
threadId,
input: [{ type: "text", text: "Summarize this repository.", text_elements: [] }],
cwd: process.cwd(),
} as never)) {
if (event.type === "text-delta" && event.kind === "text") {
process.stdout.write(event.text);
} else if (event.type === "approval-requested") {
bridge.denyApproval(event.requestId);
}
}
await bridge.close();Runnable example
examples/text-repl-agent/— text REPL built directly on the Vercel AI SDK. Demonstrates the recommended approval pattern: a deterministic policy filter plus a forcedtoolChoicethat keeps the LLM out of the yes/no decision path. Supports bothstdioandwstransports via an environment variable.
Approval pattern (recommended)
Codex emits a server-initiated approval request every time it wants to run a destructive command, edit a file, etc. The design pivot of this library is to avoid turning those approvals into a free-form chat between the LLM and the user — that pattern fails in practice:
| Approach | How it works | Failure mode |
| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| ❌ Conversational (anti-pattern) | LLM paraphrases "shall I do this?" as text, user replies yes/no, LLM interprets and calls respond_to_approval | LLM hallucinates "done" without actually calling the function — approval sits unanswered, Codex stalls |
| ✅ Structured (recommended) | (1) a deterministic policy filter auto-handles routine cases; (2) on escalation, the LLM is given a one-tool list and tool_choice is pinned to it | LLM cannot reply with plain text — the API itself rejects anything but the forced function call |
examples/text-repl-agent/ demonstrates the recommended pattern end-to-end.
Approval flow details
Codex's app-server sends server-initiated approval requests in seven
flavors (the bridge folds the two v1 legacy methods into commandExecution):
| Kind | Server method | Response shape |
| ------------------ | ------------------------------------------- | ------------------------------------------------------------------------------------------ |
| commandExecution | item/commandExecution/requestApproval | { decision: "accept" \| "acceptForSession" \| "decline" \| "cancel" \| object variant } |
| fileChange | item/fileChange/requestApproval | { decision: "accept" \| "acceptForSession" \| "decline" \| "cancel" } |
| permissions | item/permissions/requestApproval | { permissions, scope, strictAutoReview? } |
| toolUserInput | item/tool/requestUserInput | { answers: { ... } } |
| mcpElicitation | mcpServer/elicitation/request | { action, content, _meta } |
| v1 (legacy) | applyPatchApproval, execCommandApproval | { decision: "approved" \| "approved_for_session" \| "denied" \| "timed_out" \| "abort" } |
CodexBridge supplies a default-deny response automatically when:
- the consumer calls
bridge.denyApproval(requestId), or - an approval times out (default 30 s, configurable via
CodexBridgeOptions.approvalTimeoutMs), or - the bridge is closed while approvals are still pending.
Use bridge.respondToApproval(requestId, response) to send an arbitrary
kind-specific response.
Overriding the default behavior with a policy callback
const bridge = new CodexBridge({
transport: ...,
onApprovalRequested: async ({ kind, method, params }) => {
// Auto-accept safe operations.
if (isSafe(kind, params)) return { decision: "accept" };
// Auto-refuse banned operations.
if (isProhibited(kind, params)) return { decision: "refuse" };
// Anything else: fall back to the normal event path so the host can
// ask the user.
return "escalate";
},
});Return values:
{ decision: "accept" }/{ decision: "refuse" }— bridge responds immediately, noapproval-requestedevent is emitted.{ raw: <kind-specific JSON> }— bridge sends the exact JSON verbatim (advanced escape hatch)."escalate"— bridge emits the event as usual; host responds viarespondToApproval.
Transport options
stdio (default, recommended)
Spawns the local codex binary as a child process. No network exposure.
new StdioTransport({ command: "codex", args: ["app-server"], cwd });WebSocket
WebSocketTransport connects to a separately-started codex app-server
--listen ws://HOST:PORT. Loopback hosts (127.0.0.1, localhost, [::1])
are allowed without restriction. Remote hosts are refused by default —
set allowRemote: true to override (and warnOnRemote: false to silence
the startup warning).
import { WebSocketTransport } from "codex-app-server-bridge";
const transport = new WebSocketTransport({
url: "ws://127.0.0.1:8089/",
headers: { Authorization: "Bearer ..." }, // optional
});Unix socket
Reserved for a follow-up issue; the current release ships a stub that
throws CodexTransportError. Removal is also being considered since this
transport is not part of the upstream spec — see Issue #1.
Linux sandbox note (Ubuntu 24.04+)
Codex's Linux sandbox uses bubblewrap, and Ubuntu 24.04+ blocks unprivileged
user namespaces by default (kernel.apparmor_restrict_unprivileged_userns=1).
When this is in effect, the codex app-server process itself fails at
startup regardless of which transport you use — the bwrap call is internal
to Codex, not to this bridge (stdio mode where this library spawns Codex,
and ws mode where you spawn Codex yourself, are both affected equally).
Three workarounds (in order of preference):
- Pass
-c sandbox_mode=danger-full-accessas a Codex launch argument to disable Codex's own bubblewrap fence. Pair with-c approval_policy=on-requestto keep a human gate on every command execution (this is whatexamples/text-repl-agentuses).- In stdio mode:
new StdioTransport({ command: "codex", args: ["app-server", "-c", "sandbox_mode=danger-full-access", ...] }). - In ws mode: pass it on the command line you use to launch Codex
yourself:
codex app-server --listen ws://... -c sandbox_mode=danger-full-access.
- In stdio mode:
- Install a bubblewrap-specific AppArmor profile under
/etc/apparmor.d/bwrap(a one-timesudostep) so the kernel allowsbwrapto create user namespaces. Once installed, Codex's own sandbox works for both stdio and ws without the-cflag. - Run Codex inside a Docker container with user namespaces enabled,
then connect to it via
WebSocketTransportfrom the host.
Logging
The library is quiet by default. All output is either opt-in or overridable; the library itself writes nothing to stdout.
Direct console output (three narrow call sites)
| Site | Output | How to override |
| -------------------- | ------------------------------------------------------------------------------------------------------ | --------------------------------------------------- |
| StdioTransport | [codex-stderr] <line> — forwards the spawned Codex process's stderr line-by-line via console.error | Pass StdioTransportOptions.onStderr to redirect |
| WebSocketTransport | A single console.warn on first connection to a non-loopback host | WebSocketTransportOptions.warnOnRemote: false |
| Internal fallback | [codex-json-rpc-client] internal error: ... (only fires on internal invariant breakage) | Not user-overridable; should not happen in practice |
In typical use only Codex's own stderr is visible.
Structured audit log (opt-in)
Pass a CodexAuditLogger to the bridge to receive structured events
covering the connection lifecycle, thread / turn lifecycle, every approval
request and decision, and policy-filter outcomes. The default is a no-op
logger.
interface CodexAuditEvent {
timestamp?: string;
level: "info" | "warn" | "error";
source: string; // "bridge", "transport", "codex-stderr", ...
message: string;
threadId?: string;
turnId?: string;
approvalKind?: ApprovalKind;
approvalDecision?: CodexApprovalDecision;
errorCode?: number;
errorMessage?: string;
payload?: Record<string, unknown>;
}File-based JSONL logger (with credential redaction)
import { CodexBridge, createFileAuditLogger } from "codex-app-server-bridge";
const bridge = new CodexBridge({
transport,
auditLogger: createFileAuditLogger("./codex-audit.jsonl"),
});- Well-known credential-like keys (
token,Authorization,OPENAI_*,password,secret,env, ...) are redacted before serialization. - JSONL output is easy to slice with
jq. - The
pathargument is required to prevent accidental writes to unexpected locations.
Pipe into your own logger
import pino from "pino";
const log = pino();
new CodexBridge({
transport,
auditLogger: {
log: (event) => log[event.level](event, event.message),
},
});pino, winston, an OpenTelemetry collector, or any other host-side
logger plugs in here — no changes to the library.
Design intent
- The library stays quiet; the host wires up its own logger.
- The library never writes to stdout.
- stderr only carries the Codex subprocess passthrough plus one connection warning, both suppressible.
- Structured logs are fully injection-style — pino / winston / OpenTelemetry / a custom transport all work the same way.
Security notes
- The Codex CLI handles its own credentials. This package never reads
~/.codex/auth.jsonor any environment variable that may contain a credential. - The WebSocket transport refuses non-loopback hosts unless
allowRemote: trueis explicitly set. - The optional audit log redacts well-known credential-like keys (see the
Logging section for the redaction list). Use
createFileAuditLogger(path)—pathis required to prevent accidental writes to unexpected locations.
Known improvement candidates
See Issue #1 for the full audit. Highlights:
- 🔴 JSON-RPC error
-32001 Server overloadedretry (per spec) is not yet implemented. - 🔴 No dedicated WebSocket Bearer-auth helper (works today via the generic
headersfield, but abearerTokenshortcut would improve DX). - 🟡
account/chatgptAuthTokens/refreshserver-request handler not implemented. - 🟡 Typed wrappers cover ~9 of ~70 methods (generated types are available
for the rest; users fall back to the generic
request<T>for now). - 🟡 Bridge currently surfaces ~16 turn-centric notifications; ~21 thread/account/mcpServer lifecycle notifications are not yet normalized.
- 🟢 Unix-socket transport decision (remove vs. mark as stub).
The text-repl-agent example use case is production-ready as-is; the
above are quality-of-life and edge-case items.
Generated protocol types
src/protocol/generated/ is produced by codex app-server generate-ts and
checked into the repo. The currently committed types track Codex 0.128.
Regenerate after upgrading Codex:
pnpm generate:codex-typesThese generated files are derived from openai/codex (Apache-2.0). The rest
of this package is MIT-licensed; see LICENSE, LICENSE-APACHE, and
src/protocol/generated/NOTICE.md for the attribution required by Apache-2.0.
Supported Codex versions
This package follows a single-latest-major support policy. The current
release (0.x) supports Codex 0.128 and newer. When Codex bumps to 1.0 (or any
future major), this package will be updated to track the new major and the
change will be announced in CHANGELOG.md.
Development
pnpm install
pnpm typecheck
pnpm lint
pnpm test
pnpm buildIntegration tests against the real codex app-server are off by default. To
run them locally:
RUN_CODEX_INTEGRATION=1 pnpm testCI never runs integration tests (Codex credentials are intentionally kept off the CI host).
License
This package is licensed under MIT.
Generated protocol types under src/protocol/generated/ are derived from
openai/codex (Apache-2.0). See
LICENSE-APACHE and
src/protocol/generated/NOTICE.md for
attribution.
