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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
node_modules/
.DS_Store
target/
.qoder/
11 changes: 0 additions & 11 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,6 @@ name = "codex-gateway"
version = "0.6.0"
edition = "2024"

[lib]
path = "rust-src/lib.rs"

[[bin]]
name = "codex-gateway"
path = "rust-src/main.rs"

[[bin]]
name = "codex-gateway-cli"
path = "rust-src/cli.rs"

[dependencies]
async-stream = "0.3"
axum = { version = "0.8", default-features = false, features = ["http1", "json", "query", "tokio"] }
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ FROM rust:1-bookworm AS builder
WORKDIR /build

COPY Cargo.toml Cargo.lock ./
COPY rust-src ./rust-src
COPY src ./src

RUN cargo build --release --bin codex-gateway --bin codex-gateway-cli

Expand Down
255 changes: 55 additions & 200 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,257 +2,112 @@

Chinese version: [README_zh.md](./README_zh.md)

This repository is a minimal multi-session gateway for verifying that `codex app-server` can be exposed as a small HTTP/SSE service.
Codex Gateway is a Rust HTTP/SSE gateway for running isolated Codex sessions behind a small API and browser UI.

Architecture notes: [docs/architecture.md](./docs/architecture.md)
Generated Qoder documentation may exist under `.qoder/`; it is generated output and should not be edited by hand or committed as source documentation. This README is the maintained human entry point.

API reference: [docs/api.md](./docs/api.md)
## Runtime Shape

The current shape is:
The default runtime is `embedded`:

1. external clients call a Rust HTTP API
2. the Rust service creates one Codex bridge per session
3. each bridge spawns its own local `codex app-server` child process over `stdio`
4. streamed notifications are forwarded back to that client over SSE
1. A client creates a session through the Rust gateway.
2. The session owns a `CodexAppServerBridge`.
3. The bridge starts and manages one `codex app-server` subprocess over stdio.
4. App-server notifications are folded into session state and streamed to the client over SSE.

Official references used while building this:
The optional `devbox` runtime is a remote execution backend:

- [Codex App Server](https://developers.openai.com/codex/app-server/)
- [Codex CLI quickstart](https://developers.openai.com/codex/quickstart/#setup)
- [Codex configuration reference](https://developers.openai.com/codex/config-reference/)
- [Codex CI/CD auth](https://developers.openai.com/codex/auth/ci-cd-auth)
1. The outer gateway creates a Devbox runtime.
2. It waits for the gateway inside Devbox to become ready.
3. It creates a remote session in that inner gateway.
4. The inner gateway uses the `embedded` runtime to run `codex app-server`.

## What is in here
`devbox` is runtime infrastructure, not a product mode.

- `rust-src/main.rs`: Rust HTTP server with session APIs, SSE streams, health endpoints, and static file serving
- `rust-src/bridge.rs`: reusable bridge for `initialize`, `account/read`, `model/list`, `thread/start`, `turn/start`, and notification handling
- `rust-src/runtime.rs`: shared runtime helpers for API-key login and `openai_base_url` overrides
- `rust-src/session_manager.rs`: multi-session lifecycle manager for bridges and TTL cleanup
- `rust-src/cli.rs`: one-shot CLI smoke test for a single bridge
- `public/index.html`: minimal browser UI
- `public/app.js`: browser behavior that creates its own API session and listens to its own SSE stream
- `public/styles.css`: intentionally simple UI styling
- `Dockerfile`: multi-stage image that builds the Rust gateway and installs the Codex CLI on Linux
## Brain Deployment API

## Runtime model
`POST /api/deployments` is a Brain application reserved API. It is not intended to describe a general deployment product surface.

This is no longer a single shared in-memory conversation.

- `POST /api/sessions` creates a new session
- each session owns one `CodexAppServerBridge`
- each bridge owns one `codex app-server` subprocess
- all `/state`, `/events`, `/turn`, and `/thread/new` calls are scoped to one session id
- sessions expire after an idle TTL and are also removable explicitly with `DELETE /api/sessions/:id`

That makes the service usable by multiple callers without sharing one thread or transcript.
The endpoint creates a Codex task that deploys a repository and reports a machine-readable deployment result. When the active session runtime is Devbox-backed, Gateway bootstraps the Devbox runtime before starting the Brain deployment task.

## HTTP API

### Health

- `GET /healthz`
- `GET /readyz`

### Sessions

- `POST /api/sessions`
- body: `{ "model": "optional-model-id" }`
- returns: `{ ok, sessionId, session, state }`
- `GET /api/sessions/:id/state`
- returns the latest session metadata plus the current bridge state snapshot
- `GET /api/sessions/:id/events`
- SSE stream for that session only
- `POST /api/sessions/:id/turn`
- body: `{ "prompt": "..." }`
- `POST /api/sessions/:id/turn/interrupt`
- requests cancellation of the current in-flight turn while keeping the session
- `POST /api/sessions/:id/thread/new`
- body: `{ "model": "optional-model-id" }`
- `POST /api/sessions/:id/thread/resume`
- `DELETE /api/sessions/:id`
- closes the session and its child process

### Important behavior

- `codex app-server` is launched with `sandbox_mode="danger-full-access"` and `approval_policy="never"`
- legacy and modern approval requests are auto-accepted when the app-server still surfaces them
- dynamic tool calls receive a structured tool result; unsupported tools fail explicitly instead of stalling the turn
- server-initiated requests that still require interactive UI are rejected or cancelled explicitly
- session state is in memory only
- gateway auth is optional and is enabled only when `CODEX_GATEWAY_JWT_SECRET` is set
- one session can only have one active turn at a time
- in-flight turns can be interrupted without deleting the session
- `GET /api/threads`
- `GET /api/threads/:threadId`
- `POST /api/deployments`
- `GET /api/deployments/:threadId`

## Local usage
Legacy single-session routes such as `/api/state`, `/api/events`, `/api/turn`, and `/api/thread/new` are removed and return `410 Gone`.

### Web UI
## Local Usage

Start the local server:
Start the gateway:

```bash
CODEX_GATEWAY_OPENAI_API_KEY=sk-... \
CODEX_GATEWAY_OPENAI_BASE_URL=https://sub2api-xnldrpuk.usw-1.sealos.app \
CODEX_GATEWAY_OPENAI_BASE_URL=https://example-openai-compatible-endpoint.test \
CODEX_GATEWAY_JWT_SECRET=replace-with-your-hs256-secret \
cargo run --bin codex-gateway
```

Then open:
Open:

```text
http://127.0.0.1:1317
```

The page restores the last live session when possible, falls back to resuming the last thread, and subscribes to its own SSE stream. When JWT auth is enabled, paste a bearer token into the `Auth` panel before using the page.

### CLI smoke test

Run the one-shot harness:

```bash
cargo run --bin codex-gateway-cli --
```

Or with a custom prompt:

```bash
cargo run --bin codex-gateway-cli -- "Reply with exactly the single word ready."
```

## Verification

If you want to verify the project manually, the shortest path is:

1. Start the service with `cargo run --bin codex-gateway`.
2. Check `http://127.0.0.1:1317/healthz`.
3. Check `http://127.0.0.1:1317/readyz`.
4. Open `http://127.0.0.1:1317` and wait for the page status to become `ready`.
5. Send `Reply with exactly the single word ready. Do not call tools.` from the page.
6. Confirm that the transcript shows `ready`.

If you want to verify the API directly instead of the page:

Create a session:
Quick API smoke test:

```bash
curl -X POST http://127.0.0.1:1317/api/sessions \
-H 'Content-Type: application/json' \
-d '{}'
```

Send a turn:

```bash
curl -X POST http://127.0.0.1:1317/api/sessions/<SESSION_ID>/turn \
-H 'Content-Type: application/json' \
-d '{"prompt":"Reply with exactly the single word ready. Do not call tools."}'
```

Read the latest state:

```bash
curl http://127.0.0.1:1317/api/sessions/<SESSION_ID>/state
```

If the transcript contains `ready`, the gateway, bridge, and `codex app-server` handshake are all working.

## Environment variables
## Configuration

Gateway-owned settings use the `CODEX_GATEWAY_` prefix for better discoverability.
Gateway-owned settings use the `CODEX_GATEWAY_` prefix.

- `CODEX_GATEWAY_HOST`: bind address for the Rust server. Defaults to `0.0.0.0`.
- `CODEX_GATEWAY_HOST`: bind address. Defaults to `0.0.0.0`.
- `CODEX_GATEWAY_PORT`: bind port. Defaults to `1317`.
- `CODEX_GATEWAY_CWD`: working directory passed to `thread/start`. Defaults to the repository root.
- `CODEX_GATEWAY_CODEX_BIN`: path to the `codex` executable if it is not on `PATH`.
- `CODEX_GATEWAY_MODEL`: preferred default model for new bridges.
- `CODEX_GATEWAY_OPENAI_API_KEY`: API key used at startup to run `codex login --with-api-key`.
- `CODEX_GATEWAY_OPENAI_BASE_URL`: upstream OpenAI-compatible base URL. When set, the gateway configures Codex to use a custom provider with `supports_websockets = false`.
- `CODEX_GATEWAY_CWD`: working directory passed to `thread/start`.
- `CODEX_GATEWAY_CODEX_BIN`: path to the `codex` executable.
- `CODEX_GATEWAY_MODEL`: preferred default model.
- `CODEX_GATEWAY_OPENAI_API_KEY`: API key used at startup for `codex login --with-api-key`.
- `CODEX_GATEWAY_OPENAI_BASE_URL`: upstream OpenAI-compatible base URL.
- `CODEX_GATEWAY_JWT_SECRET`: optional HS256 JWT secret.
- `CODEX_GATEWAY_MAX_SESSIONS`: maximum live sessions. Defaults to `12`.
- `CODEX_GATEWAY_MAX_DEPLOYMENTS`: maximum active deployment tasks. Defaults to `4`.
- `CODEX_GATEWAY_SESSION_TTL_MS`: idle session TTL. Defaults to `1800000`.
- `CODEX_GATEWAY_DEPLOYMENT_TIMEOUT_MS`: deployment task timeout and deployment session keepalive window. Defaults to `3600000`.
- `CODEX_GATEWAY_SESSION_SWEEP_INTERVAL_MS`: cleanup sweep interval. Defaults to `60000`.
- `CODEX_GATEWAY_CODEX_HOME`: Codex runtime home for auth cache, logs, history, and config. In Docker this defaults to `/codex-home`.
- `CODEX_GATEWAY_DEBUG`: enables raw bridge message debugging when set to `1`.
- `CODEX_GATEWAY_JWT_SECRET`: optional HS256 JWT secret. When set, the gateway requires a valid bearer token for all routes except `/healthz` and `/readyz`.
- `CODEX_GATEWAY_SESSION_RUNTIME`: session runtime backend. Defaults to `local`; set to `devbox` to create a Devbox runtime before each session.
- `CODEX_GATEWAY_DEVBOX_BASE_URL`: Devbox API base URL. If omitted in devbox mode, the gateway derives `https://devbox-server.${SEALOS_HOST}` from `SEALOS_HOST`.
- `CODEX_GATEWAY_DEVBOX_TOKEN`: Devbox API bearer token. `DEVBOX_TOKEN` is also accepted.
- `CODEX_GATEWAY_DEVBOX_JWT_SIGNING_KEY`: HS256 signing key used when no Devbox token is configured. `DEVBOX_JWT_SIGNING_KEY` is also accepted.
- `CODEX_GATEWAY_DEVBOX_NAMESPACE`: Devbox namespace. Defaults to `ns-test`.
- `CODEX_GATEWAY_DEVBOX_RUNTIME_IMAGE`: optional Devbox runtime image override.
- `CODEX_GATEWAY_DEVBOX_ARCHIVE_AFTER_PAUSE_TIME`: Devbox archive delay after pause. Defaults to `24h`.
- `CODEX_GATEWAY_DEVBOX_WAIT_TIMEOUT_SECONDS`: timeout while waiting for a new Devbox to become `Running`. Defaults to `60`.
- `CODEX_GATEWAY_DEVBOX_GATEWAY_READY_TIMEOUT_SECONDS`: timeout while waiting for the Codex Gateway inside Devbox to pass health and readiness checks. Defaults to `60`.
- `CODEX_GATEWAY_DEVBOX_BOOTSTRAP_TIMEOUT_SECONDS`: timeout for the bootstrap command. Defaults to `300`.

## Docker

The container image builds the Rust gateway binary, then installs the Codex CLI on Linux with `npm install -g @openai/codex`, which matches the official Codex CLI quickstart.

Build the image:

```bash
docker build -t codex-gateway .
```

Run it:

```bash
docker run --rm \
-p 1317:1317 \
-e CODEX_GATEWAY_OPENAI_API_KEY=sk-... \
-e CODEX_GATEWAY_OPENAI_BASE_URL=https://sub2api-xnldrpuk.usw-1.sealos.app \
-e CODEX_GATEWAY_JWT_SECRET=replace-with-your-hs256-secret \
-e CODEX_GATEWAY_HOST=0.0.0.0 \
-e CODEX_GATEWAY_PORT=1317 \
-e CODEX_GATEWAY_MAX_SESSIONS=8 \
codex-gateway
```
- `CODEX_GATEWAY_SESSION_RUNTIME`: session runtime backend. Defaults to `embedded`. Supported values are `embedded` and `devbox`.
- `CODEX_GATEWAY_MAX_DEPLOYMENTS`: maximum active Brain deployment tasks. Defaults to `4`.
- `CODEX_GATEWAY_DEPLOYMENT_TIMEOUT_MS`: Brain deployment timeout and session keepalive window. Defaults to `3600000`.

Devbox-related settings are only used when the runtime is `devbox`:

- `CODEX_GATEWAY_DEVBOX_BASE_URL`
- `CODEX_GATEWAY_DEVBOX_TOKEN`
- `CODEX_GATEWAY_DEVBOX_JWT_SIGNING_KEY`
- `CODEX_GATEWAY_DEVBOX_NAMESPACE`
- `CODEX_GATEWAY_DEVBOX_RUNTIME_IMAGE`
- `CODEX_GATEWAY_DEVBOX_ARCHIVE_AFTER_PAUSE_TIME`
- `CODEX_GATEWAY_DEVBOX_WAIT_TIMEOUT_SECONDS`
- `CODEX_GATEWAY_DEVBOX_GATEWAY_READY_TIMEOUT_SECONDS`
- `CODEX_GATEWAY_DEVBOX_BOOTSTRAP_TIMEOUT_SECONDS`

Notes:

- if `CODEX_GATEWAY_OPENAI_API_KEY` is set, the container runs `codex login --with-api-key` automatically before starting the gateway
- `CODEX_GATEWAY_OPENAI_BASE_URL` is the preferred way to point Codex at a third-party OpenAI-compatible endpoint; the gateway maps it to a custom Codex provider instead of the built-in `openai` provider
- if `CODEX_GATEWAY_JWT_SECRET` is set, clients must send `Authorization: Bearer <jwt>` on normal HTTP requests; the built-in Web UI also supports pasting the token into the sidebar
- you do not need to mount `CODEX_GATEWAY_CODEX_HOME` for normal API-key-based startup; mount it only if you want Codex state to persist across container restarts
- if you want Codex to operate on another workspace inside the container, set `CODEX_GATEWAY_CWD` and mount that path too
- this is a PoC deployment shape, not a hardened public service
- after the container starts, use the same health/API/Web UI verification flow described above

## GitHub Container Registry

GitHub Actions can publish this image to GHCR after pushes to `main` and version tags such as `v0.6.0`.

Published tags:

- `ghcr.io/labring/codex-gateway:main` for the latest `main` branch image
- `ghcr.io/labring/codex-gateway:sha-<commit>` for each published commit
- `ghcr.io/labring/codex-gateway:v0.6.0`, `0.6.0`, `0.6`, `0`, and `latest` when pushing a version tag

Pull the current `main` image:

```bash
docker pull ghcr.io/labring/codex-gateway:main
```

Run it the same way as the local image:
## Verification

```bash
docker run --rm \
-p 1317:1317 \
-e CODEX_GATEWAY_OPENAI_API_KEY=sk-... \
-e CODEX_GATEWAY_OPENAI_BASE_URL=https://sub2api-xnldrpuk.usw-1.sealos.app \
-e CODEX_GATEWAY_HOST=0.0.0.0 \
-e CODEX_GATEWAY_PORT=1317 \
-e CODEX_GATEWAY_MAX_SESSIONS=8 \
ghcr.io/labring/codex-gateway:main
cargo fmt --check
cargo test
```

If the package is private, authenticate to GHCR before pulling it.

## Current limitations

- no built-in rate limiting
- no durable session persistence
- approval UI is intentionally absent because this gateway defaults to full access
- each live session consumes a `codex app-server` subprocess
- browser clients can recover from reloads by storing the session id and thread id, but gateway sessions themselves are still in-memory only
Loading
Loading