From 948cdf419d1d73553b4377b8dead4135ca122c6a Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Fri, 3 Jul 2026 11:49:32 +0530 Subject: [PATCH] feat: per-project agent engine (Claude Code / Codex) + OpenAI key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lets each project pick its coding-agent engine, and delivers the matching auth key to the box. - Hive Project: new `agent_engine` Select (Claude Code default / Codex). - Hive Settings: new global `openai_api_key` (Password). - build_boot_env: emits AGENT_ENGINE and forwards ONLY the chosen engine's key(s) — Codex → OPENAI_API_KEY; Claude → ANTHROPIC_API_KEY, or the subscription CLAUDE_CODE_OAUTH_TOKEN when no API key is set. Verified on pms.localhost: gating correct across claude+key / claude+token / codex. - Frontend: reusable SelectField; engine selector on the project Agent tab; OpenAI key + (completing #176's UI gap) the Claude OAuth token on the global Agent settings tab. yarn build green, changed files lint-clean. Box side (agent_command/summarize engine abstraction, codex baked into the golden) ships in the paired BenchSpace PR. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_011C9jzHhfxDqtU13uWtazZm --- .../doctype/hive_project/hive_project.json | 10 ++++++ .../doctype/hive_settings/hive_settings.json | 7 ++++ bwh_hive/bwh_hive/orchestrator/service.py | 19 ++++++---- .../components/project/AgentSettingsTab.tsx | 13 +++++++ .../src/components/settings/AgentSection.tsx | 10 +++++- .../src/components/settings/agent-fields.tsx | 36 +++++++++++++++++++ frontend/src/types.ts | 1 + 7 files changed, 89 insertions(+), 7 deletions(-) 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 && ( <>
+
diff --git a/frontend/src/components/settings/AgentSection.tsx b/frontend/src/components/settings/AgentSection.tsx index 4e6138b..74701f1 100644 --- a/frontend/src/components/settings/AgentSection.tsx +++ b/frontend/src/components/settings/AgentSection.tsx @@ -23,6 +23,8 @@ interface HiveSettings { default_agent_template_slug?: string | null skills_repo?: string | null anthropic_api_key?: string | null + claude_code_oauth_token?: string | null + openai_api_key?: string | null agent_spec_prompt?: string | null agent_implement_prompt?: string | null agent_changes_prompt?: string | null @@ -43,6 +45,8 @@ const SECRET_FIELDS = [ "benchspace_api_secret", "agent_callback_api_secret", "anthropic_api_key", + "claude_code_oauth_token", + "openai_api_key", "telegram_bot_token", ] as const @@ -82,6 +86,8 @@ export function AgentSection() { default_agent_template_slug: data.default_agent_template_slug ?? "", skills_repo: data.skills_repo ?? "", anthropic_api_key: "", + claude_code_oauth_token: "", + openai_api_key: "", agent_spec_prompt: data.agent_spec_prompt ?? "", agent_implement_prompt: data.agent_implement_prompt ?? "", agent_changes_prompt: data.agent_changes_prompt ?? "", @@ -163,7 +169,9 @@ export function AgentSection() { - + + + diff --git a/frontend/src/components/settings/agent-fields.tsx b/frontend/src/components/settings/agent-fields.tsx index a58af87..70381e7 100644 --- a/frontend/src/components/settings/agent-fields.tsx +++ b/frontend/src/components/settings/agent-fields.tsx @@ -2,6 +2,7 @@ import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Switch } from "@/components/ui/switch" import { Textarea } from "@/components/ui/textarea" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" export function FieldLabel({ label, hint }: { label: string; hint?: string }) { return ( @@ -36,6 +37,41 @@ export function TextField({ ) } +export function SelectField({ + id, + label, + hint, + value, + onChange, + options, +}: { + id: string + label: string + hint?: string + value: string + onChange: (v: string) => void + options: { value: string; label: string }[] +}) { + return ( +
+ + {hint &&

{hint}

} + +
+ ) +} + export function IntField({ id, label, diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 9176434..abe4a71 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -13,6 +13,7 @@ export interface HiveProject { links?: HiveProjectLink[] // Agent settings (specs/v2 09 — surface 3) agent_enabled?: 0 | 1 + agent_engine?: "Claude Code" | "Codex" | null github_pat?: string | null agent_template_slug?: string | null target_app_name?: string | null