Skip to content

Feat/cli auth autodetect -- restore claude cli integration and auto cli detection#399

Open
jasonkneen wants to merge 7 commits into
Gitlawb:mainfrom
jasonkneen:feat/cli-auth-autodetect
Open

Feat/cli auth autodetect -- restore claude cli integration and auto cli detection#399
jasonkneen wants to merge 7 commits into
Gitlawb:mainfrom
jasonkneen:feat/cli-auth-autodetect

Conversation

@jasonkneen

@jasonkneen jasonkneen commented Jul 2, 2026

Copy link
Copy Markdown

Summary

Linked issue

This pull request introduces a new package, internal/agentcli, which provides a unified API for detecting, probing, and extracting credentials from third-party agent CLI tools (such as Claude, Codex, Gemini CLI, etc.). It also adds comprehensive tests for this package, and strengthens invariants in the agent tool filtering logic. Additionally, it documents a full code review and records that no defects were found in a recent audit.

The most important changes are:

New agent CLI detection and credential extraction API

  • Added the agentcli package, which catalogs known agent CLI harnesses, provides functions to detect their presence on PATH, probes their login state via credential files or keychain entries, and exposes a stable, testable API for other packages to build on. This centralizes all harness-specific logic in one place, making maintenance and updates easier. (internal/agentcli/agentcli.go)
  • The catalog includes detailed metadata for each harness (ID, binaries, vendor, credential file/keychain locations, etc.) and supports testable dependency injection for all side-effecting operations. (internal/agentcli/agentcli.go)

Comprehensive tests for agentcli

  • Added a full test suite for the agentcli package, covering catalog invariants, detection logic, credential probing (including file and keychain scenarios), and API behaviors. (internal/agentcli/agentcli_test.go)

Strengthened tool filtering invariants

  • Added a test to lock down the invariant that /onlybash mode strictly allowlists only bash and skill tools, ensuring that delegation (e.g., to "Task") is blocked and cannot escape the restricted surface. (internal/agent/loop_test.go)

Documentation and audit records

  • Added a task file describing the scope and goals of a full code review for the repository, prioritizing correctness, security, and maintainability. (cascade/00-task.txt)
  • Recorded that a full review found zero defects in the prioritized packages, with race and vet checks passing. (cascade/10-findings.cf)

Fixes #

Checklist

  • The linked issue already has the issue-approved label.
  • go build ./..., go vet ./..., and go test ./... pass locally.
  • gofmt clean.
  • Tests added/updated for the change (and run under -race where relevant).
  • UI changes include screenshots or a short recording where possible.

Summary by CodeRabbit

  • New Features

    • Added support for connecting with local agent CLIs, including login status detection and CLI-based provider setup.
    • Added a new /onlybash mode to quickly restrict the toolset to bash and skill tools.
    • Specialist runs can now use external harnesses, with manifest support for harness and provider settings.
  • Bug Fixes

    • Improved credential handling so CLI-based profiles stay keyless and are treated as valid connections.
    • Added clearer guidance and status output when a local CLI login is detected or has expired.

Introduces a new `agentcli` layer to catalog installed agent CLIs, detect login state, and extract reusable local credentials (notably Codex and Claude). Provider config now supports `authCLI`, and the provider factory can build CLI-authenticated providers, including Claude token refresh/login flows, new `zero auth claude`, and richer `auth status` output with detected CLI logins. Specialist/swarm execution gains harness-backed members with stream decoders for harness output formats plus manifest fields for `harness`/`provider`. The TUI onboarding/provider wizard now supports “use detected CLI login” paths, and runtime adds an `onlybash` mode (`/onlybash`) that enforces bash+skill-only tooling.
Copilot AI review requested due to automatic review settings July 2, 2026 16:43
@jasonkneen jasonkneen changed the title Feat/cli auth autodetect Feat/cli auth autodetect -- restore claude cli integration and auto cli detection Jul 2, 2026
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR adds a new agentcli package for detecting and reusing local agent CLI logins (Claude Code, Codex), integrates it into provider construction, config, CLI commands, and TUI onboarding/wizard/picker flows. It also adds an onlybash tool-restriction mode and specialist/swarm harness execution paths that run external agent CLIs.

Changes

Agent CLI Detection and Credential Reuse

Layer / File(s) Summary
Harness catalog and detection
internal/agentcli/agentcli.go, internal/agentcli/agentcli_test.go
New agentcli package with harness catalog, PATH-based detection, keychain/credential-file login probing, and helpers Harnesses, Lookup, Detect, DetectOne.
Credential extraction
internal/agentcli/credentials.go, internal/agentcli/credentials_test.go
Extracts Codex/Claude credentials from auth files and keychain, returning normalized Credentials.
Config AuthCLI field
internal/config/types.go, internal/config/credentials.go, internal/config/*_test.go
ProviderProfile.AuthCLI field added, treated as a configured credential and blocking OAuth candidates.
Provider factory CLI auth
internal/providers/factory.go, internal/providers/agentclicreds.go, internal/providers/agentclicreds_test.go
newCLIAuthedProvider builds bearer/account resolvers per harness, with Claude refresh-token persistence and error surfacing.
Anthropic Claude identity
internal/providers/anthropic/provider.go, internal/providers/anthropic/provider_test.go
Injects Claude Code identity system block and beta flag for OAuth-based Claude Code sessions.
Claude Code OAuth flow
internal/provideroauth/claudecode.go, internal/provideroauth/claudecode_test.go
PKCE paste-code login and refresh-token exchange implementation.
CLI auth/detect/app wiring
internal/cli/auth.go, internal/cli/app.go, internal/cli/provider_detect.go, internal/cli/*_test.go
zero auth claude command, agent-CLI status reporting, and detectAgentCLIs dependency wiring.
Onboarding advice and model discovery
internal/provideronboarding/advice.go, internal/cli/provider_models.go, related tests
Missing-credential hints reference detected CLI logins; model discovery stamps AuthCLI credentials onto probe requests.
TUI onboarding, wizard, picker
internal/tui/onboarding.go, internal/tui/provider_wizard.go, internal/tui/picker.go, internal/tui/command_center.go, related tests
CLI-connect method skips provider/credential steps, persists AuthCLI profiles, and updates model picker/switch credential gating.

Onlybash Tool Restriction Mode

Layer / File(s) Summary
onlybash preset and filter tests
internal/modelregistry/modes.go, internal/tools/onlybash_mode_test.go, internal/agent/loop_test.go
New onlybash preset restricts tools to bash/skill and blocks Task/tool_search.
/onlybash TUI command
internal/tui/session_controls.go, internal/tui/commands.go, internal/tui/model.go, related tests
Toggles onlybash mode, stashing/restoring prior operator tool filters.

Specialist and Swarm External Harness Execution

Layer / File(s) Summary
Manifest harness metadata
internal/specialist/manifest.go, internal/specialist/format.go, related tests
Adds harness/provider/harnessArgs parsing, validation, and extends-merging.
Executor harness run
internal/specialist/exec.go, internal/specialist/harness.go, related tests
Routes harness-pinned specialists to runHarness, refactors child process/decoder plumbing.
Stream decoders
internal/specialist/harness_decode.go, internal/specialist/harness_decode_test.go
Claude/Codex/text decoders convert harness stdout into stream events.
Swarm wiring
internal/swarm/definition.go, internal/swarm/member.go, internal/swarm/team.go, internal/swarm/launcher_specialist.go, tests
Threads Harness/Provider through swarm member specs and manifests.

Estimated code review effort: 5 (Critical) | ~150 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant TUI as TUI Onboarding
  participant agentcli
  participant Factory as Provider Factory
  participant Anthropic

  User->>TUI: Select CLI connect method
  TUI->>agentcli: Detect() / DetectOne()
  agentcli-->>TUI: Detection (LoggedIn/LoggedOut)
  TUI->>TUI: Skip provider/credential steps, set AuthCLI
  User->>Factory: Save profile with AuthCLI
  Factory->>agentcli: ExtractCredentials(harness)
  Factory->>Anthropic: Build provider with bearer resolver
  Anthropic-->>Factory: Authorization header on request
Loading
sequenceDiagram
  participant Orchestrator
  participant Executor
  participant agentcli
  participant HarnessProcess as External Harness CLI
  participant Decoder

  Orchestrator->>Executor: runFresh(manifest with Harness)
  Executor->>agentcli: DetectOne(harness)
  Executor->>HarnessProcess: runChildWithDecoder(args, env)
  HarnessProcess-->>Decoder: stdout NDJSON lines
  Decoder-->>Executor: streamjson.Event(s)
  Executor-->>Orchestrator: ExecResult with final text
Loading

Possibly related PRs

  • Gitlawb/zero#58: Both PRs modify Anthropic provider request/system prompt mapping and beta-flag handling in internal/providers/anthropic/provider.go.
  • Gitlawb/zero#100: Both PRs touch specialist manifest parsing/validation in internal/specialist/manifest.go, with this PR building harness/provider metadata on top.
  • Gitlawb/zero#366: Both PRs touch config.ProviderProfile.HasConfiguredCredential() and OAuth login candidate selection logic.

Suggested reviewers: Vasanthdev2004, kevincodex1

Poem:
A rabbit sniffs each harness door,
Claude and Codex, checked once more.
Bearer tokens refresh with care,
Onlybash trims the tool-filled air.
Harness runs in specialist's den—
Hop along, review, then land, then win. 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: adding agent-CLI autodetection and restoring Claude CLI integration.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a unified “agent CLI” integration layer and wires it through the TUI, CLI, provider factory, and specialist/swarm execution paths so Zero can (a) detect third‑party agent CLIs, (b) reuse their local logins as provider credentials, and (c) run specialists via external harness CLIs when requested. It also adds an /onlybash mode preset + command to hard-restrict the tool surface and extends configuration to persist CLI-authenticated provider profiles (AuthCLI).

Changes:

  • Added internal/agentcli for detecting installed agent CLIs, probing login state, and extracting reusable credentials (Claude/Codex supported), plus tests.
  • Added CLI-authenticated provider profiles (AuthCLI) and updated TUI onboarding/provider wizard/model picker + provider factory to build providers using live-extracted CLI credentials (including Claude Code identity requirements).
  • Added harness execution support for specialists (and swarm per-member harness/provider overrides) and introduced /onlybash mode + invariants tests.

Reviewed changes

Copilot reviewed 57 out of 57 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
internal/tui/session_controls.go Adds /onlybash command handling with stash/restore of tool filters.
internal/tui/provider_wizard.go Adds CLI-based connect methods and wizard plumbing for CLI-auth profiles.
internal/tui/provider_wizard_test.go Updates wizard tests for new method-option plumbing.
internal/tui/provider_wizard_oauth_test.go Updates OAuth-path tests for new method-option plumbing.
internal/tui/provider_wizard_cli_test.go New tests covering CLI method rows, selection, profile shaping, and persistence.
internal/tui/picker.go Makes CLI-auth profiles distinct in picker grouping; avoids env key autofill for AuthCLI.
internal/tui/options.go Extends setup selection with AuthCLI.
internal/tui/onlybash_command_test.go New tests for /onlybash state transitions and invariants.
internal/tui/onboarding.go Adds CLI connect method to first-run setup and threads AuthCLI through save.
internal/tui/onboarding_test.go Updates setup tests for method-kind enum.
internal/tui/onboarding_cli_test.go New tests for CLI setup path, filtering, and AuthCLI propagation.
internal/tui/model.go Adds onlybash state fields and command routing for /onlybash.
internal/tui/model_switch_cli_test.go New tests ensuring AuthCLI profiles can switch models and aren’t normalized incorrectly.
internal/tui/commands.go Registers /onlybash command.
internal/tui/commands_test.go Adds parsing coverage for /onlybash.
internal/tui/command_center.go Exempts AuthCLI profiles from “no usable credential” gate for /model switching.
internal/tools/onlybash_mode_test.go Ensures onlybash mode tool-name literals match registered tool names.
internal/swarm/team.go Threads Definition harness/provider overrides into member spec.
internal/swarm/member.go Adds Harness and Provider fields to member spec.
internal/swarm/launcher_specialist.go Threads harness/provider from member spec into specialist manifest metadata.
internal/swarm/harness_test.go New tests asserting harness/provider plumbing through swarm + specialist manifest.
internal/swarm/definition.go Adds Harness and Provider fields to swarm definitions.
internal/specialist/manifest.go Adds harness/provider/harnessArgs metadata, parsing, validation, and summaries.
internal/specialist/harness.go Adds execution path for running specialists via external harness CLIs.
internal/specialist/harness_manifest_test.go New tests for harness/provider/harnessArgs parsing + extends merging + validation.
internal/specialist/harness_decode.go Adds harness stdout decoders (Claude stream-json, Codex NDJSON) with fallback.
internal/specialist/harness_decode_test.go New tests for harness decoder selection and decoding behavior.
internal/specialist/harness_child_test.go New tests for provider env pinning, cwd wiring, stderr/exit capture, progress callback.
internal/specialist/format.go Includes harness/provider fields in specialist show output.
internal/specialist/exec.go Adds harness dispatch, provider env pinning for self-exec children, and decoder abstraction.
internal/providers/factory.go Adds AuthCLI branch to build providers using agentcli-sourced credentials.
internal/providers/anthropic/provider.go Adds Claude Code identity support (beta flag + required system block).
internal/providers/anthropic/provider_test.go Tests Claude Code identity injection and beta behavior.
internal/providers/agentclicreds.go Implements provider construction + token resolution for CLI-authenticated profiles.
internal/providers/agentclicreds_test.go Extensive tests for CLI-auth provider behavior, refresh logic, and error shaping.
internal/provideronboarding/advice.go Threads agentcli detections into “missing credential” advice and treats AuthCLI as credentialed.
internal/provideronboarding/advice_test.go Tests AuthCLI credential recognition and detection-based hints.
internal/provideroauth/claudecode.go Adds Claude Code login + refresh PKCE/token-refresh flows (used for zero-owned Claude login).
internal/provideroauth/claudecode_test.go Tests Claude Code refresh + login flows and error handling.
internal/modelregistry/modes.go Adds onlybash preset mode (tool surface restriction only).
internal/modelregistry/modes_test.go Tests onlybash preset behavior and model-resolution guard for empty model.
internal/config/types.go Adds AuthCLI to provider profiles and JSON decoding/aliases.
internal/config/types_test.go Tests AuthCLI JSON round-trip and omitempty behavior.
internal/config/credentials.go Treats AuthCLI as a configured credential (affects OAuth resolver attachment).
internal/config/credentials_test.go Tests HasConfiguredCredential and OAuth candidates gating for AuthCLI.
internal/cli/setup.go Ensures CLI-auth selections clear APIKey/APIKeyEnv and persist AuthCLI.
internal/cli/provider_detect.go Adds agent-CLI detections into provider-detect reporting/advice surface.
internal/cli/auth.go Adds zero auth claude and extends auth status to list detected agent CLIs.
internal/cli/auth_test.go Tests agent-CLI section rendering and JSON output in auth status.
internal/agentcli/credentials.go Implements credential extraction for codex + claude stores (file/keychain).
internal/agentcli/credentials_test.go Tests credential extraction behavior and shadowing edge cases.
internal/agentcli/agentcli.go Adds agent CLI catalog + Detect/DetectOne probing and macOS keychain read support.
internal/agentcli/agentcli_test.go Tests catalog invariants, detection behavior, login probing, and LoginCommand.
internal/agent/loop_test.go Adds invariant test ensuring onlybash allowlist blocks delegation and blocks tool_search via denylist.
cascade/10-findings.cf Adds an audit/findings record file.
cascade/00-task.txt Adds a task file describing review scope/goals.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 410 to 412
func (m model) newProviderWizard() *providerWizardState {
detections := agentcli.Detect(agentcli.Deps{})
providers := providerWizardProviders()
Comment on lines +124 to +128
detections := agentcli.Detect(agentcli.Deps{})
state := setupState{
visible: options.Visible,
required: options.Required,
configPath: strings.TrimSpace(options.ConfigPath),
Comment on lines +374 to +378
out, err := exec.Command("/usr/bin/security", "find-generic-password", "-s", service, "-w").Output()
if err != nil {
return nil, fmt.Errorf("agentcli: keychain lookup for %q: %w", service, err)
}
return bytes.TrimRight(out, "\n"), nil
@kevincodex1

Copy link
Copy Markdown
Contributor

hello @jasonkneen kindly rebase to main and fix conflicts

@Vasanthdev2004

Copy link
Copy Markdown
Collaborator

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (6)
internal/tui/onboarding.go (1)

718-748: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate model-stage bootstrap logic between the CLI fast path and the generic advance path.

The CLI branch (lines 740-748) repeats the same resetSetupModels/modelErr/modelGen++/setupModelDiscoveryCmd/modelLoad sequence as the default post-advance flow (lines 797-803). Extracting a small helper (e.g. m.enterModelStage()) would prevent the two copies drifting apart.

♻️ Suggested helper extraction
+func (m *model) enterModelStage() tea.Cmd {
+	m.setup.stage = setupStageModel
+	m.resetSetupModels()
+	m.setup.modelErr = ""
+	m.setup.modelGen++
+	cmd := m.setupModelDiscoveryCmd(m.setup.modelGen)
+	m.setup.modelLoad = cmd != nil
+	return cmd
+}

Then both call sites become cmd := m.enterModelStage(); return m, cmd.

Also applies to: 795-804

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui/onboarding.go` around lines 718 - 748, The CLI fast path in
providerWizardMethodCLI duplicates the same model-stage setup sequence used in
the generic advance path, which risks the two flows drifting apart. Extract the
repeated reset-and-bootstrap steps into a shared helper on the onboarding model,
such as m.enterModelStage(), and have both the CLI branch and the default
post-advance flow call it. Keep the helper responsible for resetSetupModels,
clearing modelErr, incrementing modelGen, creating the setupModelDiscoveryCmd,
and setting modelLoad consistently.
internal/tui/provider_wizard.go (1)

1779-1787: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Persist the provider default BaseURL for CLI profiles too.

providerWizardProfile() stores the descriptor default BaseURL, but CLI profiles currently persist it empty. Keep the profile shape consistent so provider construction and next-launch reloads do not depend on downstream default hydration.

Proposed fix
 func providerWizardCLIProfile(harness agentcli.Harness, provider providercatalog.Descriptor, model string) config.ProviderProfile {
 	return config.ProviderProfile{
 		Name:         harness.ID,
 		ProviderKind: providerWizardProviderKind(provider),
 		CatalogID:    provider.ID,
+		BaseURL:      provider.DefaultBaseURL,
 		APIFormat:    providerWizardAPIFormat(provider),
 		Model:        firstProviderDisplayValue(model, provider.DefaultModel),
 		AuthCLI:      harness.ID,
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui/provider_wizard.go` around lines 1779 - 1787, The CLI profile
builder in providerWizardCLIProfile is omitting the provider default BaseURL,
unlike providerWizardProfile, so update it to persist the descriptor’s default
BaseURL when constructing config.ProviderProfile. Keep the profile fields
aligned with the non-CLI path by using the provider value from
providercatalog.Descriptor so provider construction and reloads don’t rely on
later default hydration.
internal/provideronboarding/advice.go (1)

184-188: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse profile.HasConfiguredCredential() instead of a second hand-rolled credential check.

internal/cli/app.go's profileHasCredential was just updated to delegate to profile.HasConfiguredCredential() (added in the config layer specifically to be the canonical "does this profile have configured auth" check, AuthCLI included). This function checks the same three fields by hand instead. Any future credential type added to HasConfiguredCredential won't automatically flow into onboarding advice unless someone remembers to update both places.

♻️ Proposed consolidation
 func providerProfileHasCredential(profile config.ProviderProfile) bool {
-	return strings.TrimSpace(profile.APIKey) != "" ||
-		strings.TrimSpace(profile.AuthHeaderValue) != "" ||
-		strings.TrimSpace(profile.AuthCLI) != ""
+	return profile.HasConfiguredCredential()
 }

Please confirm HasConfiguredCredential()'s semantics match exactly (i.e., it doesn't also fold in APIKeyEnv resolution, which would change onboarding-advice behavior) before applying.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/provideronboarding/advice.go` around lines 184 - 188,
`providerProfileHasCredential` is duplicating the auth check instead of using
the canonical profile method. Update this helper in
`internal/provideronboarding/advice.go` to delegate to
`profile.HasConfiguredCredential()` so onboarding advice stays in sync with the
config layer. Before changing it, verify the `HasConfiguredCredential()`
semantics in the `config.ProviderProfile` type match the current behavior
exactly and do not include any environment-resolution logic.
internal/provideroauth/claudecode.go (2)

144-151: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Default 30s client timeout may be too short for this endpoint under load.

Third-party reports of this exact endpoint describe 40-60s response times during platform degradation (and Anthropic's own CLI has a known bug from a hardcoded 15s timeout being too aggressive). The default http.Client{Timeout: 30 * time.Second} used when no client is injected risks the same false-timeout failures; runAuthClaude already overrides with 60s for login, but RefreshClaudeCode's default (used wherever a caller doesn't inject a client) stays at 30s.

Also applies to: 283-290

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/provideroauth/claudecode.go` around lines 144 - 151, The default
HTTP timeout in `RefreshClaudeCode`/`runAuthClaude` is too aggressive for this
endpoint and can cause false timeouts under load. Update the fallback client
setup in `claudecode.go` so the default `http.Client` timeout is increased to
match the longer auth/login behavior already used elsewhere, and ensure both the
`RefreshClaudeCode` path and any other default-client path in this file use the
same timeout policy when `opts.HTTPClient` is nil.

71-79: 🔒 Security & Privacy | 🔵 Trivial

Reusing Claude subscription OAuth tokens outside Claude Code may violate Anthropic's ToS — worth a compliance sanity-check before shipping.

The design here explicitly mints/refreshes Claude Code's own OAuth client tokens for use inside a third-party CLI (zero). Recent third-party reporting describes Anthropic's consumer terms as restricting this:

"Using OAuth tokens obtained through a Free, Pro, or Max account in any other product, tool, or service constitutes a violation"

...though other reports describe mixed/relaxed enforcement for CLI-reuse patterns. This is unofficial secondary reporting, not Anthropic's own published terms, so treat it as a flag to verify rather than a settled fact. Given the blast radius (user account bans/API access revocation), it's worth having someone confirm current ToS language for this specific "sign in with Claude" flow before this ships broadly.

Also applies to: 265-283

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/provideroauth/claudecode.go` around lines 71 - 79, Add a compliance
check around ClaudeCodeLogin and the refresh path used by
RefreshClaudeCode/Claude Code token persistence so we are not reusing Claude
subscription OAuth tokens in zero without explicit validation. Review the
current token-minting flow in ClaudeCodeLogin and the token-store usage it
feeds, then either gate it behind a verified Anthropic ToS allowance or replace
it with a compliant auth approach; keep the fix localized to the Claude Code
login/token-refresh symbols so the behavior is easy to audit.
internal/specialist/harness.go (1)

17-61: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

No direct test coverage for runHarness.

harness_child_test.go and harness_decode_test.go cover the shared plumbing (childProviderEnv, runChildWithDecoder, decoders) but nothing exercises Executor.runHarness itself — e.g. the "not installed" error path, session-id/accounting wiring, or runFresh's background-rejection gate in exec.go (lines 341-343). Since runHarness calls runChildWithDecoder directly instead of going through the injectable RunChild seam, testing it end-to-end would currently require a real claude/codex binary on PATH.

Consider adding a small seam (mirroring executor.RunChild) so the accounting/error-handling logic in runHarness can be unit tested without a real external CLI.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/specialist/harness.go` around lines 17 - 61, `Executor.runHarness`
has no direct unit-test coverage because it calls `runChildWithDecoder`
directly, bypassing the injectable `RunChild` seam. Add a small indirection in
`runHarness` (or a similar injectable helper on `Executor`) so tests can stub
the child process and cover the harness-not-installed error path,
session/accounting wiring, and the background-rejection behavior without
requiring a real CLI binary.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/agentcli/agentcli.go`:
- Around line 370-379: The readKeychain helper currently shells out to
/usr/bin/security without any context, so a stalled keychain lookup can block
outbound requests indefinitely. Update readKeychain to use a context with
timeout when invoking exec.Command, and make sure the timeout is applied through
the command path used in agentcli.go so the security call can be canceled
cleanly. Keep the existing error wrapping in readKeychain, but ensure the
timeout/cancellation behavior is wired into the Deps.Keychain implementation
path described by resolveDeps and agentclicreds.go.

In `@internal/providers/agentclicreds.go`:
- Around line 155-196: `claudeRefreshingBearerResolver` can race when multiple
concurrent `StreamCompletion` calls invoke the same shared resolver, causing
duplicate `refresh(ctx, refreshToken)` attempts with the same token. Serialize
the resolver’s refresh path by adding a `sync`-based lock inside
`claudeRefreshingBearerResolver` so only one goroutine can evaluate
cache/extract/refresh/save at a time, and keep the existing fast-path token
reuse in place. Update the import block to include `sync`, and use the resolver
closure plus `store`, `refresh`, and `newestClaudeRefreshToken` as the key
points for the locking change.

In `@internal/specialist/exec.go`:
- Around line 37-40: Add a small sync test in internal/specialist that compares
zeroProviderEnvVar in exec.go against config.ActiveProviderEnv. The issue is
that zeroProviderEnvVar is a duplicated literal source of truth, so a future
rename of config.ActiveProviderEnv could silently break provider pinning; fix it
by adding a test that imports both symbols and asserts they stay equal.

In `@internal/specialist/harness_decode.go`:
- Around line 97-152: Update codexJSONDecoder to parse the current codex exec
--json event schema instead of the legacy nested msg format, since decodeLine
currently only handles agent_message/task_complete/token_count and will ignore
modern top-level events. Adjust codexLine and decodeLine to recognize
item.completed and turn.completed (including usage on turn.completed.usage), and
make sure the final assistant text is still preserved and emitted via
lastMessage/finish. Keep the fix localized to codexJSONDecoder, codexLine, and
decodeLine so current Codex JSONL output is surfaced correctly.

In `@internal/tui/onboarding.go`:
- Around line 124-138: `newSetupState` eagerly calls
`agentcli.Detect(agentcli.Deps{})` during TUI startup, causing blocking
PATH/file/keychain probes before setup is actually needed. Move the detection
behind the setup flow so it only runs when the onboarding/setup UI is opened,
and keep `setupState` initialization in `newSetupState` limited to cheap fields
like `visible`, `required`, `configPath`, and `selectedMethod`. Use
`agentcli.Detect` and `setupState` as the main points to refactor so the initial
render stays responsive.

In `@internal/tui/provider_wizard_cli_test.go`:
- Around line 112-113: The provider wizard tests are constructing the wizard
through m.newProviderWizard(), which still triggers agentcli.Detect before the
test overrides are applied. Update the setup to build the wizard state with
injected detections directly, or add a test-only deps hook so newModel and
providerWizard initialization do not touch real CLI detection. Keep the fix
centered around newModel, m.newProviderWizard, and the agentCLIDetections
override used in these tests.

In `@internal/tui/provider_wizard.go`:
- Around line 543-567: The CLI method selection in providerWizard.go is using
stale login state from methodOptions(), so the wizard can keep rejecting a
harness after the user logs in. Before setting the logged-out error in the
providerWizardMethodCLI branch, refresh the underlying CLI detection state (or
re-run the harness probe used by agentCLIDetections) and rebuild the selected
option so selected.loggedIn reflects the current status. Keep the fix localized
around the switch on selected.kind and the selected.harness/login check.

In `@internal/tui/session_controls.go`:
- Around line 300-319: The /onlybash off path in handleOnlyBashCommand can wipe
existing tool restrictions when onlybash was never enabled, because
disableOnlyBash restores from empty stash fields unconditionally. Update
disableOnlyBash (and any callers like handleOnlyBashCommand) to no-op unless
onlyBashActive is true, mirroring the guard already used by enableOnlyBash, so
pre-existing agentOptions.EnabledTools and DisabledTools are preserved. Add or
adjust coverage for the explicit off/status flow where onlybash starts inactive
and an operator-configured tool restriction already exists.

---

Nitpick comments:
In `@internal/provideroauth/claudecode.go`:
- Around line 144-151: The default HTTP timeout in
`RefreshClaudeCode`/`runAuthClaude` is too aggressive for this endpoint and can
cause false timeouts under load. Update the fallback client setup in
`claudecode.go` so the default `http.Client` timeout is increased to match the
longer auth/login behavior already used elsewhere, and ensure both the
`RefreshClaudeCode` path and any other default-client path in this file use the
same timeout policy when `opts.HTTPClient` is nil.
- Around line 71-79: Add a compliance check around ClaudeCodeLogin and the
refresh path used by RefreshClaudeCode/Claude Code token persistence so we are
not reusing Claude subscription OAuth tokens in zero without explicit
validation. Review the current token-minting flow in ClaudeCodeLogin and the
token-store usage it feeds, then either gate it behind a verified Anthropic ToS
allowance or replace it with a compliant auth approach; keep the fix localized
to the Claude Code login/token-refresh symbols so the behavior is easy to audit.

In `@internal/provideronboarding/advice.go`:
- Around line 184-188: `providerProfileHasCredential` is duplicating the auth
check instead of using the canonical profile method. Update this helper in
`internal/provideronboarding/advice.go` to delegate to
`profile.HasConfiguredCredential()` so onboarding advice stays in sync with the
config layer. Before changing it, verify the `HasConfiguredCredential()`
semantics in the `config.ProviderProfile` type match the current behavior
exactly and do not include any environment-resolution logic.

In `@internal/specialist/harness.go`:
- Around line 17-61: `Executor.runHarness` has no direct unit-test coverage
because it calls `runChildWithDecoder` directly, bypassing the injectable
`RunChild` seam. Add a small indirection in `runHarness` (or a similar
injectable helper on `Executor`) so tests can stub the child process and cover
the harness-not-installed error path, session/accounting wiring, and the
background-rejection behavior without requiring a real CLI binary.

In `@internal/tui/onboarding.go`:
- Around line 718-748: The CLI fast path in providerWizardMethodCLI duplicates
the same model-stage setup sequence used in the generic advance path, which
risks the two flows drifting apart. Extract the repeated reset-and-bootstrap
steps into a shared helper on the onboarding model, such as m.enterModelStage(),
and have both the CLI branch and the default post-advance flow call it. Keep the
helper responsible for resetSetupModels, clearing modelErr, incrementing
modelGen, creating the setupModelDiscoveryCmd, and setting modelLoad
consistently.

In `@internal/tui/provider_wizard.go`:
- Around line 1779-1787: The CLI profile builder in providerWizardCLIProfile is
omitting the provider default BaseURL, unlike providerWizardProfile, so update
it to persist the descriptor’s default BaseURL when constructing
config.ProviderProfile. Keep the profile fields aligned with the non-CLI path by
using the provider value from providercatalog.Descriptor so provider
construction and reloads don’t rely on later default hydration.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1c19886c-8e97-4334-b9ba-46d159598bcc

📥 Commits

Reviewing files that changed from the base of the PR and between cdf9d83 and f85a973.

📒 Files selected for processing (57)
  • cascade/00-task.txt
  • cascade/10-findings.cf
  • internal/agent/loop_test.go
  • internal/agentcli/agentcli.go
  • internal/agentcli/agentcli_test.go
  • internal/agentcli/credentials.go
  • internal/agentcli/credentials_test.go
  • internal/cli/app.go
  • internal/cli/auth.go
  • internal/cli/auth_test.go
  • internal/cli/provider_detect.go
  • internal/cli/setup.go
  • internal/config/credentials.go
  • internal/config/credentials_test.go
  • internal/config/types.go
  • internal/config/types_test.go
  • internal/modelregistry/modes.go
  • internal/modelregistry/modes_test.go
  • internal/provideroauth/claudecode.go
  • internal/provideroauth/claudecode_test.go
  • internal/provideronboarding/advice.go
  • internal/provideronboarding/advice_test.go
  • internal/providers/agentclicreds.go
  • internal/providers/agentclicreds_test.go
  • internal/providers/anthropic/provider.go
  • internal/providers/anthropic/provider_test.go
  • internal/providers/factory.go
  • internal/specialist/exec.go
  • internal/specialist/format.go
  • internal/specialist/harness.go
  • internal/specialist/harness_child_test.go
  • internal/specialist/harness_decode.go
  • internal/specialist/harness_decode_test.go
  • internal/specialist/harness_manifest_test.go
  • internal/specialist/manifest.go
  • internal/swarm/definition.go
  • internal/swarm/harness_test.go
  • internal/swarm/launcher_specialist.go
  • internal/swarm/member.go
  • internal/swarm/team.go
  • internal/tools/onlybash_mode_test.go
  • internal/tui/command_center.go
  • internal/tui/commands.go
  • internal/tui/commands_test.go
  • internal/tui/model.go
  • internal/tui/model_switch_cli_test.go
  • internal/tui/onboarding.go
  • internal/tui/onboarding_cli_test.go
  • internal/tui/onboarding_test.go
  • internal/tui/onlybash_command_test.go
  • internal/tui/options.go
  • internal/tui/picker.go
  • internal/tui/provider_wizard.go
  • internal/tui/provider_wizard_cli_test.go
  • internal/tui/provider_wizard_oauth_test.go
  • internal/tui/provider_wizard_test.go
  • internal/tui/session_controls.go

Comment on lines +370 to +379
// readKeychain is the real Deps.Keychain implementation, wired up only on
// darwin (see resolveDeps): it shells out to the `security` CLI rather than
// linking a CGo/keychain-access library, keeping this package stdlib-only.
func readKeychain(service string) ([]byte, error) {
out, err := exec.Command("/usr/bin/security", "find-generic-password", "-s", service, "-w").Output()
if err != nil {
return nil, fmt.Errorf("agentcli: keychain lookup for %q: %w", service, err)
}
return bytes.TrimRight(out, "\n"), nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add a timeout to the keychain shell-out.

readKeychain is the real Deps.Keychain implementation, and per the package's own design (see agentclicreds.go's doc comment), credentials are re-read — including this keychain probe — on every outbound provider request, with no caching. exec.Command here has no context/timeout, so a hung or blocked security process (e.g. a stalled keychain-unlock prompt) will block the request goroutine indefinitely. golangci-lint's noctx hint is valid here, not a false positive, since this sits on a real request path.

🔧 Proposed fix
+import (
+	"context"
+)
+
 func readKeychain(service string) ([]byte, error) {
-	out, err := exec.Command("/usr/bin/security", "find-generic-password", "-s", service, "-w").Output()
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+	out, err := exec.CommandContext(ctx, "/usr/bin/security", "find-generic-password", "-s", service, "-w").Output()
 	if err != nil {
 		return nil, fmt.Errorf("agentcli: keychain lookup for %q: %w", service, err)
 	}
 	return bytes.TrimRight(out, "\n"), nil
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// readKeychain is the real Deps.Keychain implementation, wired up only on
// darwin (see resolveDeps): it shells out to the `security` CLI rather than
// linking a CGo/keychain-access library, keeping this package stdlib-only.
func readKeychain(service string) ([]byte, error) {
out, err := exec.Command("/usr/bin/security", "find-generic-password", "-s", service, "-w").Output()
if err != nil {
return nil, fmt.Errorf("agentcli: keychain lookup for %q: %w", service, err)
}
return bytes.TrimRight(out, "\n"), nil
}
// readKeychain is the real Deps.Keychain implementation, wired up only on
// darwin (see resolveDeps): it shells out to the `security` CLI rather than
// linking a CGo/keychain-access library, keeping this package stdlib-only.
func readKeychain(service string) ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
out, err := exec.CommandContext(ctx, "/usr/bin/security", "find-generic-password", "-s", service, "-w").Output()
if err != nil {
return nil, fmt.Errorf("agentcli: keychain lookup for %q: %w", service, err)
}
return bytes.TrimRight(out, "\n"), nil
}
🧰 Tools
🪛 golangci-lint (2.12.2)

[error] 374-374: os/exec.Command must not be called. use os/exec.CommandContext

(noctx)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/agentcli/agentcli.go` around lines 370 - 379, The readKeychain
helper currently shells out to /usr/bin/security without any context, so a
stalled keychain lookup can block outbound requests indefinitely. Update
readKeychain to use a context with timeout when invoking exec.Command, and make
sure the timeout is applied through the command path used in agentcli.go so the
security call can be canceled cleanly. Keep the existing error wrapping in
readKeychain, but ensure the timeout/cancellation behavior is wired into the
Deps.Keychain implementation path described by resolveDeps and agentclicreds.go.

Source: Linters/SAST tools

Comment on lines +155 to +196
func claudeRefreshingBearerResolver(harness agentcli.Harness, deps agentcli.Deps, store cliAuthTokenStore, refresh claudeRefreshFunc) providerio.TokenResolver {
return func(ctx context.Context, forceRefresh bool) (string, string, bool, error) {
now := time.Now()
var cached oauth.Token
if store != nil {
if token, ok, err := store.Load(cliAuthTokenStoreKey); err == nil && ok {
cached = token
}
}
if !forceRefresh && cached.AccessToken != "" && !cached.NeedsRefresh(now, claudeRefreshBuffer) {
return "Authorization", "Bearer " + cached.AccessToken, true, nil
}

creds, ok, err := agentcli.ExtractCredentials(harness, deps)
if err != nil {
return "", "", false, fmt.Errorf("%s credentials: %w", harness.DisplayName, err)
}
if !ok {
creds = agentcli.Credentials{}
}
extractedFresh := strings.TrimSpace(creds.AccessToken) != "" &&
!creds.ExpiresAt.IsZero() && creds.ExpiresAt.After(now.Add(claudeRefreshBuffer))
if !forceRefresh && extractedFresh {
return "Authorization", "Bearer " + creds.AccessToken, true, nil
}

refreshToken := newestClaudeRefreshToken(cached, creds)
if refreshToken == "" {
return "", "", false, cliLoginExpiredError(harness)
}
token, refreshErr := refresh(ctx, refreshToken)
if refreshErr != nil {
return "", "", false, fmt.Errorf("%w (auto-refresh failed: %v)", cliLoginExpiredError(harness), refreshErr)
}
if store != nil {
// A failed save only costs a redundant refresh next call; the token
// in hand is still good.
_ = store.Save(cliAuthTokenStoreKey, token)
}
return "Authorization", "Bearer " + token.AccessToken, true, nil
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Race on concurrent refreshes: no locking around the Claude refresh chain.

claudeRefreshingBearerResolver's closure is shared across concurrent StreamCompletion calls on the same provider (e.g. parallel tool calls). If two calls simultaneously see a stale cached/extracted token, both will call refresh(ctx, refreshToken) with the same refresh token. OAuth refresh tokens are commonly single-use/rotated server-side, so the losing call can fail with a spurious auth error instead of transparently reusing the winner's freshly-rotated token.

🔒 Proposed fix: serialize the resolver body
 func claudeRefreshingBearerResolver(harness agentcli.Harness, deps agentcli.Deps, store cliAuthTokenStore, refresh claudeRefreshFunc) providerio.TokenResolver {
+	var mu sync.Mutex
 	return func(ctx context.Context, forceRefresh bool) (string, string, bool, error) {
+		mu.Lock()
+		defer mu.Unlock()
 		now := time.Now()
 		var cached oauth.Token

(Also add "sync" to the import block.)

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func claudeRefreshingBearerResolver(harness agentcli.Harness, deps agentcli.Deps, store cliAuthTokenStore, refresh claudeRefreshFunc) providerio.TokenResolver {
return func(ctx context.Context, forceRefresh bool) (string, string, bool, error) {
now := time.Now()
var cached oauth.Token
if store != nil {
if token, ok, err := store.Load(cliAuthTokenStoreKey); err == nil && ok {
cached = token
}
}
if !forceRefresh && cached.AccessToken != "" && !cached.NeedsRefresh(now, claudeRefreshBuffer) {
return "Authorization", "Bearer " + cached.AccessToken, true, nil
}
creds, ok, err := agentcli.ExtractCredentials(harness, deps)
if err != nil {
return "", "", false, fmt.Errorf("%s credentials: %w", harness.DisplayName, err)
}
if !ok {
creds = agentcli.Credentials{}
}
extractedFresh := strings.TrimSpace(creds.AccessToken) != "" &&
!creds.ExpiresAt.IsZero() && creds.ExpiresAt.After(now.Add(claudeRefreshBuffer))
if !forceRefresh && extractedFresh {
return "Authorization", "Bearer " + creds.AccessToken, true, nil
}
refreshToken := newestClaudeRefreshToken(cached, creds)
if refreshToken == "" {
return "", "", false, cliLoginExpiredError(harness)
}
token, refreshErr := refresh(ctx, refreshToken)
if refreshErr != nil {
return "", "", false, fmt.Errorf("%w (auto-refresh failed: %v)", cliLoginExpiredError(harness), refreshErr)
}
if store != nil {
// A failed save only costs a redundant refresh next call; the token
// in hand is still good.
_ = store.Save(cliAuthTokenStoreKey, token)
}
return "Authorization", "Bearer " + token.AccessToken, true, nil
}
}
func claudeRefreshingBearerResolver(harness agentcli.Harness, deps agentcli.Deps, store cliAuthTokenStore, refresh claudeRefreshFunc) providerio.TokenResolver {
var mu sync.Mutex
return func(ctx context.Context, forceRefresh bool) (string, string, bool, error) {
mu.Lock()
defer mu.Unlock()
now := time.Now()
var cached oauth.Token
if store != nil {
if token, ok, err := store.Load(cliAuthTokenStoreKey); err == nil && ok {
cached = token
}
}
if !forceRefresh && cached.AccessToken != "" && !cached.NeedsRefresh(now, claudeRefreshBuffer) {
return "Authorization", "Bearer " + cached.AccessToken, true, nil
}
creds, ok, err := agentcli.ExtractCredentials(harness, deps)
if err != nil {
return "", "", false, fmt.Errorf("%s credentials: %w", harness.DisplayName, err)
}
if !ok {
creds = agentcli.Credentials{}
}
extractedFresh := strings.TrimSpace(creds.AccessToken) != "" &&
!creds.ExpiresAt.IsZero() && creds.ExpiresAt.After(now.Add(claudeRefreshBuffer))
if !forceRefresh && extractedFresh {
return "Authorization", "Bearer " + creds.AccessToken, true, nil
}
refreshToken := newestClaudeRefreshToken(cached, creds)
if refreshToken == "" {
return "", "", false, cliLoginExpiredError(harness)
}
token, refreshErr := refresh(ctx, refreshToken)
if refreshErr != nil {
return "", "", false, fmt.Errorf("%w (auto-refresh failed: %v)", cliLoginExpiredError(harness), refreshErr)
}
if store != nil {
// A failed save only costs a redundant refresh next call; the token
// in hand is still good.
_ = store.Save(cliAuthTokenStoreKey, token)
}
return "Authorization", "Bearer " + token.AccessToken, true, nil
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/providers/agentclicreds.go` around lines 155 - 196,
`claudeRefreshingBearerResolver` can race when multiple concurrent
`StreamCompletion` calls invoke the same shared resolver, causing duplicate
`refresh(ctx, refreshToken)` attempts with the same token. Serialize the
resolver’s refresh path by adding a `sync`-based lock inside
`claudeRefreshingBearerResolver` so only one goroutine can evaluate
cache/extract/refresh/save at a time, and keep the existing fast-path token
reuse in place. Update the import block to include `sync`, and use the resolver
closure plus `store`, `refresh`, and `newestClaudeRefreshToken` as the key
points for the locking change.

Comment on lines +37 to +40
// zeroProviderEnvVar mirrors config.ActiveProviderEnv ("ZERO_PROVIDER") without
// importing internal/config: a per-child provider override just needs the env
// var name a self-exec zero child reads at startup to resolve its profile.
const zeroProviderEnvVar = "ZERO_PROVIDER"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the actual value of config.ActiveProviderEnv matches "ZERO_PROVIDER"
rg -n 'ActiveProviderEnv' internal/config --type=go -A2 -B2

Repository: Gitlawb/zero

Length of output: 4457


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant source and nearby tests/usages.
sed -n '1,140p' internal/specialist/exec.go
printf '\n---\n'
rg -n 'zeroProviderEnvVar|SetActiveProviderEnv|ActiveProviderEnv' internal --type=go -A3 -B3

Repository: Gitlawb/zero

Length of output: 24117


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether there's already a test guarding the cross-package env var name.
rg -n 'zeroProviderEnvVar|ActiveProviderEnv|ZERO_PROVIDER' internal --type=go -A3 -B3

Repository: Gitlawb/zero

Length of output: 29869


Add a test that keeps the specialist env var in sync with config.ActiveProviderEnv.
This literal is a separate source of truth; if the config constant is renamed later, provider pinning will break silently. A tiny test in internal/specialist comparing zeroProviderEnvVar to config.ActiveProviderEnv would catch that immediately.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/specialist/exec.go` around lines 37 - 40, Add a small sync test in
internal/specialist that compares zeroProviderEnvVar in exec.go against
config.ActiveProviderEnv. The issue is that zeroProviderEnvVar is a duplicated
literal source of truth, so a future rename of config.ActiveProviderEnv could
silently break provider pinning; fix it by adding a test that imports both
symbols and asserts they stay equal.

Comment on lines +97 to +152
// codexJSONDecoder decodes the NDJSON stdout produced by `codex exec --json`:
// each line wraps a "msg" whose "type" distinguishes an assistant text chunk
// ("agent_message"), the run's completion marker ("task_complete"), and a
// best-effort token usage report ("token_count"). Codex's final answer is
// "the last agent_message seen", not a dedicated field, so the decoder must
// remember it across lines.
type codexJSONDecoder struct {
lastMessage string
done bool
}

type codexLine struct {
Msg struct {
Type string `json:"type"`
Message string `json:"message"`
InputTokens *int `json:"input_tokens"`
OutputTokens *int `json:"output_tokens"`
TotalTokens *int `json:"total_tokens"`
} `json:"msg"`
}

func (d *codexJSONDecoder) decodeLine(line string) []streamjson.Event {
var parsed codexLine
if err := json.Unmarshal([]byte(line), &parsed); err != nil {
return nil
}
switch parsed.Msg.Type {
case "agent_message":
if parsed.Msg.Message == "" {
return nil
}
d.lastMessage = parsed.Msg.Message
return []streamjson.Event{{Type: streamjson.EventText, Delta: parsed.Msg.Message}}
case "task_complete":
d.done = true
return []streamjson.Event{{Type: streamjson.EventFinal, Text: d.lastMessage}}
case "token_count":
event := streamjson.Event{Type: streamjson.EventUsage}
event.PromptTokens = parsed.Msg.InputTokens
event.CompletionTokens = parsed.Msg.OutputTokens
event.TotalTokens = parsed.Msg.TotalTokens
return []streamjson.Event{event}
default:
return nil
}
}

func (d *codexJSONDecoder) finish(int) []streamjson.Event {
// task_complete never arrived (e.g. the CLI was killed mid-run) but there
// was at least one agent_message — still surface it as the final answer
// rather than losing it.
if d.done || d.lastMessage == "" {
return nil
}
return []streamjson.Event{{Type: streamjson.EventFinal, Text: d.lastMessage}}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

rg -n "codexJSONDecoder|codex exec --json|agent_message|task_complete|token_count|item.completed|turn.completed|streamjson.Event" -S .

Repository: Gitlawb/zero

Length of output: 21939


🏁 Script executed:

sed -n '1,220p' internal/specialist/harness_decode.go && printf '\n--- TESTS ---\n' && sed -n '1,240p' internal/specialist/harness_decode_test.go

Repository: Gitlawb/zero

Length of output: 11992


🌐 Web query:

Codex CLI "exec --json" schema msg agent_message task_complete token_count item.completed turn.completed usage

💡 Result:

The Codex CLI exec --json flag switches standard output to a newline-delimited JSON Lines (JSONL) stream, where each line represents a structured event during the agent's execution lifecycle [1][2][3]. Key event types and structures include: - item.completed: Signals that an item (such as an agent_message, file_change, or command_execution) has reached a terminal state [4]. - turn.completed: Emitted when a turn is finished. It contains the usage object, which details token consumption [4][5]. - usage: This field within turn.completed events includes: - input_tokens: Total input tokens used [4][5]. - cached_input_tokens: Number of cached input tokens used [5][4]. - output_tokens: Number of output tokens generated [4][5]. - agent_message: Typically emitted as part of an item.completed event when the agent provides a final or intermediate text response [6][5]. - task_complete: Often used synonymously or in conjunction with turn.completed to signal that a specific task or turn has concluded [7][8][4]. To enforce a specific structure on the agent's final response, you can use the --output-schema <FILE> flag, which ensures the output conforms to a provided JSON Schema [1][2][9]. For automation and CI/CD pipelines, piping the output to tools like jq is standard practice to extract specific fields or filter events [1][2][3]. Example usage: codex exec --json "analyze project structure" | jq -c 'select(.type=="turn.completed")' [9] When using the --output-schema flag: codex exec "Extract metadata" --output-schema./schema.json -o./result.json [1]

Citations:


Update codexJSONDecoder to the current codex exec --json schema
internal/specialist/harness_decode.go still expects legacy {"msg": ...} lines, but current Codex JSONL uses top-level events like item.completed and turn.completed (with usage on turn.completed.usage). As written, codex-json will drop current output and never surface the assistant’s final answer.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/specialist/harness_decode.go` around lines 97 - 152, Update
codexJSONDecoder to parse the current codex exec --json event schema instead of
the legacy nested msg format, since decodeLine currently only handles
agent_message/task_complete/token_count and will ignore modern top-level events.
Adjust codexLine and decodeLine to recognize item.completed and turn.completed
(including usage on turn.completed.usage), and make sure the final assistant
text is still preserved and emitted via lastMessage/finish. Keep the fix
localized to codexJSONDecoder, codexLine, and decodeLine so current Codex JSONL
output is surfaced correctly.

Comment on lines +124 to +138
detections := agentcli.Detect(agentcli.Deps{})
state := setupState{
visible: options.Visible,
required: options.Required,
configPath: strings.TrimSpace(options.ConfigPath),
providers: providers,
allProviders: providers,
apiKey: apiKey,
agentCLIDetections: detections,
}
// Don't open the method chooser with the cursor parked on an informational
// (unselectable) CLI row — mirrors providerWizardState's identical guard.
state.selectedMethod = firstSelectableMethodIndex(filterSetupMethodOptions(providerWizardMethodOptions(detections), providers))
return state
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm callers of newSetupState and inspect the keychain Deps default for a timeout.
rg -n "newSetupState\(" --type=go
rg -n -A10 "func resolveDeps" internal/agentcli/agentcli.go
rg -n -A15 "security.*find-generic-password" internal/agentcli

Repository: Gitlawb/zero

Length of output: 150


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## newSetupState call sites\n'
rg -n "newSetupState\(" --type=go .

printf '\n## agentcli files\n'
git ls-files | rg '^internal/agentcli/.*\.go$'

printf '\n## inspect agentcli implementation\n'
for f in $(git ls-files | rg '^internal/agentcli/.*\.go$'); do
  echo "### $f"
  ast-grep outline "$f" --view expanded || true
done

Repository: Gitlawb/zero

Length of output: 3793


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## locate setup state and setup flow\n'
rg -n "type setupState|func newSetupState|setupStages|setupReturnToMethod|providerWizardMethodCLI|providerWizardMethodOptions" internal/tui -g '*.go'

printf '\n## inspect onboarding file sections around newSetupState\n'
wc -l internal/tui/onboarding.go
sed -n '1,220p' internal/tui/onboarding.go

printf '\n## inspect agentcli detection code\n'
sed -n '1,260p' internal/agentcli/*.go

Repository: Gitlawb/zero

Length of output: 19279


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## exact detect-related references\n'
rg -n "Detect\(|type Deps|LookPath|find-generic-password|security" internal/agentcli internal/tui -g '*.go'

printf '\n## find startup entry points using onboarding\n'
rg -n "onboarding|setupState|newSetupState\(" . -g '*.go'

Repository: Gitlawb/zero

Length of output: 8093


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## inspect model initialization around newSetupState\n'
sed -n '720,790p' internal/tui/model.go

printf '\n## inspect CLI/app setup launch path and onboarding gating\n'
sed -n '1,220p' internal/cli/setup.go
printf '\n---\n'
sed -n '520,620p' internal/cli/app.go

Repository: Gitlawb/zero

Length of output: 15102


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## inspect agentcli Detect and resolveDeps\n'
sed -n '280,390p' internal/agentcli/agentcli.go

Repository: Gitlawb/zero

Length of output: 3125


Defer agentcli.Detect until setup is needed
newSetupState runs agentcli.Detect(agentcli.Deps{}) synchronously during interactive TUI startup, so the PATH/file/keychain probes happen before the UI renders even when setup isn’t shown. The macOS security call has no timeout, so a stalled keychain prompt can freeze the TUI.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui/onboarding.go` around lines 124 - 138, `newSetupState` eagerly
calls `agentcli.Detect(agentcli.Deps{})` during TUI startup, causing blocking
PATH/file/keychain probes before setup is actually needed. Move the detection
behind the setup flow so it only runs when the onboarding/setup UI is opened,
and keep `setupState` initialization in `newSetupState` limited to cheap fields
like `visible`, `required`, `configPath`, and `selectedMethod`. Use
`agentcli.Detect` and `setupState` as the main points to refactor so the initial
render stays responsive.

Comment on lines +112 to +113
m := newModel(context.Background(), Options{})
m.providerWizard = m.newProviderWizard()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Avoid real CLI detection before overriding test detections.

m.newProviderWizard() runs agentcli.Detect(agentcli.Deps{}) before these tests replace agentCLIDetections, so they can still touch the developer’s PATH/keychain. Build the wizard state with injected detections directly, or add a test-only deps hook.

Also applies to: 166-167

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui/provider_wizard_cli_test.go` around lines 112 - 113, The
provider wizard tests are constructing the wizard through m.newProviderWizard(),
which still triggers agentcli.Detect before the test overrides are applied.
Update the setup to build the wizard state with injected detections directly, or
add a test-only deps hook so newModel and providerWizard initialization do not
touch real CLI detection. Keep the fix centered around newModel,
m.newProviderWizard, and the agentCLIDetections override used in these tests.

Comment on lines +543 to +567
options := wizard.methodOptions()
wizard.selectedMethod = clampInt(wizard.selectedMethod, 0, maxInt(0, len(options)-1))
wizard.err = ""
if len(options) > 0 && options[wizard.selectedMethod].oauth {
if len(options) == 0 {
return
}
selected := options[wizard.selectedMethod]
wizard.oauthMode = false
wizard.cliHarness = nil
switch selected.kind {
case providerWizardMethodOAuth:
wizard.oauthMode = true
wizard.providers = providerWizardOAuthDescriptors()
} else {
wizard.oauthMode = false
wizard.selectedProvider = 0
wizard.refreshModels()
wizard.step = providerWizardStepProvider
case providerWizardMethodCLI:
if selected.disabled {
// Informational row only — nothing to advance into.
return
}
if !selected.loggedIn {
wizard.err = selected.harness.DisplayName + " is not logged in — run `" +
selected.harness.LoginCommand() + "` to log in, then try again."
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Refresh CLI login state before rejecting the harness.

methodOptions() uses login state captured when the wizard opened. After this error tells users to run codex login/claude login and try again, the same wizard still sees selected.loggedIn == false until it is reopened. Reprobe this harness or refresh agentCLIDetections before returning the logged-out error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui/provider_wizard.go` around lines 543 - 567, The CLI method
selection in providerWizard.go is using stale login state from methodOptions(),
so the wizard can keep rejecting a harness after the user logs in. Before
setting the logged-out error in the providerWizardMethodCLI branch, refresh the
underlying CLI detection state (or re-run the harness probe used by
agentCLIDetections) and rebuild the selected option so selected.loggedIn
reflects the current status. Keep the fix localized around the switch on
selected.kind and the selected.harness/login check.

Comment on lines +300 to +319
func (m model) handleOnlyBashCommand(args string) (model, string) {
args = strings.TrimSpace(strings.ToLower(args))
switch args {
case "", "toggle":
if m.onlyBashActive {
m = m.disableOnlyBash()
} else {
m = m.enableOnlyBash()
}
case "on":
m = m.enableOnlyBash()
case "off":
m = m.disableOnlyBash()
case "status":
// report only — fall through to onlyBashText() below
default:
return m, "Onlybash\nUsage: /onlybash [on|off|status]"
}
return m, m.onlyBashText()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Bug: /onlybash off clobbers pre-existing filters when never turned on.

enableOnlyBash guards stashing with if !m.onlyBashActive, but disableOnlyBash has no symmetric guard. Calling /onlybash off (or explicit "off") while onlyBashActive is already false unconditionally overwrites agentOptions.EnabledTools/DisabledTools with the stash fields — which are nil if onlybash was never enabled — silently wiping any operator-configured tool restrictions (e.g. a deliberately disabled "bash" tool). None of the provided tests cover this path (all "off" tests first call "on").

🐛 Proposed fix
 func (m model) disableOnlyBash() model {
+	if !m.onlyBashActive {
+		return m
+	}
 	m.agentOptions.EnabledTools = m.onlyBashStashedEnabledTools
 	m.agentOptions.DisabledTools = m.onlyBashStashedDisabledTools
 	m.onlyBashStashedEnabledTools = nil
 	m.onlyBashStashedDisabledTools = nil
 	m.onlyBashActive = false
 	return m
 }

Also applies to: 346-353

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/tui/session_controls.go` around lines 300 - 319, The /onlybash off
path in handleOnlyBashCommand can wipe existing tool restrictions when onlybash
was never enabled, because disableOnlyBash restores from empty stash fields
unconditionally. Update disableOnlyBash (and any callers like
handleOnlyBashCommand) to no-op unless onlyBashActive is true, mirroring the
guard already used by enableOnlyBash, so pre-existing agentOptions.EnabledTools
and DisabledTools are preserved. Add or adjust coverage for the explicit
off/status flow where onlybash starts inactive and an operator-configured tool
restriction already exists.

Brings in main's independent work since divergence: `zero providers models`
CLI discovery, config resolver/paths changes, gemini provider updates,
sandbox test fixes, and tools/edit_file + web_search changes, alongside
this branch's AuthCLI auto-detection and Claude Code OAuth work.

Resolved conflicts (keep-both) in:
- internal/cli/app.go: appDeps struct/wiring union of discoverProviderModels
  (main) and detectAgentCLIs (this branch)
- internal/specialist/manifest.go: import union of agentcli and config

Also wires AuthCLI-detected credentials (e.g. Claude Code) into main's new
discoverProviderModels so `zero providers models` authenticates AuthCLI
profiles the same way a real request would: a stored API key is used
verbatim, an OAuth access token becomes an Authorization: Bearer header,
and the anthropic catalog additionally gets the oauth-2025-04-20 beta
header. Adds internal/cli/provider_models.go:applyAuthCLIDiscoveryCredential
plus unit tests covering anthropic/chatgpt x APIKey/AccessToken.
cascade/00-task.txt and cascade/10-findings.cf were tracked by
0ffe141 before /cascade and /.claude were added to .gitignore in
3d5c52d. Remove them from tracking so the ignore rule actually
applies; files remain on disk.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
internal/cli/provider_models.go (2)

110-124: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider wiring AuthCLI credential extraction through appDeps for testability/consistency.

discoveryCredentialProfile calls agentcli.Lookup/agentcli.ExtractCredentials as bare package functions rather than through an injectable dependency, unlike provider_detect.go's deps.detectAgentCLIs (defaulting to agentcli.Detect). As a result, provider_models_test.go can only unit-test the pure applyAuthCLIDiscoveryCredential helper — there's no coverage for the actual AuthCLI dispatch branch (harness lookup + credential extraction feeding into the discovery profile), and this path can't be mocked without touching real files/keychain.

♻️ Illustrative sketch (adjust to match appDeps' actual shape)
-func discoveryCredentialProfile(profile config.ProviderProfile) config.ProviderProfile {
+func discoveryCredentialProfile(profile config.ProviderProfile, extractCreds func(agentcli.Harness, agentcli.Deps) (agentcli.Credentials, bool, error)) config.ProviderProfile {
 	authed := profile
 	if strings.TrimSpace(authed.AuthCLI) != "" {
 		if harness, ok := agentcli.Lookup(authed.AuthCLI); ok && harness.CatalogID != "" {
-			if creds, ok, err := agentcli.ExtractCredentials(harness, agentcli.Deps{}); err == nil && ok {
+			if creds, ok, err := extractCreds(harness, agentcli.Deps{}); err == nil && ok {
 				authed = applyAuthCLIDiscoveryCredential(authed, harness.CatalogID, creds)
 			}
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/provider_models.go` around lines 110 - 124,
`discoveryCredentialProfile` currently hardcodes `agentcli.Lookup` and
`agentcli.ExtractCredentials`, which makes the AuthCLI discovery branch
impossible to mock in tests. Route this logic through `appDeps` (following the
pattern used by `provider_detect.go` and `deps.detectAgentCLIs`) so the harness
lookup and credential extraction can be injected, then update the
`discoveryCredentialProfile` call sites and tests in `provider_models_test.go`
to cover the full AuthCLI path, not just `applyAuthCLIDiscoveryCredential`.

143-163: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Hoist the Anthropic OAuth beta string into a shared constant. The same "oauth-2025-04-20" literal is duplicated in the CLI discovery path and the Claude CLI auth provider; centralizing it in a shared package will keep those call sites from drifting.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/provider_models.go` around lines 143 - 163, The Anthropic OAuth
beta value is hardcoded in applyAuthCLIDiscoveryCredential and duplicated
elsewhere, so hoist the "oauth-2025-04-20" string into a shared constant in a
common package and update applyAuthCLIDiscoveryCredential to use that constant
when setting the anthropic-beta header.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@internal/cli/provider_models.go`:
- Around line 110-124: `discoveryCredentialProfile` currently hardcodes
`agentcli.Lookup` and `agentcli.ExtractCredentials`, which makes the AuthCLI
discovery branch impossible to mock in tests. Route this logic through `appDeps`
(following the pattern used by `provider_detect.go` and `deps.detectAgentCLIs`)
so the harness lookup and credential extraction can be injected, then update the
`discoveryCredentialProfile` call sites and tests in `provider_models_test.go`
to cover the full AuthCLI path, not just `applyAuthCLIDiscoveryCredential`.
- Around line 143-163: The Anthropic OAuth beta value is hardcoded in
applyAuthCLIDiscoveryCredential and duplicated elsewhere, so hoist the
"oauth-2025-04-20" string into a shared constant in a common package and update
applyAuthCLIDiscoveryCredential to use that constant when setting the
anthropic-beta header.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5eb3c0ab-f4d3-4739-a917-610a270fd32f

📥 Commits

Reviewing files that changed from the base of the PR and between f85a973 and 149c0ce.

📒 Files selected for processing (11)
  • .gitignore
  • internal/agent/loop_test.go
  • internal/cli/app.go
  • internal/cli/provider_detect_test.go
  • internal/cli/provider_models.go
  • internal/cli/provider_models_test.go
  • internal/cli/setup_test.go
  • internal/specialist/harness_dispatch_test.go
  • internal/specialist/manifest.go
  • internal/tui/model.go
  • internal/tui/picker.go
✅ Files skipped from review due to trivial changes (1)
  • .gitignore
🚧 Files skipped from review as they are similar to previous changes (3)
  • internal/tui/picker.go
  • internal/cli/app.go
  • internal/specialist/manifest.go

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants