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
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,15 @@ COMPOSE_PROJECT_NAME=codegen_orchestrator
# Use an ISOLATED COPY of ~/.claude, not your live session — workers get rw access.
# mkdir -p ~/.claude-worker && cp ~/.claude/.credentials.json ~/.claude-worker/
# HOST_CLAUDE_DIR=/home/youruser/.claude-worker
# Host path to a dedicated Codex profile. Do not use ~/.codex: Codex workers
# need read-write access so the CLI can refresh ChatGPT subscription tokens.
# One-time setup (run on the Docker host):
# install -d -m 0700 ~/.codex-worker
# printf 'cli_auth_credentials_store = "file"\n' > ~/.codex-worker/config.toml
# chmod 0600 ~/.codex-worker/config.toml
# CODEX_HOME=~/.codex-worker codex login --device-auth
# chmod 0600 ~/.codex-worker/auth.json
# HOST_CODEX_HOME=/home/youruser/.codex-worker
# Per-agent-process timeout inside the worker (seconds). Code default is 900,
# which fits live LLM engineering; override here only to tune.
# WORKER_SUBPROCESS_TIMEOUT_SECONDS=900
1 change: 1 addition & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ jobs:
# Worker agents
FACTORY_API_KEY=${{ secrets.FACTORY_API_KEY }}
HOST_CLAUDE_DIR=${{ secrets.HOST_CLAUDE_DIR }}
HOST_CODEX_HOME=${{ secrets.HOST_CODEX_HOME }}

# Observability
LANGCHAIN_API_KEY=${{ secrets.LANGCHAIN_API_KEY }}
Expand Down
30 changes: 23 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ help:
@echo " make rebuild - Rebuild everything (services + worker images), restart stack"
@echo ""
@echo "Worker Images:"
@echo " make rebuild-worker-images - Rebuild worker base images (common → claude)"
@echo " make rebuild-worker-images - Rebuild worker base images (common → claude/factory/codex)"
@echo " make rebuild-worker-images-hard - Rebuild with --no-cache (when cache is stale)"

# === Dependency Lock Files ===
Expand Down Expand Up @@ -122,7 +122,7 @@ cleanup-agents:
@echo "✅ Agent containers cleaned up"

# === Worker Base Images ===
# Build the worker image chain: common -> claude/factory
# Build the worker image chain: common -> claude/factory/codex
# Use rebuild-worker-images after changing worker-wrapper or worker-base Dockerfiles

rebuild-worker-images:
Expand All @@ -133,6 +133,12 @@ rebuild-worker-images:
@echo "🔨 Building worker-base-claude..."
docker build -t worker-base-claude:latest \
-f services/worker-manager/images/worker-base-claude/Dockerfile .
@echo "🔨 Building worker-base-factory..."
docker build -t worker-base-factory:latest \
-f services/worker-manager/images/worker-base-factory/Dockerfile .
@echo "🔨 Building worker-base-codex..."
docker build -t worker-base-codex:latest \
-f services/worker-manager/images/worker-base-codex/Dockerfile .
@echo "🧹 Cleaning cached worker:* images..."
@docker images -q 'worker:*' | xargs -r docker rmi 2>/dev/null || true
@echo "✅ Worker images rebuilt!"
Expand All @@ -146,20 +152,30 @@ rebuild-worker-images-hard:
@echo "🔨 Building worker-base-claude (no-cache)..."
docker build --no-cache -t worker-base-claude:latest \
-f services/worker-manager/images/worker-base-claude/Dockerfile .
@echo "🔨 Building worker-base-factory (no-cache)..."
docker build --no-cache -t worker-base-factory:latest \
-f services/worker-manager/images/worker-base-factory/Dockerfile .
@echo "🔨 Building worker-base-codex (no-cache)..."
docker build --no-cache -t worker-base-codex:latest \
-f services/worker-manager/images/worker-base-codex/Dockerfile .
@echo "🧹 Cleaning cached worker:* images..."
@docker images -q 'worker:*' | xargs -r docker rmi 2>/dev/null || true
@echo "✅ Worker images rebuilt (no-cache)!"

# Check if worker images are stale and rebuild if needed
check-worker-images:
@CURRENT=$(WORKER_SOURCE_HASH); \
STORED=$$(docker inspect worker-base-common:latest \
--format '{{index .Config.Labels "org.codegen.worker_source_hash"}}' 2>/dev/null || echo "none"); \
if [ "$$CURRENT" != "$$STORED" ]; then \
echo "⚠️ Worker source files changed ($$STORED → $$CURRENT) — rebuilding images..."; \
STALE=""; \
for IMAGE in worker-base-common worker-base-claude worker-base-factory worker-base-codex; do \
STORED=$$(docker inspect "$$IMAGE:latest" \
--format '{{index .Config.Labels "org.codegen.worker_source_hash"}}' 2>/dev/null || echo "missing"); \
if [ "$$CURRENT" != "$$STORED" ]; then STALE="$$STALE $$IMAGE($$STORED)"; fi; \
done; \
if [ -n "$$STALE" ]; then \
echo "⚠️ Worker base images stale:$$STALE; rebuilding for hash $$CURRENT..."; \
$(MAKE) rebuild-worker-images; \
else \
echo "✅ Worker images up to date (hash: $$CURRENT)"; \
echo "✅ Worker base images up to date (hash: $$CURRENT)"; \
fi

# === Quality ===
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

- **Автономность**: Человек заходит раз в несколько дней, смотрит отчёты, докидывает деньги
- **Агенты как узлы графа**: Product Owner — это агент LangGraph, управляющий процессом.
- **Worker Manager**: Запускает изолированные контейнеры для Engineering/DevOps задач (Claude Code, Factory.ai).
- **Worker Manager**: Запускает изолированные контейнеры для Engineering/DevOps задач (Claude Code, Factory.ai, OpenAI Codex).
- **Нелинейность**: Агенты могут вызывать друг друга в любом порядке
- **Spec-first**: Используем [service-template](https://github.com/vladmesh/service-template) для генерации кода

Expand Down
3 changes: 2 additions & 1 deletion docker-compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
# - DB credentials have no defaults (fail-fast if .env is misconfigured)
# - Redis AOF persistence enabled
#
# Secret paths (SSH key, GitHub App PEM, Telethon session, HOST_CLAUDE_DIR)
# Secret paths (SSH key, GitHub App PEM, Telethon session, HOST_CLAUDE_DIR,
# HOST_CODEX_HOME)
# are controlled via env vars in .env — see docs/DEPLOY.md for required values.

services:
Expand Down
4 changes: 4 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,9 @@ services:
SERVICE_NAME: worker-manager
# Host path for mounting .claude dir into workers
HOST_CLAUDE_DIR: ${HOST_CLAUDE_DIR:-/home/user/.claude}
# Dedicated file-backed Codex profile. Never point this at the live ~/.codex.
HOST_CODEX_HOME: ${HOST_CODEX_HOME:-}
HOST_CODEX_VALIDATION_PATH: /host-codex
SCAFFOLDED_WORKSPACE_PATH: /data/workspaces
INTERNAL_NETWORK: codegen_internal
WORKER_NETWORK: codegen_worker
Expand All @@ -272,6 +275,7 @@ services:
- /var/run/docker.sock:/var/run/docker.sock
- ${WORKSPACE_HOST_PATH:-/data/workspaces}:/data/workspaces
- ${HOST_CLAUDE_DIR:-/home/user/.claude}:/host-claude:ro
- ${HOST_CODEX_HOME:-./secrets/codex-unconfigured}:/host-codex:ro
depends_on:
redis:
condition: service_healthy
Expand Down
15 changes: 14 additions & 1 deletion docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@

## 2026-07-18

- Add OpenAI Codex CLI as a developer-worker type alongside Claude Code and
Factory Droid. Project config now routes `agent_type=codex` without changing
the worker queue envelope, and unknown values fail explicitly. The dedicated
`worker-base-codex` image pins Codex CLI 0.144.6 and runs as the existing
non-root worker with the same workspace and resource limits. Host-session
mode validates and mounts an isolated file-backed `HOST_CODEX_HOME` read-write
for token refresh, without requiring an API key or defaulting to `~/.codex`.
Production deployment persists that host profile path and pulls the Codex
base image alongside the common, Claude, and Factory images.
Codex runs non-interactively with workspace-only writes and reports through
the existing HTTP bridge; sandboxed network commands are enabled for the
bridge, dependency access, and Git push. Its stdout and stderr are not
persisted as results. Optional API-key mode uses `CODEX_API_KEY`.
- Make live harness API reads fail loud before body parsing. Parsed `tests/live` API responses now
call `raise_for_status()` first, including scaffold/project polling and auth-gated server,
ssh-key and port-allocation checks, with a mock-transport regression for a rejected polling call.
Expand Down Expand Up @@ -88,7 +101,7 @@

### Changed
- **Typed `Run.result` (Sprint 002 Phase 2 keystone, `codegen_orchestrator-440`)**: `RunDTO.result` is no longer `dict | None`. Added `shared/contracts/dto/run_result.py` with one `extra="forbid"` model per `RunType` — `EngineeringRunResult` (`engineering_status` + commit/modules/tests), `DeployRunResult` (`deploy_outcome` + deployed_url/application_id/bot_username/deploy_fix_attempt/error_details/action + opaque deployment/smoke blobs), `QARunResult` (`qa_outcome` + summary/`failed_checks: list[QAFailedCheck]`/report/qa_attempt/deployed_url/error). `RunDTO` binds the union to `type` (`_check_result_matches_type`): a payload of the wrong type, an unknown field, an unknown outcome, or a missing required field is rejected at the boundary; `result=None` stays valid for runs with no result yet. Producers (`deploy`, `deploy_result_handler`, `deploy_failure_handler`, `engineering_result_handler`, `qa`) now build the typed model and `model_dump(mode="json")` — one wire form, and the duplicated inline classification→outcome dict in `deploy_result_handler` is replaced by the shared `_classification_to_outcome`. The scheduler supervisor reads `run.result.deploy_outcome` / `.qa_outcome` / typed attributes instead of `run.result or {}` + `.get()` + `DeployOutcome(str)`. `result=None` is allowed only before the outcome exists (`QUEUED`/`RUNNING`/`CANCELLED`); a `COMPLETED`/`FAILED` run without a result is rejected, so a terminal run that lost its outcome surfaces loudly instead of wedging a story forever. All producer failure paths that reach a terminal status now write a typed result — deploy outcomes, `QAOutcome.ERROR` when QA can't resolve its server (previously the run stayed `QUEUED` and the story sat in `TESTING` forever), and `EngineeringStatus.FAILED`/`GAVE_UP` on engineering failure/give-up paths. Migration: `Run.result` stays a JSON column and the API's `RunRead` stays dict-typed (passthrough), so no DB migration and no break on historical reads; in-flight runs parse by construction. The scheduler validates only the latest run per story (`get_latest_run_by_story` parses `rows[0]` alone), so an older legacy/corrupt run can't fail a story whose current run is valid; a latest run that fails validation (wrong-type/corrupt, or terminal-without-result) is routed to a terminal, visible state (`supervisor._fail_story_on_invalid_result`: fail story once + notify admins) instead of poison-looping. `deployed_url`/`application_id` stay optional on `DeployRunResult` (a standalone deploy, or a success where the app record didn't resolve, legitimately lacks `application_id`), but the QA handoff needs both — so `_handle_deploy_success_story` validates them *before* transitioning the story or creating a QA run, routing a `SUCCESS` that can't reach QA to a visible failure instead of crashing the tick after partial state. Contract tests in `shared/tests/unit/test_run_result.py` (each type, `result=None` per non-terminal status, terminal-without-result rejection, cross-type/unknown-outcome/unknown-field/missing-field rejection, optional-field round-trip); scheduler routing/invalid-result/CANCELLED-skip/terminal-no-result/missing-handoff-fields tests split across `services/scheduler/tests/unit/test_supervisor.py` and `test_supervisor_run_routing.py` (shared factories in `_run_routing_factories.py`); latest-only validation tests in `test_api_client.py`; QA server-resolve terminal-result test in `test_qa_consumer.py`. Closes the last slice of Phase 2; next is Phase 3 typed Redis consume.
- **Unified contract vocabularies (Sprint 002 Phase 2, `codegen_orchestrator-436`)**: added `shared/contracts/vocab.py` with one canonical `StrEnum` per cross-service concept — `AgentType` (moved here, re-exported from `queues.worker`), `ActionType`, `ResultStatus`, `LifecycleEvent`. Replaced the competing inline `Literal[...]` sets: `BaseResult.status` (dropped the `error` failure synonym, now `success`/`failed`/`timeout`), `EngineeringMessage.action` (`ActionType`), and `AgentConfigDTO.type` (`AgentType`). `LifecycleEvent` is the canonical member set, but each wire keeps its own supported slice as an explicit `Literal` subset over the enum members — `TaskProgressKind` (`started`/`progress`/`completed`/`failed`) for `ProgressEvent.type` and `WorkerEvent.event_type`, `WorkerLifecycleKind` (`started`/`completed`/`failed`/`stopped`) for `WorkerLifecycleEvent.event` — so the historically different per-field vocabularies are preserved, not merged (a progress stream still rejects `stopped`, the worker-lifecycle stream still rejects `progress`). Worker-side comparisons now use the enum instead of raw `"claude"`/`"factory"` strings (`worker-wrapper` config + wrapper, `worker-manager` container_config/manager/`_get_agent`), and `worker-manager` consumer stops passing `agent_type.value`. The provisioner-result / infra-service producer/consumer and the telegram admin notifier use `ResultStatus`. Because `BaseResult.status` no longer accepts `error`, the scheduler `provisioner:results` consumer now treats a message that fails validation as terminal (`handle_provisioner_entry`: logs it and ACKs) instead of leaving it unacked to poison-loop the `claim_pending` reclaim; transient processing errors still stay unacked for retry. Two historically-mismatched vocabularies are kept distinct on purpose, not merged: `DeployAction` (adds `stop`/`undeploy`) and `TaskType` (adds `refactor`) stay separate from `ActionType`, and `WorkerEvent.worker_type` is now `WorkerCliKind` (`droid`/`claude_code`/`codex`), explicitly disjoint from `AgentType`. Contract tests in `shared/tests/unit/test_vocab.py` (accepted values + rejection of unknowns and of out-of-slice lifecycle values per field) and `services/scheduler/tests/unit/test_provisioner_entry.py` (poison message ACKed, valid processed+ACKed, transient error left unacked). Out of scope (still open in Phase 2/3): `Run.result` union, typed Redis consume / raw-dict consumers.
- **Unified contract vocabularies (Sprint 002 Phase 2, `codegen_orchestrator-436`)**: added `shared/contracts/vocab.py` with one canonical `StrEnum` per cross-service concept — `AgentType` (moved here, re-exported from `queues.worker`), `ActionType`, `ResultStatus`, `LifecycleEvent`. Replaced the competing inline `Literal[...]` sets: `BaseResult.status` (dropped the `error` failure synonym, now `success`/`failed`/`timeout`), `EngineeringMessage.action` (`ActionType`), and `AgentConfigDTO.type` (`AgentType`). `LifecycleEvent` is the canonical member set, but each wire keeps its own supported slice as an explicit `Literal` subset over the enum members — `TaskProgressKind` (`started`/`progress`/`completed`/`failed`) for `ProgressEvent.type` and `WorkerEvent.event_type`, `WorkerLifecycleKind` (`started`/`completed`/`failed`/`stopped`) for `WorkerLifecycleEvent.event` — so the historically different per-field vocabularies are preserved, not merged (a progress stream still rejects `stopped`, the worker-lifecycle stream still rejects `progress`). Worker-side comparisons now use the enum instead of raw `"claude"`/`"factory"` strings (`worker-wrapper` config + wrapper, `worker-manager` container_config/manager/`_get_agent`), and `worker-manager` consumer stops passing `agent_type.value`. The provisioner-result / infra-service producer/consumer and the telegram admin notifier use `ResultStatus`. Because `BaseResult.status` no longer accepts `error`, the scheduler `provisioner:results` consumer now treats a message that fails validation as terminal (`handle_provisioner_entry`: logs it and ACKs) instead of leaving it unacked to poison-loop the `claim_pending` reclaim; transient processing errors still stay unacked for retry. Two historically-mismatched vocabularies are kept distinct on purpose, not merged: `DeployAction` (adds `stop`/`undeploy`) and `TaskType` (adds `refactor`) stay separate from `ActionType`, while `WorkerEvent.worker_type` remains the separate `WorkerCliKind` vocabulary (`droid`/`claude_code`/`codex`). Contract tests in `shared/tests/unit/test_vocab.py` (accepted values + rejection of unknowns and of out-of-slice lifecycle values per field) and `services/scheduler/tests/unit/test_provisioner_entry.py` (poison message ACKed, valid processed+ACKed, transient error left unacked). Out of scope (still open in Phase 2/3): `Run.result` union, typed Redis consume / raw-dict consumers.
- **Typed response-DTO lifecycle fields (B7 slice of Sprint 002 Phase 2)**: lifecycle fields on the read-side DTOs now declare their existing `StrEnum` instead of bare `str`, so Pydantic rejects unknown values at the read boundary. `TaskDTO.status/type` and `TaskEventDTO.event_type/from_status/to_status`, `StoryDTO.status/type`, `ServerDTO.status` (+ `ServerCreate.status`), `ApplicationDTO/Create/Update.status`, `IncidentDTO.incident_type/status` (+ `IncidentCreate.incident_type`, `IncidentUpdate.status`), and `ServiceDeploymentDTO.status` (`DeploymentResult`). Dropped the "use str for flexibility" comments. Added accept-valid / reject-unknown unit tests per DTO. Only the B7 response-DTO slice; the duplicated vocabularies and `Run.result` typing from Phase 2 remain open.

### Fixed
Expand Down
18 changes: 14 additions & 4 deletions docs/CONTRACTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ instead of restating a `Literal[...]` or a local enum:

| Enum | Values | Used by |
|------|--------|---------|
| `AgentType` | `claude`, `factory`, `noop` | `WorkerConfig.agent_type`, `AgentConfigDTO.type`, worker-manager/worker-wrapper agent branching (re-exported from `queues.worker`) |
| `AgentType` | `claude`, `factory`, `codex`, `noop` | `WorkerConfig.agent_type`, `AgentConfigDTO.type`, worker-manager/worker-wrapper agent branching (re-exported from `queues.worker`) |
| `ActionType` | `create`, `feature`, `fix` | `EngineeringMessage.action` |
| `ResultStatus` | `success`, `failed`, `timeout` | `BaseResult.status` (and its subclasses) |
| `LifecycleEvent` | `started`, `progress`, `completed`, `failed`, `stopped` (canonical member set) | via the field-specific subsets below |
Expand All @@ -34,8 +34,18 @@ enums do not, so merging them would broaden a field past what the wire supports:
- `DeployAction` (`create`/`feature`/`fix` **plus** `stop`/`undeploy`) — deploy
operations, a superset of `ActionType`. `TaskType` (**plus** `refactor`) — the
planning-layer task kind.
- `WorkerCliKind` (`droid`/`claude_code`/`codex`) — the CLI's self-reported wire
identity on `worker:events`, which does **not** map onto `AgentType`.
- `WorkerCliKind` (`droid`/`claude_code`/`codex`) is the CLI's self-reported wire
identity on `worker:events`. It remains a separate concept from `AgentType`;
only the `codex` spelling currently overlaps.

`WorkerConfig.host_codex_home` adds no new queue shape beyond an optional field.
For `agent_type=codex` and `auth_mode=host_session`, worker-manager validates a
dedicated file-backed ChatGPT profile before image resolution. It then mounts
that host directory read-write at `/home/worker/.codex` and sets `CODEX_HOME`
to the same path. The profile must contain access and refresh tokens so the
non-interactive worker can refresh its session. Unknown agent values fail
Pydantic validation or explicit
LangGraph/image-routing checks; there is no fallback to Claude.

`ResultStatus` dropped the old `error` failure synonym: a failed result is
`failed`, never `error`. The `provisioner:results` consumer treats a message
Expand Down Expand Up @@ -1116,7 +1126,7 @@ The Orchestrator (LangGraph) listens to **one** stream for all worker results:
# shared/contracts/queues/worker.py

# AgentType is the canonical enum (shared/contracts/vocab.py), re-exported here.
from shared.contracts.vocab import AgentType # claude / factory / noop
from shared.contracts.vocab import AgentType # claude / factory / codex / noop


class WorkerCapability(StrEnum):
Expand Down
1 change: 1 addition & 0 deletions docs/DEPLOY.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ All secrets must be configured in the repository's **production** environment.
|--------|-------------|
| `FACTORY_API_KEY` | Factory.ai API key |
| `HOST_CLAUDE_DIR` | Path to `.claude` directory on prod server |
| `HOST_CODEX_HOME` | Path to the dedicated file-backed Codex profile described in `docs/coding-agents.md` |

### Admin UI

Expand Down
Loading
Loading