https://github.com/slankdev/codexweb
https://github.com/slankdev/codexweb
Last synced: about 1 month ago
JSON representation
- Host: GitHub
- URL: https://github.com/slankdev/codexweb
- Owner: slankdev
- Created: 2026-05-11T22:04:28.000Z (about 1 month ago)
- Default Branch: main
- Last Pushed: 2026-05-11T23:23:26.000Z (about 1 month ago)
- Last Synced: 2026-05-12T00:30:03.501Z (about 1 month ago)
- Language: TypeScript
- Size: 43.9 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# codexweb (test)
OpenAI Codex CLI ([`openai/codex`](https://github.com/openai/codex)) を Web UI から
触れるようにする、Next.js ベースの薄いラッパーです。Codex Web や Claude Code
Cloud のような **チャット形式の Web UI** で Codex エージェントにタスクを投げ、
ストリーミング結果を眺めたり、追加の指示を送ったりできます。
## 仕組み
```
┌──────────┐ SSE ┌────────────────┐ spawn ┌──────────────┐
│ Browser │ ◀──────── │ Next.js server │ ──────▶ │ codex exec │
│ (chat) │ HTTP │ (API routes) │ pipe │ (vendor/codex│
└──────────┘ └────────────────┘ └──────────────┘
```
- Next.js App Router の API ルートで `codex exec --json` を `child_process.spawn`
- 各行を JSON としてパースし、`assistant_message` / `tool_call` / `tool_result`
などの内部イベント型に変換
- ブラウザは `EventSource` で per-task の SSE エンドポイントを購読し、チャット
風に描画
## セットアップ
依存ツール: Node.js 18.18+ と git。
```bash
# 1. 依存パッケージ
npm install
# 2. openai/codex を vendor/codex にサブモジュールとして追加
npm run setup:codex
# (or: bash scripts/setup-codex.sh)
# 3. codex CLI のビルド/インストール
# openai/codex の README に従ってください。例:
# cd vendor/codex/codex-cli && npm install && npm run build
# あるいはグローバルインストール:
# npm i -g @openai/codex
# どこにバイナリがあるかに応じて CODEX_BIN を設定 (下記)。
# 4. 環境変数
cp .env.example .env.local
# OPENAI_API_KEY と必要なら CODEX_BIN を設定
# 5. 起動
npm run dev
# → http://localhost:3000
```
### 環境変数
| 変数 | 説明 |
| --- | --- |
| `CODEX_BIN` | spawn する codex バイナリへの絶対パス。未指定なら `vendor/codex/codex-cli/bin/codex.js` → `codex` (PATH) の順で解決。 |
| `CODEX_DEFAULT_CWD` | 「新しいタスク」ダイアログでデフォルトに入れる作業ディレクトリ。 |
| `OPENAI_API_KEY` | Codex CLI が呼び出すモデルの認証用 (詳細は upstream の README 参照)。 |
| `AUTH_SECRET` | **必須**。セッション/state Cookie の HMAC 署名鍵 (16 文字以上)。`openssl rand -base64 32` で生成。 |
| `OAUTH_CLIENT_ID` | **必須**。OAuth 2.0 クライアント ID。 |
| `OAUTH_CLIENT_SECRET` | confidential クライアント (Google "Web application" 型など) のみ必要。public クライアントなら省略可。 |
| `OAUTH_AUTHORIZE_URL` / `OAUTH_TOKEN_URL` / `OAUTH_USERINFO_URL` | IdP のエンドポイント。デフォルトは Google。 |
| `OAUTH_SCOPES` | 要求スコープ (デフォルト `openid email profile`)。 |
| `ALLOWED_EMAILS` | ログイン許可リスト。`alice@example.com,@your-company.com` のようにメール or `@` ドメインを並べる。未設定だと **誰でもログイン可能**。 |
| `AUTH_BASE_URL` | OAuth リダイレクト URL を組み立てるベース URL。未設定ならリクエストヘッダから自動推測 (Cloud Run など proxy 経由なら明示推奨)。 |
| `CODEXWEB_AUTH_PROXY_URL` | (preview デプロイ用) Preview の `/login` ボタンがリダイレクトする canonical (= prod) の URL。これがセットされていると preview は自分で OAuth せず prod に丸投げする。 |
| `CODEXWEB_PREVIEW_ORIGIN_PATTERN` | (prod デプロイ用) Prod が `?preview=…` 付き login を受け付けて handoff token を bounce してよい origin の正規表現。未設定だと preview への bounce を全部拒否する (= preview auth が動かない)。 |
### 認証 (OAuth 2.0 + PKCE)
Web UI と API ルートは Next.js Middleware で保護されており、未ログインだと
ブラウザは `/login` にリダイレクト、API は `401 Unauthorized` を返します。
フローは **OAuth 2.0 Authorization Code + PKCE (S256)** で常時。
`OAUTH_CLIENT_SECRET` は設定されていれば token endpoint に併送されるので、
public / confidential どちらのクライアントにも対応します。
#### IdP の選び方
| IdP | クライアント種別 | `OAUTH_CLIENT_SECRET` | メモ |
| --- | --- | --- | --- |
| Google "Web application" | confidential | 必須 | Cloud Run など HTTPS ホスティング向け。 |
| Google "Desktop app" | public | 不要 | ローカル限定 (redirect URI が loopback)。 |
| Auth0 / Keycloak / Okta / Cognito (Native/SPA) | public | 不要 | 任意 HTTPS redirect 可。 |
| Auth0 / Keycloak (Regular Web App) | confidential | 必須 | 同上。 |
#### Google "Web application" でのセットアップ
1. [Google Cloud Console](https://console.cloud.google.com/) で OAuth 2.0
クライアント ID を作成 (Application type: **Web application**)。
2. Authorized redirect URIs に下記を追加:
- `http://localhost:3000/api/auth/callback` (開発用)
- `https:///api/auth/callback` (本番用)
3. `.env.local` または Secret Manager / GitHub Variables 経由で設定:
```env
OAUTH_CLIENT_ID=
OAUTH_CLIENT_SECRET=
AUTH_SECRET=$(openssl rand -base64 32)
ALLOWED_EMAILS=you@example.com
```
#### 他の IdP の例 (Keycloak public client)
```env
OAUTH_CLIENT_ID=codexweb
# OAUTH_CLIENT_SECRET は省略 (Keycloak で public client にする)
OAUTH_AUTHORIZE_URL=https://kc.example.com/realms/main/protocol/openid-connect/auth
OAUTH_TOKEN_URL=https://kc.example.com/realms/main/protocol/openid-connect/token
OAUTH_USERINFO_URL=https://kc.example.com/realms/main/protocol/openid-connect/userinfo
```
#### 提供エンドポイント
| Method | Path | 説明 |
| --- | --- | --- |
| GET | `/api/auth/login` | IdP の同意画面へリダイレクト (`?redirect=/path` 対応) |
| GET | `/api/auth/callback` | IdP からのコールバック (内部用) |
| GET/POST | `/api/auth/logout` | セッション Cookie を破棄 |
| GET | `/api/auth/me` | 現在ログイン中のユーザ情報 (未ログインなら `{ user: null }`) |
## 使い方
1. 右上の **+ New** から新しいタスクを作成
2. プロンプトと作業ディレクトリ (絶対パス) を入力して **実行**
3. 左サイドバーにタスクが追加され、メイン領域に Codex の出力がチャット形式で
ストリーミング表示される
4. 完了後、下部のフォームから追加の指示を送れます
## API (内部)
| Method | Path | 説明 |
| --- | --- | --- |
| GET | `/api/tasks` | タスク一覧 |
| POST | `/api/tasks` | 新規タスク作成 (`prompt`, `cwd`, `model?`) |
| GET | `/api/tasks/:id` | タスク詳細 (これまでのイベント全部) |
| GET | `/api/tasks/:id/events` | SSE ストリーム |
| POST | `/api/tasks/:id/messages` | 追加メッセージ (アイドル時のみ) |
| POST | `/api/tasks/:id/stop` | 実行中のタスクを停止 |
## デプロイ
### ローカル Docker (任意)
公式のレジストリ push は行っていないので、ローカルで動作確認したい場合は
リポジトリ直下でイメージをビルドしてから起動してください。
```bash
docker build -t codexweb:local .
cat > .env <<'EOF'
OPENAI_API_KEY=sk-...
# 任意: CODEX_EXTRA_ARGS=
EOF
docker run --rm -p 3000:3000 \
--env-file .env \
-v "$PWD":/workspace \
-e CODEX_DEFAULT_CWD=/workspace \
codexweb:local
```
→ ブラウザで http://localhost:3000
### Cloud Run
`main` への push で `.github/workflows/cloudrun.yml` が起動し、Artifact
Registry にイメージを push したあと Cloud Run にデプロイします。
**シングルインスタンス前提** で動作するよう設定済み (`min-instances=1`,
`max-instances=1`, `--timeout=3600`) で、これはタスクストアが
インメモリ実装である現状の制約に合わせたものです。
#### 前提セットアップ (Google Cloud 側、一度だけ)
```bash
# 変数
PROJECT_ID=your-project
REGION=asia-northeast1
REPO=codexweb # Artifact Registry repo 名
SERVICE=codexweb # Cloud Run service 名
# API 有効化
gcloud services enable run.googleapis.com artifactregistry.googleapis.com \
secretmanager.googleapis.com iamcredentials.googleapis.com \
--project="$PROJECT_ID"
# Artifact Registry リポジトリ
gcloud artifacts repositories create "$REPO" \
--repository-format=docker --location="$REGION" --project="$PROJECT_ID"
# デプロイ用 Service Account
SA=codexweb-deployer
gcloud iam service-accounts create "$SA" --project="$PROJECT_ID"
SA_EMAIL="$SA@$PROJECT_ID.iam.gserviceaccount.com"
for role in roles/run.admin roles/artifactregistry.writer \
roles/iam.serviceAccountUser roles/secretmanager.secretAccessor; do
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
--member="serviceAccount:$SA_EMAIL" --role="$role"
done
# Workload Identity Federation (GitHub → GCP の keyless 認証)
gcloud iam workload-identity-pools create github \
--location=global --project="$PROJECT_ID"
gcloud iam workload-identity-pools providers create-oidc github \
--location=global --workload-identity-pool=github \
--issuer-uri=https://token.actions.githubusercontent.com \
--attribute-mapping='google.subject=assertion.sub,attribute.repository=assertion.repository' \
--attribute-condition='assertion.repository=="/codexweb"' \
--project="$PROJECT_ID"
PROJECT_NUM=$(gcloud projects describe "$PROJECT_ID" --format='value(projectNumber)')
gcloud iam service-accounts add-iam-policy-binding "$SA_EMAIL" \
--role=roles/iam.workloadIdentityUser \
--member="principalSet://iam.googleapis.com/projects/$PROJECT_NUM/locations/global/workloadIdentityPools/github/attribute.repository//codexweb"
# Secret Manager にシークレット投入 (Google Web app 型を使う想定)
printf "%s" "$(openssl rand -base64 32)" | gcloud secrets create AUTH_SECRET --data-file=- --project="$PROJECT_ID"
printf "%s" "" | gcloud secrets create OAUTH_CLIENT_ID --data-file=- --project="$PROJECT_ID"
printf "%s" "" | gcloud secrets create OAUTH_CLIENT_SECRET --data-file=- --project="$PROJECT_ID"
printf "%s" "sk-..." | gcloud secrets create OPENAI_API_KEY --data-file=- --project="$PROJECT_ID"
```
> **public client を使う場合**: `OAUTH_CLIENT_SECRET` シークレットを作らず、
> リポジトリ Variables で `OAUTH_USE_CLIENT_SECRET=false` をセットすると
> workflow が secret 取得をスキップします。
#### GitHub 側の設定
リポジトリ Settings → Secrets and variables → Actions → **Variables** タブで:
| Variable | 例 |
| --- | --- |
| `GCP_PROJECT_ID` | `your-project` |
| `GCP_REGION` | `asia-northeast1` |
| `ARTIFACT_REGISTRY_REPO` | `codexweb` |
| `CLOUD_RUN_SERVICE` | `codexweb` |
| `WIF_PROVIDER` | `projects//locations/global/workloadIdentityPools/github/providers/github` |
| `WIF_SERVICE_ACCOUNT` | `codexweb-deployer@.iam.gserviceaccount.com` |
| `ALLOWED_EMAILS` | (推奨) 許可メール/ドメイン |
| `AUTH_BASE_URL` | (推奨) `https://--.a.run.app` |
| `CODEXWEB_AUTH_PROXY_URL` | (PR Preview を使うなら必須) 本番の URL。例 `https://--.a.run.app` |
| `CODEXWEB_PREVIEW_ORIGIN_PATTERN` | (PR Preview を使うなら必須) `^https://codexweb-pr-\d+-[a-z0-9-]+\.[a-z0-9-]+\.run\.app$` のような preview URL の正規表現 |
| `CODEX_DEFAULT_CWD` | (任意) 例 `/tmp` |
初回デプロイで URL が確定するので、その URL を Google Cloud Console の
OAuth クライアントの "Authorized redirect URIs" にも追加してください
(`/api/auth/callback`)。あわせて `AUTH_BASE_URL` を変数にセットして
2 回目以降のデプロイで使うのが確実です。
### PR Preview
`pull_request` イベントで `.github/workflows/pr-preview.yml` が起動し、
**PR ごとに専用 Cloud Run サービス** (`codexweb-pr-`) を立てます。
本番と同じ Artifact Registry / WIF / Secret を使い回しますが、`min-instances=0`
で idle 中は無料です。タスクストアは **インメモリ** (Cloud SQL アタッチなし)
なので、preview の chat 履歴はデプロイのたびに失われます。
ライフサイクル:
- PR open / push: 自動デプロイ → URL を bot コメントで通知
- PR close (merge / 不採用問わず): Cloud Run サービスを自動削除
#### Auth (本番経由プロキシ)
Google OAuth は redirect URI のワイルドカード非対応なので、PR ごとに変わる
preview URL を都度登録するのは現実的でない。代わりに **preview は本番経由で
認証を完結させる** 仕組みになっている:
```
preview /login
└─ "Sign in" を押すと → prod /api/auth/login?preview=&redirect=...
│ state Cookie を prod ドメインに置いて Google へ
▼
Google → prod /api/auth/callback
│ token 交換 → userinfo → email allowlist
│ state.previewOrigin が pattern にマッチすれば
▼
handoff token (90s 有効) を URL に乗せて
preview /api/auth/preview-callback?token=...&redirect=...
│ AUTH_SECRET で検証
▼
Session Cookie をセットして redirect 先へ
```
- Google OAuth client に登録する redirect URI は **本番の 1 つだけ** で OK。
- 2 回目以降の preview ログインは prod 側のセッションがあれば Google を経由
せずに即 handoff される (= 1 クリックでログイン)。
- `AUTH_SECRET` は本番と preview で共有 (Secret Manager の 1 つを両方が参照)。
- preview は **`OAUTH_CLIENT_ID/SECRET` を持たない** (OAuth dance は prod 側だけ
が行うため)。
- 必須の Variable: `CODEXWEB_AUTH_PROXY_URL` (preview に渡る、prod の URL)、
`CODEXWEB_PREVIEW_ORIGIN_PATTERN` (prod に渡る、preview origin を許可する
正規表現)。詳細は下記。
#### 制約メモ
- タスクは **インメモリ** 保持なので、リビジョン入れ替え (新規デプロイ)
で進行中タスクは失われます。本番運用するなら Firestore 等への永続化が
必要 (現状未対応)。
- `--timeout=3600` を指定しているので Cloud Run としては 60 分まで SSE が
維持されます。それ以上のタスクは途中で切れるので、ローカル/専用 VM での
運用を推奨。
## トラブルシュート
### `Codex process error: spawn codex EACCES`
`spawn EACCES` は Node が **`spawn` 失敗の汎用報告フォーマット** なので、
ファイル自体ではなく `cwd` への chdir 失敗でも同じメッセージが出ます。最も
よくある原因は **bind-mount したホストディレクトリにコンテナ内ユーザーが
traverse できない** ケース。
```bash
# 直る組み合わせ (rootless Podman / Docker)
docker run --rm -p 3000:3000 --user 0 \
--env-file .env \
-v "$PWD":/workspace -e CODEX_DEFAULT_CWD=/workspace \
codexweb:local
```
最新イメージは既定で root 起動なので `--user 0` は不要 (古いタグを使って
いる場合のみ必要)。
切り分け:
```bash
# コンテナ内で codex の実体と権限を確認
docker run --rm --entrypoint sh codexweb:local -c '
set -x;
command -v codex;
ls -la "$(command -v codex)";
readlink -f "$(command -v codex)";
ls -la "$(readlink -f "$(command -v codex)")";
'
```
対処:
1. **Apple Silicon (M1/M2/M3) の Mac で動かしている場合**
ローカルビルドはホストのアーキを使うので通常そのままで動きます。
どうしてもアーキを切り替えたいときは:
```bash
docker buildx build --platform linux/arm64 -t codexweb:local --load .
# or
docker run --platform linux/arm64 ...
```
2. **手元の codex バイナリをマウントして使う**
```bash
docker run --rm -p 3000:3000 \
-e OPENAI_API_KEY=sk-... \
-e CODEX_BIN=/opt/codex/codex \
-v /usr/local/bin/codex:/opt/codex/codex:ro \
codexweb:local
```
3. **イメージ内 codex の同梱をやめて自前で用意**
```bash
docker build --build-arg INSTALL_CODEX=false -t codexweb .
# 起動時に CODEX_BIN を指定
```
### `codex_api::endpoint::responses_websocket: ... 401 Unauthorized`
Codex CLI が `wss://api.openai.com/v1/responses` への接続で 401 を返すのは、
`OPENAI_API_KEY` 環境変数だけだと WebSocket 側の認証経路が成立しないため。
`codex login --with-api-key` で `~/.codex/auth.json` に key を焼き込んでおけば
通ります。
最新イメージは entrypoint がこれを自動で行うので、`OPENAI_API_KEY` を
`.env` 経由で渡せば追加操作は不要です。手動でやる場合:
```bash
docker exec -it sh -c '
printf "%s\n" "$OPENAI_API_KEY" | codex login --with-api-key
'
```
それでも 401 が続く場合は、key が project-scoped で Responses API への
アクセスが許可されていない / アカウントの tier が足りない可能性。OpenAI
ダッシュボードで該当 project の Model permissions を確認してください。
### `spawn codex ENOENT`
PATH に `codex` がない。`CODEX_BIN` に絶対パスを指定するか、`npm i -g @openai/codex`
等でインストールしてください。
## 既知の制約 (MVP)
- タスクの永続化は **メモリのみ**。プロセス再起動で消えます。
- マルチターン対話は、各ターンで `codex exec` を新規プロセスとして起動する
シンプル実装です (Codex 側の会話履歴は CLI の挙動に依存)。
- 認証は Google OAuth のみサポート (`ALLOWED_EMAILS` でアカウント制限可)。
- Codex CLI の `--json` 出力スキーマはバージョンにより差異があり、未知の
イベントは `stdout` として表示されます (`lib/codex-runner.ts` の
`mapCodexEvent` で必要に応じて拡張可能)。