From 0f2b199fdedcc7695f1460eccbfbb83b7feb3b65 Mon Sep 17 00:00:00 2001 From: Zyy Date: Tue, 16 Jun 2026 17:46:48 +0800 Subject: [PATCH] docs: add architecture overview Co-authored-by: Codex --- .dockerignore | 18 + .gitignore | 2 + designs/codex-proxy-rs-overview-final.pen | 644 ++++++++++++++++++++ docs/architecture.md | 679 ++++++++++++++++++++++ 4 files changed, 1343 insertions(+) create mode 100644 .dockerignore create mode 100644 docs/architecture.md diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..32c126b79 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,18 @@ +target +web/node_modules +web/dist +data +logs +local.yaml +local.yml +.git +.codegraph +.worktrees +worktrees +*.sqlite +*.sqlite3 +*.db +*.db-shm +*.db-wal +*.log +*.tmp diff --git a/.gitignore b/.gitignore index 942e3a4bf..ef180eb04 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,5 @@ *.codegraph */node_modules */dist +.agents +.cargo \ No newline at end of file diff --git a/designs/codex-proxy-rs-overview-final.pen b/designs/codex-proxy-rs-overview-final.pen index b4fcf376b..34202bc99 100644 --- a/designs/codex-proxy-rs-overview-final.pen +++ b/designs/codex-proxy-rs-overview-final.pen @@ -24752,6 +24752,650 @@ ] } ] + }, + { + "type": "frame", + "id": "ezRqM", + "x": 16020, + "y": 0, + "name": "登录页 - Route Chamber", + "clip": true, + "width": 1920, + "height": 1080, + "fill": { + "type": "gradient", + "gradientType": "linear", + "enabled": true, + "rotation": 115, + "size": { + "height": 1 + }, + "colors": [ + { + "color": "#F8FAFC", + "position": 0 + }, + { + "color": "#FFFFFF", + "position": 0.46 + }, + { + "color": "#F1F7FF", + "position": 1 + } + ] + }, + "gap": 88, + "padding": [ + 72, + 96 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "fptiY", + "name": "左侧律动区域", + "width": 980, + "height": "fill_container", + "fill": "#00000000", + "stroke": "#00000000", + "effect": { + "type": "shadow", + "shadowType": "outer", + "enabled": false, + "color": "#000000" + }, + "layout": "none", + "children": [ + { + "type": "frame", + "id": "hGhiA", + "x": 44, + "y": 42, + "name": "左侧品牌", + "width": 420, + "height": 52, + "fill": "#00000000", + "gap": 13, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "k88pD", + "name": "左侧品牌图标底", + "width": 44, + "height": 44, + "fill": "#111827", + "cornerRadius": 13, + "layout": "none", + "children": [ + { + "type": "icon", + "id": "inEW3", + "x": 11, + "y": 11, + "name": "左侧品牌图标", + "width": 22, + "height": 22, + "icon": "square-terminal", + "library": "lucide", + "fill": "#FFFFFF" + } + ] + }, + { + "type": "frame", + "id": "XNdOV", + "name": "左侧品牌文字", + "height": 42, + "fill": "#00000000", + "layout": "vertical", + "gap": 4, + "justifyContent": "center", + "children": [ + { + "type": "text", + "id": "PMXVU", + "name": "左侧品牌名称", + "fill": "#0E1726", + "content": "Codex Proxy RS", + "lineHeight": 1.1, + "fontFamily": "Inter", + "fontSize": 17, + "fontWeight": "760" + }, + { + "type": "text", + "id": "ImuER", + "name": "左侧品牌版本", + "fill": "#64748B", + "content": "Proxy RS · v0.1.0", + "lineHeight": 1.1, + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "650" + } + ] + } + ] + }, + { + "type": "frame", + "id": "ffamL", + "x": 56, + "y": 744, + "name": "左侧短文案", + "width": 580, + "height": 128, + "fill": "#00000000", + "layout": "vertical", + "gap": 14, + "children": [ + { + "type": "text", + "id": "VNWwS", + "name": "左侧标题", + "fill": "#0E1726", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "安静进入控制台。", + "lineHeight": 1.08, + "fontFamily": "Inter", + "fontSize": 38, + "fontWeight": "780" + }, + { + "type": "text", + "id": "cRXIR", + "name": "左侧说明", + "fill": "#64748B", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "几条请求轨迹,连接账号池、模型目录与管理入口。", + "lineHeight": 1.45, + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "500" + } + ] + }, + { + "type": "path", + "id": "i0RKwl", + "x": 8, + "y": 264, + "name": "信号谱线 蓝主线", + "geometry": "M0 134c154-104 304-80 440-18 152 70 286 62 470-62", + "viewBox": [ + 0, + 0, + 910, + 238 + ], + "fill": "#00000000", + "width": 910, + "height": 238, + "stroke": "#2563EB", + "strokeWidth": 2.2, + "strokeLinejoin": "round", + "strokeLinecap": "round" + }, + { + "type": "path", + "id": "t7Obu", + "x": 24, + "y": 356, + "name": "信号谱线 青主线", + "geometry": "M0 62c138 88 286 92 424 32 156-68 284-46 446 56", + "viewBox": [ + 0, + 0, + 870, + 210 + ], + "fill": "#00000000", + "width": 870, + "height": 210, + "stroke": "#0F9F9A", + "strokeWidth": 2, + "strokeLinejoin": "round", + "strokeLinecap": "round" + }, + { + "type": "path", + "id": "Ih3hQ", + "x": 42, + "y": 304, + "name": "信号谱线 轻回声一", + "geometry": "M0 118c132-64 266-42 392 6 148 54 274 32 428-38", + "viewBox": [ + 0, + 0, + 820, + 210 + ], + "fill": "#00000000", + "width": 820, + "height": 210, + "stroke": "#93C5FD66", + "strokeWidth": 1.2, + "strokeLinejoin": "round", + "strokeLinecap": "round" + }, + { + "type": "path", + "id": "SlACI", + "x": 60, + "y": 460, + "name": "信号谱线 轻回声二", + "geometry": "M0 78c118-36 224 40 352 0 148-46 280 18 428-16", + "viewBox": [ + 0, + 0, + 780, + 178 + ], + "fill": "#00000000", + "width": 780, + "height": 178, + "stroke": "#99F6E466", + "strokeWidth": 1.2, + "strokeLinejoin": "round", + "strokeLinecap": "round" + }, + { + "type": "path", + "id": "dgWuE", + "x": 96, + "y": 574, + "name": "信号谱线 静默底线", + "geometry": "M0 62c108 26 192-32 316-4 126 28 232-16 364 8", + "viewBox": [ + 0, + 0, + 680, + 130 + ], + "fill": "#00000000", + "width": 680, + "height": 130, + "stroke": "#CBD5E180", + "strokeWidth": 1.1, + "strokeLinejoin": "round", + "strokeLinecap": "round" + }, + { + "type": "ellipse", + "id": "wgWMY", + "x": 446, + "y": 378, + "name": "谱线节点 蓝", + "fill": "#2563EB", + "width": 7, + "height": 7, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#2563EB66", + "blur": 14 + } + }, + { + "type": "ellipse", + "id": "D17Lp", + "x": 602, + "y": 448, + "name": "谱线节点 青", + "fill": "#0F9F9A", + "width": 7, + "height": 7, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#0F9F9A66", + "blur": 14 + } + }, + { + "type": "ellipse", + "id": "TxcTy", + "x": 262, + "y": 338, + "name": "谱线节点 淡", + "fill": "#94A3B8", + "width": 4, + "height": 4 + } + ] + }, + { + "type": "frame", + "id": "fmwBG", + "name": "右侧登录区", + "width": 520, + "height": "fill_container", + "fill": "#00000000", + "layout": "vertical", + "gap": 22, + "justifyContent": "center", + "children": [ + { + "type": "frame", + "id": "LBb4m", + "name": "登录表单", + "width": 480, + "fill": "#FFFFFF", + "cornerRadius": 22, + "stroke": "#E2E8F0", + "strokeWidth": 1, + "effect": [ + { + "type": "shadow", + "shadowType": "outer", + "color": "#0E17261F", + "offset": { + "x": 0, + "y": 12 + }, + "blur": 26, + "spread": -18 + }, + { + "type": "shadow", + "shadowType": "outer", + "color": "#0E172629", + "offset": { + "x": 0, + "y": 34 + }, + "blur": 70, + "spread": -40 + } + ], + "layout": "vertical", + "gap": 22, + "padding": 36, + "children": [ + { + "type": "frame", + "id": "Vh4qO", + "name": "登录表单头部", + "width": "fill_container", + "height": 84, + "fill": "#00000000", + "layout": "vertical", + "gap": 10, + "children": [ + { + "type": "text", + "id": "WKbRM", + "name": "登录标题", + "fill": "#0E1726", + "content": "登录控制台", + "lineHeight": 1.12, + "fontFamily": "Inter", + "fontSize": 34, + "fontWeight": "800" + }, + { + "type": "text", + "id": "azMth", + "name": "登录说明", + "fill": "#64748B", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "使用管理员账号和访问密钥进入 Codex Proxy RS。", + "lineHeight": 1.55, + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "Rre6N", + "name": "管理员账号", + "width": "fill_container", + "height": 72, + "fill": "#00000000", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "text", + "id": "OGamD", + "name": "管理员账号 标签", + "fill": "#0E1726", + "content": "管理员账号", + "lineHeight": 1.1, + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "700" + }, + { + "type": "frame", + "id": "alIMQ", + "name": "管理员账号 输入框", + "width": "fill_container", + "height": 46, + "fill": "#EEF2F7", + "cornerRadius": 12, + "gap": 10, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "Y2gc9C", + "name": "管理员账号 图标", + "width": 17, + "height": 17, + "icon": "mail", + "library": "lucide", + "fill": "#64748B" + }, + { + "type": "text", + "id": "DrsCN", + "name": "管理员账号 值", + "fill": "#0E1726", + "content": "admin@example.com", + "lineHeight": 1.1, + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + } + ] + }, + { + "type": "frame", + "id": "uOcjS", + "name": "访问密钥", + "width": "fill_container", + "height": 72, + "fill": "#00000000", + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "text", + "id": "GQaue", + "name": "访问密钥 标签", + "fill": "#0E1726", + "content": "访问密钥", + "lineHeight": 1.1, + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "700" + }, + { + "type": "frame", + "id": "ZTZ2u", + "name": "访问密钥 输入框", + "width": "fill_container", + "height": 46, + "fill": "#EEF2F7", + "cornerRadius": 12, + "gap": 10, + "padding": [ + 0, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "BWHck", + "name": "访问密钥 图标", + "width": 17, + "height": 17, + "icon": "key-round", + "library": "lucide", + "fill": "#64748B" + }, + { + "type": "text", + "id": "U168Qy", + "name": "访问密钥 值", + "fill": "#94A3B8", + "content": "输入会话密钥", + "lineHeight": 1.1, + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "500" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Z8uAB", + "name": "表单选项", + "width": "fill_container", + "height": 30, + "fill": "#00000000", + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "V6QXQ", + "name": "保持登录", + "height": 22, + "fill": "#00000000", + "gap": 8, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "XqPpK", + "name": "保持登录勾选框", + "width": 16, + "height": 16, + "fill": "#FFFFFF", + "cornerRadius": 5, + "stroke": "#CBD5E1", + "strokeWidth": 1 + }, + { + "type": "text", + "id": "fqCp3", + "name": "保持登录文字", + "fill": "#64748B", + "content": "保持登录", + "lineHeight": 1.1, + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "650" + } + ] + }, + { + "type": "text", + "id": "rVrLI", + "name": "忘记密钥", + "fill": "#2563EB", + "content": "忘记密钥", + "lineHeight": 1.1, + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "700" + } + ] + }, + { + "type": "frame", + "id": "Q2vJi", + "name": "登录按钮", + "width": "fill_container", + "height": 48, + "fill": "#2563EB", + "cornerRadius": 12, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#2563EB66", + "offset": { + "x": 0, + "y": 12 + }, + "blur": 24, + "spread": -16 + }, + "gap": 10, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ka5Jn", + "name": "登录按钮文字", + "fill": "#FFFFFF", + "content": "登录", + "lineHeight": 1.1, + "fontFamily": "Inter", + "fontSize": 15, + "fontWeight": "760" + }, + { + "type": "icon", + "id": "j7l5g", + "name": "登录按钮图标", + "width": 18, + "height": 18, + "icon": "arrow-right", + "library": "lucide", + "fill": "#FFFFFF" + } + ] + }, + { + "type": "text", + "id": "dZM17", + "name": "登录安全说明", + "fill": "#64748B", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "本地会话将加密保存,仅用于当前管理端。", + "lineHeight": 1.35, + "textAlign": "center", + "fontFamily": "Inter", + "fontSize": 11, + "fontWeight": "600" + } + ] + } + ] + } + ] } ], "variables": { diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 000000000..49aa0419e --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,679 @@ +# Architecture + +This document is the canonical Rust workspace architecture for +`codex-proxy-rs`. The Rust implementation must converge to this directory +shape and naming exactly. A migration is not complete while any extra Rust +source file, extra Rust source directory, or forbidden transitional name remains. + +This document intentionally governs Rust workspace source layout. The `web/` +frontend has its own package layout and is not part of the Rust source +white-list below. + +## Architectural Style + +The application is a modular monolith with hexagonal boundaries. + +- `core` owns domain model, use cases, protocol semantics, policy, and ports. +- `adapters` owns concrete implementations of `core` ports. +- `runtime` owns composition, application state, and background task wiring. +- `server` owns the Axum HTTP boundary. +- `platform` owns shared infrastructure primitives. +- `assets` owns compiled frontend asset serving. +- `xtask` owns local automation. + +## Non-Negotiable Rules + +- Rust source directories and files must match this document exactly at + completion. +- Do not keep transitional module names after migration. +- Do not hide structure drift behind compatibility re-exports. +- `core` must not depend on any project crate in the final architecture. +- `core` must not own concrete SQLx, Reqwest, Axum, filesystem path, TLS, or + upstream IO implementations. +- `server` must not decide business policy. +- `runtime` must not contain raw SQL, HTTP handlers, or domain rules. +- `adapters` must not define domain policy. +- Architecture tests must enforce directory shape, dependency direction, + forbidden names, and forbidden imports. + +## Root Workspace Layout + +```text +codex-proxy-rs/ +├── Cargo.toml +├── Cargo.lock +├── README.md +├── AGENTS.md +├── src/ +│ └── lib.rs +├── crates/ +│ ├── core/ +│ ├── adapters/ +│ ├── runtime/ +│ ├── server/ +│ ├── platform/ +│ ├── assets/ +│ └── xtask/ +├── tests/ +├── web/ +└── docs/ +``` + +Root `src/lib.rs` is the only root facade. The root `Cargo.toml` must use the +default library path or explicitly point to `src/lib.rs`. Files such as +`crates/server/facade.rs` are forbidden. + +## Dependency Direction + +Final dependency graph: + +```text +root facade -> server + +server -> runtime + core + assets + platform +runtime -> core + adapters + platform +adapters -> core + platform +core -> no project crate dependencies +assets -> no project crate dependencies +xtask -> workspace tooling only +platform -> no domain crate dependencies +``` + +Forbidden project-crate dependencies: + +- `core` must not depend on `platform`, `server`, `runtime`, `adapters`, or + `assets`. +- `platform` must not depend on `core`, `server`, `runtime`, `adapters`, or + `assets`. +- `adapters` must not depend on `runtime` or `server`. +- `runtime` must not depend on `server`. +- `assets` must not depend on project crates. + +`runtime` maps `platform` configuration into `core` configuration DTOs before +constructing core services. This keeps `core` independent from config loading, +filesystem paths, environment variables, and storage bootstrap concerns. + +## Crate Responsibilities + +### `crates/core` + +Owns domain concepts, invariants, use case orchestration, protocol semantics, +and ports. + +Allowed: + +- account pool and scheduling policy +- admin business use cases +- authentication/session domain behavior +- model catalog rules +- usage and event policy +- OpenAI/Codex protocol data structures and pure conversion +- request dispatch, fallback, retry, quota, affinity, recovery, and usage policy +- port traits +- typed domain and application errors + +Forbidden: + +- Axum extractors, routers, responses, or `IntoResponse` +- SQLx pools, queries, rows, transactions, or migrations +- concrete `reqwest::Client` +- concrete WebSocket/TLS connection setup +- concrete filesystem path decisions +- platform config loader/types +- public re-exports of project crates + +### `crates/adapters` + +Owns concrete implementations of ports defined by `core`. + +Allowed: + +- SQLx implementations of storage ports +- encrypted SQLite stores +- Reqwest HTTP clients +- concrete OpenAI OAuth client +- concrete Codex upstream HTTP/SSE/WebSocket clients +- concrete fingerprint update client +- adapter-specific translation from framework errors into `core` errors + +Forbidden: + +- business policy +- Axum route handlers +- runtime task orchestration + +### `crates/runtime` + +Owns application composition. + +Allowed: + +- `AppState` +- service construction +- repository construction +- adapter construction +- dependency injection +- background task startup and shutdown +- startup restore flows +- platform-to-core config mapping + +Forbidden: + +- HTTP request/response mapping +- raw SQL query strings +- concrete upstream protocol side effects +- account selection, fallback, quota, model, or recovery policy + +### `crates/server` + +Owns the HTTP boundary. + +Allowed: + +- Axum routers and route handlers +- extractors and middleware +- request IDs, tracing, CORS, and auth extraction +- admin response envelopes +- OpenAI-compatible HTTP response bodies +- HTTP status and header mapping +- SSE framing for client responses + +Forbidden: + +- SQL queries +- concrete upstream Codex IO +- account selection, fallback, quota, model, or recovery policy +- background task orchestration + +### `crates/platform` + +Owns cross-cutting infrastructure primitives. + +Allowed: + +- config loading and config schema types +- crypto primitives +- identity hashing primitives +- SQLite connection setup and schema files +- filesystem path primitives +- logging primitives +- JSON and pagination helpers + +Forbidden: + +- Codex/OpenAI business use cases +- admin business use cases +- Axum handlers +- implementations of `core` ports + +### `crates/assets` + +Owns static frontend asset serving. + +Allowed: + +- root `index.html` +- `/assets/*` +- SPA fallback after API routes +- static cache and security headers + +### `crates/xtask` + +Owns local automation. + +Allowed: + +- frontend build orchestration +- architecture checks +- release/package commands +- developer maintenance commands + +## Exact Rust Source Shape + +### Root Facade + +```text +src/ +└── lib.rs +``` + +### Core + +```text +crates/core/ +├── Cargo.toml +└── src/ + ├── lib.rs + ├── error.rs + ├── admin/ + │ ├── mod.rs + │ ├── ports.rs + │ ├── auth.rs + │ ├── client_keys.rs + │ └── settings.rs + ├── accounts/ + │ ├── mod.rs + │ ├── model.rs + │ ├── ports.rs + │ ├── service.rs + │ ├── pool.rs + │ ├── lifecycle.rs + │ ├── cookies.rs + │ ├── jwt.rs + │ └── usage.rs + ├── auth/ + │ ├── mod.rs + │ ├── ports.rs + │ ├── oauth.rs + │ └── session.rs + ├── models/ + │ ├── mod.rs + │ ├── model.rs + │ ├── ports.rs + │ ├── catalog.rs + │ └── service.rs + ├── events/ + │ ├── mod.rs + │ ├── model.rs + │ ├── ports.rs + │ └── service.rs + ├── usage/ + │ ├── mod.rs + │ ├── model.rs + │ ├── ports.rs + │ └── service.rs + ├── protocol/ + │ ├── mod.rs + │ ├── openai/ + │ │ ├── mod.rs + │ │ ├── chat.rs + │ │ ├── responses.rs + │ │ ├── models.rs + │ │ ├── errors.rs + │ │ └── schema.rs + │ └── codex/ + │ ├── mod.rs + │ ├── chat.rs + │ ├── responses.rs + │ ├── events.rs + │ ├── sse.rs + │ ├── websocket.rs + │ └── schema.rs + ├── gateway/ + │ ├── mod.rs + │ ├── ports.rs + │ ├── fingerprint.rs + │ ├── conversation.rs + │ └── installation.rs + └── serving/ + ├── mod.rs + ├── chat.rs + ├── responses.rs + ├── errors.rs + ├── routing.rs + ├── fallback.rs + ├── affinity.rs + ├── quota.rs + ├── stream.rs + ├── recovery.rs + └── usage.rs +``` + +`protocol` contains only protocol data structures and pure conversion. +`gateway` contains upstream-facing ports and transport-neutral request context. +Concrete HTTP, SSE, WebSocket, TLS, and cookie-jar behavior lives in adapters. + +### Adapters + +```text +crates/adapters/ +├── Cargo.toml +└── src/ + ├── lib.rs + ├── sqlite/ + │ ├── mod.rs + │ ├── accounts.rs + │ ├── account_tokens.rs + │ ├── account_usage.rs + │ ├── refresh_leases.rs + │ ├── cookies.rs + │ ├── events.rs + │ ├── models.rs + │ ├── session_affinity.rs + │ ├── admin_sessions.rs + │ └── client_keys.rs + ├── http/ + │ ├── mod.rs + │ ├── reqwest_client.rs + │ └── headers.rs + ├── codex/ + │ ├── mod.rs + │ ├── client.rs + │ ├── sse.rs + │ ├── models.rs + │ ├── fingerprint.rs + │ └── websocket/ + │ ├── mod.rs + │ ├── client.rs + │ ├── pool.rs + │ ├── codec.rs + │ ├── deflate.rs + │ └── opening.rs + └── oauth/ + ├── mod.rs + └── openai.rs +``` + +### Runtime + +```text +crates/runtime/ +├── Cargo.toml +└── src/ + ├── lib.rs + ├── bootstrap.rs + ├── state.rs + ├── services.rs + ├── repositories.rs + ├── upstream.rs + ├── config.rs + └── tasks/ + ├── mod.rs + ├── coordinator.rs + ├── token_refresh.rs + ├── quota_refresh.rs + ├── model_refresh.rs + ├── cookie_cleanup.rs + ├── session_cleanup.rs + └── fingerprint_update.rs +``` + +`runtime/src/config.rs` maps `platform` configuration into `core` configuration +DTOs. + +### Server + +```text +crates/server/ +├── Cargo.toml +└── src/ + ├── main.rs + ├── lib.rs + ├── router.rs + ├── error/ + │ ├── mod.rs + │ ├── admin.rs + │ └── openai.rs + ├── middleware/ + │ ├── mod.rs + │ ├── request_id.rs + │ ├── trace.rs + │ ├── auth.rs + │ └── cors.rs + ├── admin_api/ + │ ├── mod.rs + │ ├── router.rs + │ ├── response.rs + │ ├── session.rs + │ ├── settings.rs + │ ├── diagnostics.rs + │ ├── models.rs + │ ├── usage.rs + │ ├── accounts/ + │ │ ├── mod.rs + │ │ ├── list.rs + │ │ ├── create.rs + │ │ ├── import.rs + │ │ ├── export.rs + │ │ ├── lifecycle.rs + │ │ ├── quota.rs + │ │ ├── cookies.rs + │ │ ├── oauth.rs + │ │ └── health.rs + │ ├── client_keys/ + │ │ ├── mod.rs + │ │ ├── list.rs + │ │ ├── create.rs + │ │ ├── import.rs + │ │ ├── export.rs + │ │ └── lifecycle.rs + │ └── logs/ + │ ├── mod.rs + │ ├── query.rs + │ ├── detail.rs + │ └── state.rs + └── openai_api/ + ├── mod.rs + ├── router.rs + ├── auth.rs + ├── chat.rs + ├── responses.rs + ├── models.rs + ├── diagnostics.rs + ├── error.rs + └── sse.rs +``` + +`server/src/error/openai.rs` maps application errors into OpenAI-compatible HTTP +responses. `server/src/openai_api/error.rs` and `server/src/openai_api/sse.rs` +own route-local OpenAI API error/event framing. + +### Platform + +```text +crates/platform/ +├── Cargo.toml +└── src/ + ├── lib.rs + ├── config/ + │ ├── mod.rs + │ ├── loader.rs + │ └── types.rs + ├── crypto/ + │ ├── mod.rs + │ ├── secret_box.rs + │ └── hash.rs + ├── identity/ + │ ├── mod.rs + │ ├── admin_password.rs + │ └── client_key.rs + ├── storage/ + │ ├── mod.rs + │ ├── sqlite.rs + │ ├── schema.sql + │ └── paths.rs + ├── logging/ + │ ├── mod.rs + │ └── rotation.rs + └── json/ + ├── mod.rs + └── pagination.rs +``` + +### Assets + +```text +crates/assets/ +├── Cargo.toml +└── src/ + ├── lib.rs + ├── router.rs + └── headers.rs +``` + +### Xtask + +```text +crates/xtask/ +├── Cargo.toml +└── src/ + ├── main.rs + ├── build_web.rs + ├── check_architecture.rs + └── release.rs +``` + +## Cargo Integration Test Shape + +Cargo integration test entry files must remain at `tests/*.rs`; submodules live +under matching directories. + +```text +tests/ +├── admin.rs +├── openai_api.rs +├── accounts.rs +├── codex_upstream.rs +├── runtime.rs +├── architecture.rs +├── admin/ +├── openai_api/ +├── accounts/ +├── codex_upstream/ +├── runtime/ +├── architecture/ +│ ├── mod.rs +│ ├── dependency_direction.rs +│ ├── forbidden_imports.rs +│ └── directory_shape.rs +└── support/ + ├── mod.rs + ├── app.rs + ├── fixtures.rs + └── wiremock.rs +``` + +Additional fixture files are allowed under `tests/fixtures/`. + +## Docs Shape + +```text +docs/ +├── architecture.md +├── adr/ +├── api/ +└── operations/ +``` + +Historical migration plans may remain under `docs/superpowers/` while active +development is ongoing. They are not part of the final architecture white-list. + +## Web Shape + +The `web/` directory is outside this Rust workspace architecture white-list. +It is governed by frontend build tooling and must not be checked by Rust +directory-shape tests. `web/node_modules/` and `web/dist/` are build artifacts +from the frontend toolchain, not Rust architecture inputs. + +## Required Names + +- Root facade: `src/lib.rs` +- HTTP admin boundary: `server/src/admin_api` +- HTTP OpenAI-compatible boundary: `server/src/openai_api` +- Core port files: `ports.rs` +- Concrete SQLite adapters: `adapters/src/sqlite` +- Concrete OAuth adapter: `adapters/src/oauth/openai.rs` +- Concrete Codex upstream adapters: `adapters/src/codex` +- Concrete Codex WebSocket adapter: `adapters/src/codex/websocket` +- Shared middleware: `server/src/middleware` +- HTTP response errors: `server/src/error` +- OpenAI API route-local SSE framing: `server/src/openai_api/sse.rs` +- Business errors: `core/src/error.rs` or domain-local `errors.rs` + +## Forbidden Names + +- `crates/admin` +- `crates/server/facade.rs` +- `server/src/admin.rs` +- `server/src/codex_http` +- `server/src/openai_api/stream.rs` +- `core/src/**/repository.rs` +- `core/src/**/repository/` +- `adapters/src/*_repository.rs` +- `runtime/src/*_repository.rs` +- `core/src/codex/gateway/transport` +- `core/src/codex/gateway/oauth/client.rs` +- `core/src/codex/serving/http` +- `platform/src/platform` +- `platform/src/utils` + +## Port Placement + +Ports are traits owned by `core`. + +- account storage ports live in `core/src/accounts/ports.rs` +- admin session/client-key ports live in `core/src/admin/ports.rs` +- OAuth and token refresh ports live in `core/src/auth/ports.rs` +- model snapshot ports live in `core/src/models/ports.rs` +- event log ports live in `core/src/events/ports.rs` +- usage ports live in `core/src/usage/ports.rs` +- upstream Codex and fingerprint ports live in `core/src/gateway/ports.rs` + +Concrete implementations live in `adapters`. + +- `SqliteAccountStore` lives in `adapters/src/sqlite/accounts.rs` +- `SqliteAdminSessionStore` lives in `adapters/src/sqlite/admin_sessions.rs` +- `OpenAiOAuthClient` lives in `adapters/src/oauth/openai.rs` +- `ReqwestCodexClient` lives in `adapters/src/codex/client.rs` +- `CodexWebSocketPool` implementation lives in `adapters/src/codex/websocket/pool.rs` + +## Migration Rules + +Every migration step must move the codebase closer to this document. + +Required order: + +1. Add architecture tests for directory shape and forbidden names. +2. Move the root facade to `src/lib.rs`. +3. Rename server HTTP boundaries to `admin_api` and `openai_api`. +4. Move admin business code into `core/src/admin`. +5. Move all core repository traits into `ports.rs`. +6. Move all SQLx implementations into `adapters/src/sqlite`. +7. Move all Reqwest/SSE/WebSocket/OAuth implementations into `adapters`. +8. Collapse runtime into composition-only files. +9. Collapse platform into config/crypto/identity/storage/logging/json primitives. +10. Update facade exports after internal structure is correct. +11. Run full verification only after the structure is fully aligned. + +Do not use transitional aliases as a substitute for migration. A transitional +alias is acceptable only if it is removed in the same migration series before +claiming completion. + +## Architecture Tests + +Architecture tests must enforce: + +- every required Rust source file exists +- every required Rust source directory exists +- forbidden Rust source files and directories do not exist +- no extra Rust source files exist under `src/` and `crates/*/src/` +- no extra Rust source directories exist under `src/` and `crates/*/src/` +- `Cargo.toml` workspace members match this document +- root library path is `src/lib.rs` +- `core` has no project-crate dependencies +- `core` has no `axum`, `sqlx`, or concrete `reqwest::Client` +- `server` has no SQLx queries +- `runtime` has no SQLx query strings and no Axum handlers +- `adapters` owns concrete SQLx/Reqwest/OAuth/Codex IO +- `core` ports are declared in `ports.rs` +- root facade does not hide invalid internal directory names + +The required architecture test files are: + +```text +tests/architecture/dependency_direction.rs +tests/architecture/forbidden_imports.rs +tests/architecture/directory_shape.rs +``` + +## Completion Criteria + +The migration is complete only when all of the following are true: + +- The Rust workspace source tree matches this document exactly. +- Forbidden names are absent from Rust source directories. +- Architecture tests enforce this document. +- `cargo fmt --check` passes. +- `cargo check --workspace --all-targets` passes. +- `cargo test --workspace` passes. +- `cargo clippy --workspace --all-targets --all-features --locked -- -D warnings` passes.