diff --git a/bwh_hive/bwh_hive/doctype/hive_project/hive_project.json b/bwh_hive/bwh_hive/doctype/hive_project/hive_project.json index a2194f0..de4d3ae 100644 --- a/bwh_hive/bwh_hive/doctype/hive_project/hive_project.json +++ b/bwh_hive/bwh_hive/doctype/hive_project/hive_project.json @@ -24,6 +24,7 @@ "github_repo", "agent_section", "agent_enabled", + "agent_engine", "github_pat", "agent_template_slug", "target_app_name", @@ -139,6 +140,15 @@ "fieldtype": "Check", "label": "Agent Enabled" }, + { + "default": "Claude Code", + "depends_on": "agent_enabled", + "description": "Coding agent that runs inside the box. Codex uses the OpenAI API key; Claude Code uses the Anthropic key or subscription token (both in Hive Settings).", + "fieldname": "agent_engine", + "fieldtype": "Select", + "label": "Agent Engine", + "options": "Claude Code\nCodex" + }, { "depends_on": "agent_enabled", "description": "Project PAT used inside the box for gh / git push (GIT_PAT).", diff --git a/bwh_hive/bwh_hive/doctype/hive_settings/hive_settings.json b/bwh_hive/bwh_hive/doctype/hive_settings/hive_settings.json index a4e4573..3d7c402 100644 --- a/bwh_hive/bwh_hive/doctype/hive_settings/hive_settings.json +++ b/bwh_hive/bwh_hive/doctype/hive_settings/hive_settings.json @@ -27,6 +27,7 @@ "skills_repo", "anthropic_api_key", "claude_code_oauth_token", + "openai_api_key", "agent_prompts_section", "agent_spec_prompt", "agent_implement_prompt", @@ -182,6 +183,12 @@ "fieldtype": "Password", "label": "Claude Code OAuth Token" }, + { + "description": "OpenAI API key, injected as OPENAI_API_KEY into boxes whose project uses the Codex engine.", + "fieldname": "openai_api_key", + "fieldtype": "Password", + "label": "OpenAI API Key" + }, { "collapsible": 1, "fieldname": "agent_prompts_section", diff --git a/bwh_hive/bwh_hive/orchestrator/service.py b/bwh_hive/bwh_hive/orchestrator/service.py index 973aa29..f5bd289 100644 --- a/bwh_hive/bwh_hive/orchestrator/service.py +++ b/bwh_hive/bwh_hive/orchestrator/service.py @@ -120,14 +120,20 @@ def build_boot_env(task: Document) -> dict: spec_run_timeout = max(spec_min - 5, 5) * 60 impl_run_timeout = max(impl_min - 5, 5) * 60 - # Claude auth: an Anthropic API key (API billing) OR a subscription OAuth token from - # `claude setup-token`. The box's claude falls back to the OAuth token when - # ANTHROPIC_API_KEY is empty, so only forward the token when no API key is set — - # never both, to avoid an ambiguous auth mode. + # Coding-agent engine (per-project): "codex" or "claude" (default). Drives which CLI the + # box runs (AGENT_ENGINE) and which auth key it needs. + engine = "codex" if (project.get("agent_engine") or "").strip().lower().startswith("codex") else "claude" + + # Auth keys (global, Hive Settings). Only the chosen engine's key(s) are forwarded. + # Claude: an Anthropic API key (API billing) OR a subscription OAuth token from + # `claude setup-token` — the box falls back to the token when ANTHROPIC_API_KEY is empty, + # so forward the token only when no API key is set (never both). Codex: OPENAI_API_KEY. anthropic_api_key = settings.get_password("anthropic_api_key", raise_exception=False) or "" oauth_token = settings.get_password("claude_code_oauth_token", raise_exception=False) or "" + openai_api_key = settings.get_password("openai_api_key", raise_exception=False) or "" env = { + "AGENT_ENGINE": engine, "AGENT_MODE": "1", "HIVE_BASE_URL": frappe.utils.get_url(), "HIVE_API_KEY": settings.agent_callback_api_key or "", @@ -141,8 +147,9 @@ def build_boot_env(task: Document) -> dict: "TARGET_APP_REPO": project.get("target_app_repo") or "", "TARGET_APP_BRANCH": project.get("target_app_branch") or "develop", "SKILLS_REPO": skills_repo or "", - "ANTHROPIC_API_KEY": anthropic_api_key, - "CLAUDE_CODE_OAUTH_TOKEN": oauth_token if not anthropic_api_key else "", + "ANTHROPIC_API_KEY": anthropic_api_key if engine == "claude" else "", + "CLAUDE_CODE_OAUTH_TOKEN": oauth_token if (engine == "claude" and not anthropic_api_key) else "", + "OPENAI_API_KEY": openai_api_key if engine == "codex" else "", "SPEC_RUN_TIMEOUT": spec_run_timeout, "IMPL_RUN_TIMEOUT": impl_run_timeout, } diff --git a/frontend/src/components/project/AgentSettingsTab.tsx b/frontend/src/components/project/AgentSettingsTab.tsx index 2d65b73..1b012ad 100644 --- a/frontend/src/components/project/AgentSettingsTab.tsx +++ b/frontend/src/components/project/AgentSettingsTab.tsx @@ -10,6 +10,7 @@ import { SwitchField, SecretField, PromptField, + SelectField, } from "@/components/settings/agent-fields" import { PROMPT_TOKENS } from "@/lib/agent" import type { HiveProject } from "@/types" @@ -70,6 +71,7 @@ export function AgentSettingsTab({ projectId, onSaved }: AgentSettingsTabProps) // eslint-disable-next-line react-hooks/set-state-in-effect setForm({ agent_enabled: data.agent_enabled ?? 0, + agent_engine: data.agent_engine ?? "Claude Code", agent_template_slug: data.agent_template_slug ?? "", target_app_name: data.target_app_name ?? "", target_app_repo: data.target_app_repo ?? "", @@ -128,6 +130,17 @@ export function AgentSettingsTab({ projectId, onSaved }: AgentSettingsTabProps) {enabled && ( <>
{hint}
} + +