From 79bdaf3818ecd3940e5ddbdc0d3da8a6eabb63e9 Mon Sep 17 00:00:00 2001 From: "zhang,zhao" <905781330@qq.com> Date: Tue, 14 Jul 2026 10:50:57 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=E6=96=87=E6=A1=A3=EF=BC=9A=E8=AE=BE?= =?UTF-8?q?=E8=AE=A1=20Docker=20=E6=A8=A1=E5=BC=8F=20Codex=20CLI=20?= =?UTF-8?q?=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2026-07-14-docker-codex-cli-design.md | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-14-docker-codex-cli-design.md diff --git a/docs/superpowers/specs/2026-07-14-docker-codex-cli-design.md b/docs/superpowers/specs/2026-07-14-docker-codex-cli-design.md new file mode 100644 index 0000000..2a57121 --- /dev/null +++ b/docs/superpowers/specs/2026-07-14-docker-codex-cli-design.md @@ -0,0 +1,69 @@ +# Docker 模式支持 Codex CLI 设计 + +## 背景 + +TickFlow 的 Docker 服务运行在独立容器中。即使 macOS 主机已经安装并登录 Codex CLI,容器内的 `shutil.which("codex")` 仍无法找到主机命令,因此设置页显示“未找到 Codex CLI 命令: codex”。 + +## 目标 + +- Docker 镜像内提供可直接执行的 `codex` 命令。 +- 复用主机已有的 Codex 登录凭据,无需在容器内重复交互式登录。 +- 保持现有 `codex exec`、临时工作目录、只读沙箱和隔离 `CODEX_HOME` 逻辑不变。 +- 保持镜像构建可复现,并允许维护者显式升级 Codex CLI 版本。 + +## 非目标 + +- 不允许用户配置任意可执行文件路径。 +- 不改变本机开发模式和桌面客户端的 Codex 命令解析逻辑。 +- 不把 Codex 凭据复制进镜像或提交到仓库。 +- 不为 Dockerfile 文本结构增加脆弱的单元测试。 + +## 方案 + +### 镜像构建 + +Dockerfile 新增独立的 `codex-builder` 阶段,使用 Node bookworm 镜像安装固定版本的 `@openai/codex`。版本由 `CODEX_CLI_VERSION` 构建参数控制,并提供项目验证过的默认值。 + +运行阶段从构建阶段复制 Codex 包,并在 `/usr/local/bin/codex` 提供入口。运行镜像继续使用已有的 Debian Node.js 运行时,不额外保留 npm,从而减少运行层体积和可变依赖。 + +### 凭据挂载 + +`docker-compose.yml` 将主机 `${HOME}/.codex` 挂载到容器 `/root/.codex`,模式为只读。挂载目录不会写入镜像或仓库。 + +后端现有 `_prepare_codex_home` 会从只读挂载中读取 `auth.json` 和兼容配置,再复制或生成到单次请求的临时 `CODEX_HOME`。Codex 子进程只使用临时目录,因此不会修改主机凭据目录。 + +### 数据流 + +1. Compose 启动容器并只读挂载主机 Codex home。 +2. 设置接口调用 `codex_cli_available()`。 +3. `_resolve_command("codex")` 在容器 PATH 中找到镜像内入口。 +4. 发起 AI 请求时,后端建立临时工作区和临时 `CODEX_HOME`。 +5. 后端复用挂载目录中的认证信息并执行 `codex exec`。 +6. 请求结束后临时目录自动删除。 + +## 错误处理 + +- 主机未安装 Codex 不影响容器命令,因为 CLI 已包含在镜像中。 +- 主机未登录或 `${HOME}/.codex/auth.json` 不存在时,设置页仍可识别 CLI;实际调用会返回现有的 Codex 登录错误。 +- 构建时无法下载指定 npm 包时,镜像构建应失败,不静默切换到不固定版本。 +- 挂载目录保持只读;任何意外写入都会由容器文件系统拒绝。 + +## 安全性 + +- Codex home 仅以只读方式挂载。 +- 凭据不进入 Docker build context 的产物层。 +- 现有 `--ephemeral`、`--sandbox read-only`、`approval_policy = "never"` 和空白临时工作区保持不变。 +- 文档明确说明:启用 Docker Codex 模式意味着 TickFlow 容器能够读取 Codex 登录凭据,应仅在受信任的本机环境使用。 + +## 验证 + +1. 构建镜像成功。 +2. 容器内 `command -v codex` 返回 `/usr/local/bin/codex`。 +3. 容器内 `codex --version` 返回设计中固定的版本。 +4. 设置接口返回 `ai_configured: true`,页面不再显示“未找到 Codex CLI 命令”。 +5. 执行一次真实连接测试,确认 `codex exec` 能读取认证并返回内容。 +6. 运行现有后端 AI provider 测试,确认非 Docker 路径无回归。 + +## 文档更新 + +README 的 Docker 启动说明增加 Codex CLI 凭据挂载、安全边界和版本覆盖方式,避免用户把“主机已安装”误认为“容器自动可见”。 From 8206a6010d2ccc89a64626b3c94608633e076704 Mon Sep 17 00:00:00 2001 From: "zhang,zhao" <905781330@qq.com> Date: Tue, 14 Jul 2026 11:23:33 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9ADocker=20?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E6=94=AF=E6=8C=81=20Codex=20CLI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 20 ++++++++++++++++++++ docker-compose.yml | 3 +++ 2 files changed, 23 insertions(+) diff --git a/Dockerfile b/Dockerfile index 62b8629..58fd5a0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,7 @@ ARG PYPI_INDEX=https://pypi.tuna.tsinghua.edu.cn/simple # 备用 PyPI 源:主源同步延迟/故障时自动兜底(阿里云与清华互为补充) ARG PYPI_FALLBACK=https://mirrors.aliyun.com/pypi/simple ARG BACKEND_EXTRAS= +ARG CODEX_CLI_VERSION=0.144.3 # === Stage 1: 前端构建 === FROM node:20-alpine AS frontend-builder @@ -46,6 +47,20 @@ RUN if [ "$INCLUDE_STOCKSDK" = "1" ]; then \ mkdir -p /build/node_modules; \ fi +# === Stage 1c: Codex CLI === +# 固定版本保证镜像可复现;只复制安装产物到运行镜像,不保留 npm。 +FROM node:20-bookworm-slim AS codex-builder +ARG USE_CN_MIRROR=1 +ARG NPM_REGISTRY=https://registry.npmmirror.com +ARG CODEX_CLI_VERSION=0.144.3 +RUN if [ "$USE_CN_MIRROR" = "1" ]; then npm config set registry "$NPM_REGISTRY"; fi \ + && npm install --global --prefix /opt/codex "@openai/codex@${CODEX_CLI_VERSION}" \ + && CODEX_NATIVE="$(find /opt/codex -type f -path '*/vendor/*/bin/codex' -print -quit)" \ + && test -n "$CODEX_NATIVE" \ + && cp "$CODEX_NATIVE" /opt/codex-native \ + && chmod +x /opt/codex-native \ + && /opt/codex-native --version + # === Stage 2: Python 运行时 === FROM python:3.11-slim AS runtime ARG USE_CN_MIRROR=1 @@ -56,6 +71,7 @@ ARG INCLUDE_STOCKSDK=0 WORKDIR /app # Node.js 运行时: 仅在启用 stock-sdk 插件时安装(供 node bridge.mjs 使用)。 +# Codex CLI 从官方 npm 包提取原生二进制,不依赖运行时 Node.js。 # bookworm 自带 nodejs 18.19, 满足插件 engines>=18; --no-install-recommends 精简, # 自带 libnode/libc-ares 等全部动态依赖, 无需手动补库。 # 国内构建走 apt mirror 已在 debian 镜像sources.list 配好, 无需额外换源。 @@ -110,6 +126,10 @@ ENV STATIC_DIR=/app/static \ # Frontend 静态产物 COPY --from=frontend-builder /build/dist ./static +# Codex CLI 使用官方 npm 包携带的当前平台原生二进制,无需运行时 Node.js。 +COPY --from=codex-builder /opt/codex-native /usr/local/bin/codex +RUN codex --version + ENV PYTHONPATH=/app # 兜底时区: 交易时段判断已在代码里显式用北京时间 (app/market_time.py), # 此处让日志时间戳等其余 naive 时间也对齐北京时间。 diff --git a/docker-compose.yml b/docker-compose.yml index c1fb2cc..aa3d9eb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,6 +7,7 @@ services: dockerfile: Dockerfile args: BACKEND_EXTRAS: ${BACKEND_EXTRAS:-} + CODEX_CLI_VERSION: ${CODEX_CLI_VERSION:-0.144.3} container_name: TickFlow_Stock_Panel ports: - "${PORT:-3018}:3018" @@ -21,4 +22,6 @@ services: volumes: - ./data:/app/data - ./tiers.yaml:/app/tiers.yaml:ro + # 复用主机 Codex 登录态;后端只读后复制到单次请求的临时 CODEX_HOME。 + - ${HOME}/.codex:/root/.codex:ro restart: unless-stopped From 452816aee3e491f68759bcb3a6ded4e06d7f80d1 Mon Sep 17 00:00:00 2001 From: "zhang,zhao" <905781330@qq.com> Date: Tue, 14 Jul 2026 11:25:39 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=E6=96=87=E6=A1=A3=EF=BC=9A=E8=A1=A5?= =?UTF-8?q?=E5=85=85=20Docker=20Codex=20CLI=20=E4=BD=BF=E7=94=A8=E8=AF=B4?= =?UTF-8?q?=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 8 +++ .../plans/2026-07-14-docker-codex-cli.md | 66 +++++++++++++++++++ .../2026-07-14-docker-codex-cli-design.md | 4 +- 3 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 docs/superpowers/plans/2026-07-14-docker-codex-cli.md diff --git a/README.md b/README.md index f930347..ae1c990 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,14 @@ docker compose up --build # 打开 http://localhost:3018 ``` +Docker 镜像内置固定版本的 **Codex CLI**,Compose 会将主机 `${HOME}/.codex` 只读挂载到容器,因此主机需先完成 Codex 登录。需要覆盖镜像内版本时可设置构建参数: + +```bash +CODEX_CLI_VERSION=0.144.3 docker compose up --build +``` + +> Codex CLI 模式允许 TickFlow 容器读取本机 Codex 登录凭据,仅应在受信任的本机环境启用。凭据目录以只读方式挂载,不会写入镜像。 + 镜像已内置 **stock-sdk** 数据源插件(Node 运行时 + 依赖),开箱即用。 > 📖 Docker 进阶、GitHub Actions 自构建、老 CPU 兼容、访问密码设置等见 [docs/deployment.md](./docs/deployment.md)。 diff --git a/docs/superpowers/plans/2026-07-14-docker-codex-cli.md b/docs/superpowers/plans/2026-07-14-docker-codex-cli.md new file mode 100644 index 0000000..544d12f --- /dev/null +++ b/docs/superpowers/plans/2026-07-14-docker-codex-cli.md @@ -0,0 +1,66 @@ +# Docker Codex CLI Support Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make Docker deployments detect and execute the host-authenticated Codex CLI without putting credentials into the image. + +**Architecture:** Install a pinned `@openai/codex` package in a dedicated Node builder stage, extract its platform-native Linux binary, and copy only that binary into the runtime image. Mount `${HOME}/.codex` read-only so the existing backend can copy authentication into its per-request temporary Codex home. + +**Tech Stack:** Docker multi-stage builds, Docker Compose, `@openai/codex`, FastAPI, pytest + +--- + +### Task 1: Establish baseline and impact + +**Files:** No changes. + +- [x] Confirm host `codex --version` reports `0.144.3`. +- [x] Confirm the old container cannot resolve `codex`. +- [x] Run GitNexus impact on `_resolve_command`; avoid changing the HIGH-risk backend symbol chain. +- [x] Run `uv run pytest tests/test_ai_provider.py -q`; expect 15 passing tests. + +### Task 2: Package Codex in Docker + +**Files:** +- Modify: `Dockerfile` + +- [x] Add `CODEX_CLI_VERSION=0.144.3` as a reproducible build argument. +- [x] Install `@openai/codex` in `codex-builder` and locate `*/vendor/*/bin/codex`. +- [x] Copy the native binary to `/opt/codex-native` and verify its version in the builder. +- [x] Copy only `/opt/codex-native` to runtime `/usr/local/bin/codex` and verify it there. +- [x] Keep runtime Node.js conditional on stock-sdk because the extracted Codex binary is self-contained. + +### Task 3: Reuse host authentication safely + +**Files:** +- Modify: `docker-compose.yml` + +- [x] Pass `CODEX_CLI_VERSION` through Compose. +- [x] Mount `${HOME}/.codex:/root/.codex:ro`. +- [x] Run `docker compose config` and confirm the version and read-only mount. + +### Task 4: Document behavior + +**Files:** +- Modify: `README.md` +- Create: `docs/superpowers/plans/2026-07-14-docker-codex-cli.md` + +- [x] Document the pinned version, version override, host login requirement, and credential security boundary. +- [ ] Run `git diff --check`. +- [ ] Run `gitnexus detect-changes` before the documentation commit. + +### Task 5: End-to-end verification and PR + +**Files:** No changes. + +- [x] Build the Codex builder stage and verify `codex-cli 0.144.3`. +- [x] Copy the extracted binary into the current TickFlow runtime image and verify it executes without Node.js. +- [ ] Recreate the app with the Codex-enabled image and existing data. +- [ ] Verify `/api/settings` reports Codex configured. +- [ ] POST `/api/strategies/ai/test` and expect `{"ok":true}` with `OK`. +- [ ] Re-run provider tests and inspect final Git/GitNexus scope. +- [ ] Push `codex/docker-codex-cli` and open a Draft PR against `main`. + +### Known external build issue + +A cold full-image build currently reaches the Codex stages successfully, then fails in the pre-existing backend dependency layer because `backend/uv.lock` contains direct Tsinghua mirror wheel URLs returning HTTP 403. This PR does not rewrite the lockfile or mix that unrelated dependency-source problem into the Codex fix; runtime compatibility is verified separately against the existing TickFlow image. diff --git a/docs/superpowers/specs/2026-07-14-docker-codex-cli-design.md b/docs/superpowers/specs/2026-07-14-docker-codex-cli-design.md index 2a57121..c7e8099 100644 --- a/docs/superpowers/specs/2026-07-14-docker-codex-cli-design.md +++ b/docs/superpowers/specs/2026-07-14-docker-codex-cli-design.md @@ -22,9 +22,9 @@ TickFlow 的 Docker 服务运行在独立容器中。即使 macOS 主机已经 ### 镜像构建 -Dockerfile 新增独立的 `codex-builder` 阶段,使用 Node bookworm 镜像安装固定版本的 `@openai/codex`。版本由 `CODEX_CLI_VERSION` 构建参数控制,并提供项目验证过的默认值。 +Dockerfile 新增独立的 `codex-builder` 阶段,使用 Node bookworm 镜像安装固定版本的 `@openai/codex`,再从官方包中提取当前构建平台的 Linux 原生二进制。版本由 `CODEX_CLI_VERSION` 构建参数控制,并提供项目验证过的默认值。 -运行阶段从构建阶段复制 Codex 包,并在 `/usr/local/bin/codex` 提供入口。运行镜像继续使用已有的 Debian Node.js 运行时,不额外保留 npm,从而减少运行层体积和可变依赖。 +运行阶段只把提取后的原生二进制复制到 `/usr/local/bin/codex`,不复制 npm 包,也不要求为了 Codex 常驻安装 Node.js。这样保留多架构构建能力,同时减少运行层体积和可变依赖。 ### 凭据挂载 From 46338f4435619003bbb20403de9af5914ff1d968 Mon Sep 17 00:00:00 2001 From: "zhang,zhao" <905781330@qq.com> Date: Tue, 14 Jul 2026 11:50:04 +0800 Subject: [PATCH 4/5] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E9=80=82?= =?UTF-8?q?=E9=85=8D=20Docker=20Codex=20=E6=9C=AC=E5=9C=B0=E8=AE=BF?= =?UTF-8?q?=E9=97=AE=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- backend/app/services/ai_provider.py | 39 +++++++++++ backend/tests/test_ai_provider.py | 65 +++++++++++++++++++ docker-compose.yml | 2 + .../plans/2026-07-14-docker-codex-cli.md | 8 ++- .../2026-07-14-docker-codex-cli-design.md | 3 + 6 files changed, 115 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ae1c990..2298d88 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,7 @@ docker compose up --build # 打开 http://localhost:3018 ``` -Docker 镜像内置固定版本的 **Codex CLI**,Compose 会将主机 `${HOME}/.codex` 只读挂载到容器,因此主机需先完成 Codex 登录。需要覆盖镜像内版本时可设置构建参数: +Docker 镜像内置固定版本的 **Codex CLI**,Compose 会将主机 `${HOME}/.codex` 只读挂载到容器,因此主机需先完成 Codex 登录。若主机 Codex 使用 loopback local-access provider,容器会保留实际端口并自动将主机名映射为 `host.docker.internal`。需要覆盖镜像内版本时可设置构建参数: ```bash CODEX_CLI_VERSION=0.144.3 docker compose up --build diff --git a/backend/app/services/ai_provider.py b/backend/app/services/ai_provider.py index 8df8f0c..2886520 100644 --- a/backend/app/services/ai_provider.py +++ b/backend/app/services/ai_provider.py @@ -14,6 +14,7 @@ import tomllib from collections.abc import AsyncIterator, Callable, Sequence from pathlib import Path from types import TracebackType +from urllib.parse import urlsplit, urlunsplit from app import secrets_store from app.config import settings @@ -699,6 +700,10 @@ def _codex_home() -> Path: def _write_compatible_codex_config(path: Path) -> None: config = _read_codex_config() lines: list[str] = [] + local_provider = _docker_codex_local_provider(config) + + if local_provider: + lines.append(_toml_string("model_provider", "codex_local_access")) model = current_ai_model() or normalize_codex_model(str(config.get("model") or "")) if model: @@ -713,9 +718,43 @@ def _write_compatible_codex_config(path: Path) -> None: lines.append(_toml_string("approval_policy", "never")) lines.append(_toml_string("sandbox_mode", "read-only")) + if local_provider: + lines.append("") + lines.append("[model_providers.codex_local_access]") + for key in ("name", "base_url", "wire_api", "experimental_bearer_token"): + value = local_provider.get(key) + if isinstance(value, str) and value: + lines.append(_toml_string(key, value)) + for key in ("requires_openai_auth", "supports_websockets"): + value = local_provider.get(key) + if isinstance(value, bool): + lines.append(f"{key} = {'true' if value else 'false'}") + path.write_text("\n".join(lines) + "\n", encoding="utf-8") +def _docker_codex_local_provider(config: dict) -> dict | None: + """Return the local-access provider adapted to Docker's host gateway.""" + docker_host = os.environ.get("CODEX_DOCKER_HOST", "").strip() + if not docker_host or config.get("model_provider") != "codex_local_access": + return None + + providers = config.get("model_providers") + if not isinstance(providers, dict): + return None + source = providers.get("codex_local_access") + if not isinstance(source, dict): + return None + + provider = dict(source) + base_url = str(provider.get("base_url") or "").strip() + parsed = urlsplit(base_url) + if parsed.hostname in {"localhost", "127.0.0.1", "::1"}: + port = f":{parsed.port}" if parsed.port else "" + provider["base_url"] = urlunsplit(parsed._replace(netloc=f"{docker_host}{port}")) + return provider + + def _read_codex_config() -> dict: path = _codex_home() / "config.toml" if not path.exists(): diff --git a/backend/tests/test_ai_provider.py b/backend/tests/test_ai_provider.py index 5ff5c05..1b5a89c 100644 --- a/backend/tests/test_ai_provider.py +++ b/backend/tests/test_ai_provider.py @@ -1,5 +1,7 @@ from __future__ import annotations +import tomllib + import httpx import openai @@ -161,3 +163,66 @@ def test_codex_process_env_excludes_application_secrets(monkeypatch, tmp_path): assert "AI_API_KEY" not in env assert "OPENAI_API_KEY" not in env assert "AUTH_PASSWORD" not in env + + +def test_codex_config_adapts_local_access_provider_for_docker(monkeypatch, tmp_path): + monkeypatch.setenv("CODEX_DOCKER_HOST", "host.docker.internal") + monkeypatch.setattr(ai_provider, "current_ai_model", lambda: "") + monkeypatch.setattr(ai_provider, "current_codex_reasoning_effort", lambda: "") + monkeypatch.setattr( + ai_provider, + "_read_codex_config", + lambda: { + "model_provider": "codex_local_access", + "model": "gpt-5.6-sol", + "model_providers": { + "codex_local_access": { + "name": "Codex API Service", + "base_url": "http://localhost:62678/v1", + "wire_api": "responses", + "requires_openai_auth": True, + "supports_websockets": False, + "experimental_bearer_token": "local-secret", + } + }, + }, + ) + path = tmp_path / "config.toml" + + ai_provider._write_compatible_codex_config(path) + + with path.open("rb") as f: + config = tomllib.load(f) + assert config["model_provider"] == "codex_local_access" + provider = config["model_providers"]["codex_local_access"] + assert provider["base_url"] == "http://host.docker.internal:62678/v1" + assert provider["experimental_bearer_token"] == "local-secret" + assert provider["requires_openai_auth"] is True + assert provider["supports_websockets"] is False + + +def test_codex_config_does_not_copy_provider_without_docker_opt_in(monkeypatch, tmp_path): + monkeypatch.delenv("CODEX_DOCKER_HOST", raising=False) + monkeypatch.setattr(ai_provider, "current_ai_model", lambda: "") + monkeypatch.setattr(ai_provider, "current_codex_reasoning_effort", lambda: "") + monkeypatch.setattr( + ai_provider, + "_read_codex_config", + lambda: { + "model_provider": "codex_local_access", + "model_providers": { + "codex_local_access": { + "base_url": "http://localhost:62678/v1", + "experimental_bearer_token": "must-not-leak", + } + }, + }, + ) + path = tmp_path / "config.toml" + + ai_provider._write_compatible_codex_config(path) + + text = path.read_text(encoding="utf-8") + assert "model_provider" not in text + assert "model_providers" not in text + assert "must-not-leak" not in text diff --git a/docker-compose.yml b/docker-compose.yml index aa3d9eb..d32c8c7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,6 +19,8 @@ services: # 导致每次 up --build 重建容器都丢数据。environment 优先级高于 env_file, # 无论 .env 怎么写这里都以容器路径为准。 - DATA_DIR=/app/data + # 将主机 Codex Desktop 的 loopback local-access 端点映射到 Docker host gateway。 + - CODEX_DOCKER_HOST=host.docker.internal volumes: - ./data:/app/data - ./tiers.yaml:/app/tiers.yaml:ro diff --git a/docs/superpowers/plans/2026-07-14-docker-codex-cli.md b/docs/superpowers/plans/2026-07-14-docker-codex-cli.md index 544d12f..2eb2267 100644 --- a/docs/superpowers/plans/2026-07-14-docker-codex-cli.md +++ b/docs/superpowers/plans/2026-07-14-docker-codex-cli.md @@ -37,6 +37,7 @@ - [x] Pass `CODEX_CLI_VERSION` through Compose. - [x] Mount `${HOME}/.codex:/root/.codex:ro`. +- [x] Set `CODEX_DOCKER_HOST=host.docker.internal` for loopback local-access providers. - [x] Run `docker compose config` and confirm the version and read-only mount. ### Task 4: Document behavior @@ -55,9 +56,10 @@ - [x] Build the Codex builder stage and verify `codex-cli 0.144.3`. - [x] Copy the extracted binary into the current TickFlow runtime image and verify it executes without Node.js. -- [ ] Recreate the app with the Codex-enabled image and existing data. -- [ ] Verify `/api/settings` reports Codex configured. -- [ ] POST `/api/strategies/ai/test` and expect `{"ok":true}` with `OK`. +- [x] Add red/green tests for opt-in local-access provider mapping and default token isolation. +- [x] Recreate the app with the Codex-enabled image and existing data. +- [x] Verify `/api/settings` reports Codex configured. +- [x] POST `/api/strategies/ai/test` and receive `{"ok":true}` with `OK`. - [ ] Re-run provider tests and inspect final Git/GitNexus scope. - [ ] Push `codex/docker-codex-cli` and open a Draft PR against `main`. diff --git a/docs/superpowers/specs/2026-07-14-docker-codex-cli-design.md b/docs/superpowers/specs/2026-07-14-docker-codex-cli-design.md index c7e8099..569d979 100644 --- a/docs/superpowers/specs/2026-07-14-docker-codex-cli-design.md +++ b/docs/superpowers/specs/2026-07-14-docker-codex-cli-design.md @@ -32,6 +32,8 @@ Dockerfile 新增独立的 `codex-builder` 阶段,使用 Node bookworm 镜像 后端现有 `_prepare_codex_home` 会从只读挂载中读取 `auth.json` 和兼容配置,再复制或生成到单次请求的临时 `CODEX_HOME`。Codex 子进程只使用临时目录,因此不会修改主机凭据目录。 +当主机配置明确选择 `codex_local_access` 时,Compose 通过 `CODEX_DOCKER_HOST=host.docker.internal` 启用受控适配:临时配置只复制该 provider 的必要白名单字段,将 `localhost`、`127.0.0.1` 或 `::1` 改为 Docker host gateway,并保留原端口。未设置该环境变量时继续使用原有隔离配置,不复制 provider 或 bearer token。 + ### 数据流 1. Compose 启动容器并只读挂载主机 Codex home。 @@ -52,6 +54,7 @@ Dockerfile 新增独立的 `codex-builder` 阶段,使用 Node bookworm 镜像 - Codex home 仅以只读方式挂载。 - 凭据不进入 Docker build context 的产物层。 +- local-access bearer token 仅从只读主机配置复制到单次请求的临时配置,且只在 Docker 显式启用适配时发生。 - 现有 `--ephemeral`、`--sandbox read-only`、`approval_policy = "never"` 和空白临时工作区保持不变。 - 文档明确说明:启用 Docker Codex 模式意味着 TickFlow 容器能够读取 Codex 登录凭据,应仅在受信任的本机环境使用。 From 8a6c85bfc8522ef652c4138f856e5c50fbd4d71b Mon Sep 17 00:00:00 2001 From: "zhang,zhao" <905781330@qq.com> Date: Tue, 14 Jul 2026 17:33:55 +0800 Subject: [PATCH 5/5] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E8=A1=A5?= =?UTF-8?q?=E5=85=85=20Linux=20Docker=20=E4=B8=BB=E6=9C=BA=E6=98=A0?= =?UTF-8?q?=E5=B0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 2 + .../plans/2026-07-14-docker-codex-cli.md | 68 ------------------ .../2026-07-14-docker-codex-cli-design.md | 72 ------------------- 3 files changed, 2 insertions(+), 140 deletions(-) delete mode 100644 docs/superpowers/plans/2026-07-14-docker-codex-cli.md delete mode 100644 docs/superpowers/specs/2026-07-14-docker-codex-cli-design.md diff --git a/docker-compose.yml b/docker-compose.yml index d32c8c7..5234533 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,8 @@ services: container_name: TickFlow_Stock_Panel ports: - "${PORT:-3018}:3018" + extra_hosts: + - "host.docker.internal:host-gateway" env_file: - .env environment: diff --git a/docs/superpowers/plans/2026-07-14-docker-codex-cli.md b/docs/superpowers/plans/2026-07-14-docker-codex-cli.md deleted file mode 100644 index 2eb2267..0000000 --- a/docs/superpowers/plans/2026-07-14-docker-codex-cli.md +++ /dev/null @@ -1,68 +0,0 @@ -# Docker Codex CLI Support Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Make Docker deployments detect and execute the host-authenticated Codex CLI without putting credentials into the image. - -**Architecture:** Install a pinned `@openai/codex` package in a dedicated Node builder stage, extract its platform-native Linux binary, and copy only that binary into the runtime image. Mount `${HOME}/.codex` read-only so the existing backend can copy authentication into its per-request temporary Codex home. - -**Tech Stack:** Docker multi-stage builds, Docker Compose, `@openai/codex`, FastAPI, pytest - ---- - -### Task 1: Establish baseline and impact - -**Files:** No changes. - -- [x] Confirm host `codex --version` reports `0.144.3`. -- [x] Confirm the old container cannot resolve `codex`. -- [x] Run GitNexus impact on `_resolve_command`; avoid changing the HIGH-risk backend symbol chain. -- [x] Run `uv run pytest tests/test_ai_provider.py -q`; expect 15 passing tests. - -### Task 2: Package Codex in Docker - -**Files:** -- Modify: `Dockerfile` - -- [x] Add `CODEX_CLI_VERSION=0.144.3` as a reproducible build argument. -- [x] Install `@openai/codex` in `codex-builder` and locate `*/vendor/*/bin/codex`. -- [x] Copy the native binary to `/opt/codex-native` and verify its version in the builder. -- [x] Copy only `/opt/codex-native` to runtime `/usr/local/bin/codex` and verify it there. -- [x] Keep runtime Node.js conditional on stock-sdk because the extracted Codex binary is self-contained. - -### Task 3: Reuse host authentication safely - -**Files:** -- Modify: `docker-compose.yml` - -- [x] Pass `CODEX_CLI_VERSION` through Compose. -- [x] Mount `${HOME}/.codex:/root/.codex:ro`. -- [x] Set `CODEX_DOCKER_HOST=host.docker.internal` for loopback local-access providers. -- [x] Run `docker compose config` and confirm the version and read-only mount. - -### Task 4: Document behavior - -**Files:** -- Modify: `README.md` -- Create: `docs/superpowers/plans/2026-07-14-docker-codex-cli.md` - -- [x] Document the pinned version, version override, host login requirement, and credential security boundary. -- [ ] Run `git diff --check`. -- [ ] Run `gitnexus detect-changes` before the documentation commit. - -### Task 5: End-to-end verification and PR - -**Files:** No changes. - -- [x] Build the Codex builder stage and verify `codex-cli 0.144.3`. -- [x] Copy the extracted binary into the current TickFlow runtime image and verify it executes without Node.js. -- [x] Add red/green tests for opt-in local-access provider mapping and default token isolation. -- [x] Recreate the app with the Codex-enabled image and existing data. -- [x] Verify `/api/settings` reports Codex configured. -- [x] POST `/api/strategies/ai/test` and receive `{"ok":true}` with `OK`. -- [ ] Re-run provider tests and inspect final Git/GitNexus scope. -- [ ] Push `codex/docker-codex-cli` and open a Draft PR against `main`. - -### Known external build issue - -A cold full-image build currently reaches the Codex stages successfully, then fails in the pre-existing backend dependency layer because `backend/uv.lock` contains direct Tsinghua mirror wheel URLs returning HTTP 403. This PR does not rewrite the lockfile or mix that unrelated dependency-source problem into the Codex fix; runtime compatibility is verified separately against the existing TickFlow image. diff --git a/docs/superpowers/specs/2026-07-14-docker-codex-cli-design.md b/docs/superpowers/specs/2026-07-14-docker-codex-cli-design.md deleted file mode 100644 index 569d979..0000000 --- a/docs/superpowers/specs/2026-07-14-docker-codex-cli-design.md +++ /dev/null @@ -1,72 +0,0 @@ -# Docker 模式支持 Codex CLI 设计 - -## 背景 - -TickFlow 的 Docker 服务运行在独立容器中。即使 macOS 主机已经安装并登录 Codex CLI,容器内的 `shutil.which("codex")` 仍无法找到主机命令,因此设置页显示“未找到 Codex CLI 命令: codex”。 - -## 目标 - -- Docker 镜像内提供可直接执行的 `codex` 命令。 -- 复用主机已有的 Codex 登录凭据,无需在容器内重复交互式登录。 -- 保持现有 `codex exec`、临时工作目录、只读沙箱和隔离 `CODEX_HOME` 逻辑不变。 -- 保持镜像构建可复现,并允许维护者显式升级 Codex CLI 版本。 - -## 非目标 - -- 不允许用户配置任意可执行文件路径。 -- 不改变本机开发模式和桌面客户端的 Codex 命令解析逻辑。 -- 不把 Codex 凭据复制进镜像或提交到仓库。 -- 不为 Dockerfile 文本结构增加脆弱的单元测试。 - -## 方案 - -### 镜像构建 - -Dockerfile 新增独立的 `codex-builder` 阶段,使用 Node bookworm 镜像安装固定版本的 `@openai/codex`,再从官方包中提取当前构建平台的 Linux 原生二进制。版本由 `CODEX_CLI_VERSION` 构建参数控制,并提供项目验证过的默认值。 - -运行阶段只把提取后的原生二进制复制到 `/usr/local/bin/codex`,不复制 npm 包,也不要求为了 Codex 常驻安装 Node.js。这样保留多架构构建能力,同时减少运行层体积和可变依赖。 - -### 凭据挂载 - -`docker-compose.yml` 将主机 `${HOME}/.codex` 挂载到容器 `/root/.codex`,模式为只读。挂载目录不会写入镜像或仓库。 - -后端现有 `_prepare_codex_home` 会从只读挂载中读取 `auth.json` 和兼容配置,再复制或生成到单次请求的临时 `CODEX_HOME`。Codex 子进程只使用临时目录,因此不会修改主机凭据目录。 - -当主机配置明确选择 `codex_local_access` 时,Compose 通过 `CODEX_DOCKER_HOST=host.docker.internal` 启用受控适配:临时配置只复制该 provider 的必要白名单字段,将 `localhost`、`127.0.0.1` 或 `::1` 改为 Docker host gateway,并保留原端口。未设置该环境变量时继续使用原有隔离配置,不复制 provider 或 bearer token。 - -### 数据流 - -1. Compose 启动容器并只读挂载主机 Codex home。 -2. 设置接口调用 `codex_cli_available()`。 -3. `_resolve_command("codex")` 在容器 PATH 中找到镜像内入口。 -4. 发起 AI 请求时,后端建立临时工作区和临时 `CODEX_HOME`。 -5. 后端复用挂载目录中的认证信息并执行 `codex exec`。 -6. 请求结束后临时目录自动删除。 - -## 错误处理 - -- 主机未安装 Codex 不影响容器命令,因为 CLI 已包含在镜像中。 -- 主机未登录或 `${HOME}/.codex/auth.json` 不存在时,设置页仍可识别 CLI;实际调用会返回现有的 Codex 登录错误。 -- 构建时无法下载指定 npm 包时,镜像构建应失败,不静默切换到不固定版本。 -- 挂载目录保持只读;任何意外写入都会由容器文件系统拒绝。 - -## 安全性 - -- Codex home 仅以只读方式挂载。 -- 凭据不进入 Docker build context 的产物层。 -- local-access bearer token 仅从只读主机配置复制到单次请求的临时配置,且只在 Docker 显式启用适配时发生。 -- 现有 `--ephemeral`、`--sandbox read-only`、`approval_policy = "never"` 和空白临时工作区保持不变。 -- 文档明确说明:启用 Docker Codex 模式意味着 TickFlow 容器能够读取 Codex 登录凭据,应仅在受信任的本机环境使用。 - -## 验证 - -1. 构建镜像成功。 -2. 容器内 `command -v codex` 返回 `/usr/local/bin/codex`。 -3. 容器内 `codex --version` 返回设计中固定的版本。 -4. 设置接口返回 `ai_configured: true`,页面不再显示“未找到 Codex CLI 命令”。 -5. 执行一次真实连接测试,确认 `codex exec` 能读取认证并返回内容。 -6. 运行现有后端 AI provider 测试,确认非 Docker 路径无回归。 - -## 文档更新 - -README 的 Docker 启动说明增加 Codex CLI 凭据挂载、安全边界和版本覆盖方式,避免用户把“主机已安装”误认为“容器自动可见”。