Skip to content
Open
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
10 changes: 10 additions & 0 deletions bwh_hive/bwh_hive/doctype/hive_project/hive_project.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"github_repo",
"agent_section",
"agent_enabled",
"agent_engine",
"github_pat",
"agent_template_slug",
"target_app_name",
Expand Down Expand Up @@ -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).",
Expand Down
7 changes: 7 additions & 0 deletions bwh_hive/bwh_hive/doctype/hive_settings/hive_settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
19 changes: 13 additions & 6 deletions bwh_hive/bwh_hive/orchestrator/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 "",
Expand All @@ -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,
}
Expand Down
13 changes: 13 additions & 0 deletions frontend/src/components/project/AgentSettingsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 ?? "",
Expand Down Expand Up @@ -128,6 +130,17 @@ export function AgentSettingsTab({ projectId, onSaved }: AgentSettingsTabProps)
{enabled && (
<>
<div className="space-y-4">
<SelectField
id="proj-agent-engine"
label="Agent engine"
hint="Codex uses the OpenAI API key; Claude Code uses the Anthropic key or subscription token (both in global Agent settings)."
value={s("agent_engine") || "Claude Code"}
onChange={set("agent_engine")}
options={[
{ value: "Claude Code", label: "Claude Code" },
{ value: "Codex", label: "Codex" },
]}
/>
<TextField id="proj-agent-template" label="Template slug" hint="Blank = the global default template." value={s("agent_template_slug")} onChange={set("agent_template_slug")} placeholder="Inherit global default" />
</div>

Expand Down
10 changes: 9 additions & 1 deletion frontend/src/components/settings/AgentSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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 ?? "",
Expand Down Expand Up @@ -163,7 +169,9 @@ export function AgentSection() {
<SecretField id="agent-bs-secret" label="BenchSpace API Secret" isSet={secretsSet.benchspace_api_secret} value={s("benchspace_api_secret")} onChange={set("benchspace_api_secret")} />
<TextField id="agent-cb-key" label="Agent Callback API Key" value={s("agent_callback_api_key")} onChange={set("agent_callback_api_key")} />
<SecretField id="agent-cb-secret" label="Agent Callback API Secret" isSet={secretsSet.agent_callback_api_secret} value={s("agent_callback_api_secret")} onChange={set("agent_callback_api_secret")} />
<SecretField id="agent-anthropic-key" label="Anthropic API Key" isSet={secretsSet.anthropic_api_key} value={s("anthropic_api_key")} onChange={set("anthropic_api_key")} />
<SecretField id="agent-anthropic-key" label="Anthropic API Key" hint="Claude Code (API billing)." isSet={secretsSet.anthropic_api_key} value={s("anthropic_api_key")} onChange={set("anthropic_api_key")} />
<SecretField id="agent-claude-oauth" label="Claude Code OAuth Token" hint="claude setup-token — used when no Anthropic API key is set (subscription auth)." isSet={secretsSet.claude_code_oauth_token} value={s("claude_code_oauth_token")} onChange={set("claude_code_oauth_token")} />
<SecretField id="agent-openai-key" label="OpenAI API Key" hint="Used by projects on the Codex engine." isSet={secretsSet.openai_api_key} value={s("openai_api_key")} onChange={set("openai_api_key")} />
<TextField id="agent-skills-repo" label="Skills Repo" value={s("skills_repo")} onChange={set("skills_repo")} placeholder="owner/repo" />
</div>

Expand Down
36 changes: 36 additions & 0 deletions frontend/src/components/settings/agent-fields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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 (
<div className="grid gap-1.5">
<Label htmlFor={id}>{label}</Label>
{hint && <p className="text-xs text-muted-foreground">{hint}</p>}
<Select value={value} onValueChange={onChange}>
<SelectTrigger id={id} className="h-8 text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
{options.map((o) => (
<SelectItem key={o.value} value={o.value}>
{o.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)
}

export function IntField({
id,
label,
Expand Down
1 change: 1 addition & 0 deletions frontend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading