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
65 changes: 16 additions & 49 deletions zenith/src/zenith_harness/acp_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ def _acp_subprocess_env(provider) -> dict[str, str]:
# Env-var hints — harmless if codex ignores them.
env["CODEX_SANDBOX"] = "danger-full-access"
env["CODEX_DISABLE_SANDBOX"] = "1"
else:
env.pop("CODEX_SANDBOX", None)
env.pop("CODEX_DISABLE_SANDBOX", None)
return env


Expand Down Expand Up @@ -566,13 +569,6 @@ async def run_node(
handoff_path = store.attempt_path(project_id, mission_id, spawn_ts, task.id)
handoff_path.parent.mkdir(parents=True, exist_ok=True)

# 0) For claude-agent-acp: drop a project-level settings.json that
# overrides the user's global ~/.claude/settings.json. Adapter
# rejects session/new with `Invalid permissions.defaultMode: auto`
# if the user's global setting carries the SDK-unsupported "auto"
# default.
_ensure_claude_settings(Path(workspace_dir), role_config.worker_provider)

# 1) Start the worker MCP server subprocess.
mcp_port = self._find_free_port()
mcp_process = await self._start_worker_mcp_server(
Expand Down Expand Up @@ -646,6 +642,7 @@ async def run_node(
"clientInfo": {"name": "zenith", "version": "0.1.0"},
},
)
await self._maybe_refresh_auth(client, role_config.worker_provider)
session_params: dict[str, Any] = {
"cwd": workspace_dir,
"mcpServers": [worker_mcp_cfg],
Expand Down Expand Up @@ -732,9 +729,6 @@ async def run_terminal_review(
report_path = store.terminal_review_path(project_id, mission_id, spawn_ts)
report_path.parent.mkdir(parents=True, exist_ok=True)

# Same claude-agent-acp settings workaround as run_node.
_ensure_claude_settings(Path(workspace_dir), role_config.worker_provider)

mcp_port = self._find_free_port()
mcp_process = await self._start_terminal_reviewer_mcp(
project_id=project_id,
Expand Down Expand Up @@ -792,6 +786,7 @@ async def run_terminal_review(
"clientInfo": {"name": "zenith", "version": "0.1.0"},
},
)
await self._maybe_refresh_auth(client, role_config.worker_provider)
session_params: dict[str, Any] = {
"cwd": workspace_dir,
"mcpServers": [worker_mcp_cfg],
Expand Down Expand Up @@ -947,6 +942,17 @@ async def _maybe_set_mode(
f"Failed to set ACP runtime mode {mode!r} for {provider.name}: {exc}"
) from exc

async def _maybe_refresh_auth(self, client: ACPClient, provider) -> None:
if getattr(provider, "name", None) != "codex":
return
try:
status = await client.send_request("authentication/status", {})
except Exception: # noqa: BLE001
return
if not isinstance(status, dict) or status.get("type") != "chat-gpt":
return
await client.send_request("authenticate", {"methodId": "chat-gpt"})

# ------------------------------------------------------------------
# Prompt rendering
# ------------------------------------------------------------------
Expand Down Expand Up @@ -1190,45 +1196,6 @@ def review(
)


# ---------------------------------------------------------------------------
# claude-agent-acp settings workaround
# ---------------------------------------------------------------------------


def _ensure_claude_settings(workspace: Path, provider) -> None:
"""Write `<workspace>/.claude/settings.json` overriding `permissions.defaultMode`.

The `@zed-industries/claude-agent-acp` adapter loads settings as
user → project → local → enterprise (last write wins). Without this, a
user with `"permissions": {"defaultMode": "auto"}` in their global
`~/.claude/settings.json` will see session/new fail with
`Invalid permissions.defaultMode: auto.` — the Claude Code SDK does not
accept "auto".

We touch this file only when:
- The provider declares a non-empty `acp_runtime_mode` (i.e. claude).
- The file does not already exist (respect any user-authored override).

In v5 the workspace is the user's repo, so we conservatively no-op on
pre-existing files.
"""
mode = getattr(provider, "acp_runtime_mode", None)
if not mode:
return
claude_dir = workspace / ".claude"
claude_dir.mkdir(parents=True, exist_ok=True)
settings_path = claude_dir / "settings.json"
if settings_path.exists():
# Respect user-authored settings; the user can put whatever they want
# in there. If their setting is "auto" we can't help — they need to
# change it manually.
return
settings_path.write_text(
json.dumps({"permissions": {"defaultMode": mode}}, indent=2) + "\n",
encoding="utf-8",
)


__all__ = [
"ACPClient",
"ACPError",
Expand Down
2 changes: 1 addition & 1 deletion zenith/src/zenith_harness/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ def _resolve_selection(
) -> ProviderSelection:
if agent and orchestrator and agent != orchestrator:
raise click.UsageError("--agent conflicts with --orchestrator-provider")
orch = orchestrator or agent or "claude"
orch = orchestrator or agent or "codex"
wrk = worker or (agent if agent in provider_names_for_role("worker") else None) or default_worker_provider_name(orch)
return ProviderSelection(
orchestrator=get_provider(orch),
Expand Down
2 changes: 1 addition & 1 deletion zenith/src/zenith_harness/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def discover(cls) -> HarnessConfig:
or harness_home / "projects"
)
orchestrator_provider_name = os.environ.get(
"ZENITH_ORCHESTRATOR_PROVIDER", "claude"
"ZENITH_ORCHESTRATOR_PROVIDER", "codex"
)
worker_provider_name = os.environ.get(
"ZENITH_WORKER_PROVIDER"
Expand Down
4 changes: 1 addition & 3 deletions zenith/src/zenith_harness/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,8 @@ def _dedupe_paths(paths: tuple[str, ...]) -> tuple[str, ...]:
skill_dirs=(".claude/skills", ".agents/skills"),
skill_alias_dirs=(".claude/skills", ".agents/skills"),
config_format="mcp_json",
default_worker_acp_command="claude-agent-acp",
agent_output_dir=".claude/agents",
orchestrator_prompt_output_path=".claude/orchestrator_prompt.md",
acp_runtime_mode="bypassPermissions",
),
"codex": ProviderDefinition(
name="codex",
Expand Down Expand Up @@ -158,4 +156,4 @@ def provider_names_for_role(role: Literal["orchestrator", "worker"]) -> tuple[st
def default_worker_provider_name(orchestrator_provider_name: str) -> str:
if orchestrator_provider_name in PROVIDERS:
return orchestrator_provider_name
return "claude"
return "codex"