Skip to content

setup-wizard: LLM-provider/adapter selection doesn't install required software or provision keys — claude_code agents skip with 'claude CLI not found' on a stock install (generalises to Ollama model pull, API keys) #12681

Description

@mrveiss

Summary

Selecting an agent LLM provider / execution adapter (e.g. claude_code) in the Setup Wizard does not install the software that adapter requires or provision its credentials. The choice is recorded, but the runtime dependency is left unmet — so an agent configured with that provider silently fails to run. This is a setup-wizard / provider-selection gap, upstream of the adapter's own error handling (#12478).

How it was found (concrete, live)

Exercising the LLC "run a hired agent" path (ABR marketing-agent dogfooding) on a stock deployment:

  1. Company activated, mission goal assigned, CEO agent hired with adapter claude_code / model claude-sonnet-4-6 — all succeed.
  2. Triggering the agent heartbeat returns a run_id (202) but the run is skipped:
    Heartbeat skipped: claude CLI not found on PATH or configured location;
      install it or set AUTOBOT_CLAUDE_CLI_PATH
    
  3. Root cause: the claude CLI the adapter needs was never installed anywhere the service account (autobot) can execute. On this box it existed only in a human user's home (~/.local/bin, mode 0750 — unreadable by autobot).
  4. Manual remediation that should have been the wizard's job: copying the claude binary to /usr/local/bin (on the service PATH) immediately fixed resolution — the next heartbeat ran with zero "not found" skips and reached completed.

So the only thing between "adapter selected" and "agent runs" was a piece of provisioning the wizard never performed.

The gap generalises beyond claude

This is not a claude-specific bug — it is a missing responsibility in the provider-selection step. Every provider choice has an unmet-dependency failure mode the wizard should close:

provider / adapter required software required credential
claude_code claude CLI, on the service PATH (not a user home) Anthropic auth / API key for the CLI
Anthropic API — ANTHROPIC_API_KEY
AWS Bedrock — AWS creds / role
Ollama (local) ollama daemon + the selected model pulled —
OpenAI-compatible — base URL + API key

Today the wizard lets you pick any of these and reports success, while the actual precondition (binary installed for the service user, model pulled, key present) is silently absent. The failure only surfaces later, at first agent run, as an opaque skip.

Why this matters

Recommendation

Make provider selection own its preconditions. In the wizard's LLM-provider step, for the chosen provider(s):

  1. Preflight + install the required software to a service-reachable location (e.g. claude → /usr/local/bin, on the service PATH; ollama pull <model> for local models), or clearly report what the operator must install and verify it before completing the step.
  2. Provision credentials in the step: capture the API key / auth and write it where the service reads it (service env / secrets store), rather than assuming it exists.
  3. Verify as the service account, not the installing user — e.g. sudo -u autobot claude --version, sudo -u autobot ollama list — so a binary in a human home (0750) is correctly detected as not usable.
  4. Surface the result in the UI — provider "configured" should mean "software present + reachable by the service + credential valid," tested, not just "selection saved."
  5. Have install.sh provisioning cover the same ground for headless installs, so wizard and installer agree.

This closes the gap that #12478 could only paper over: #12478 gave the adapter a good error message and an AUTOBOT_CLAUDE_CLI_PATH escape hatch, but nothing installs claude or points that variable at a service-reachable copy. That is the wizard/provisioning step's job.

Secondary observation (separate, lower confidence)

Once the CLI resolved, the run reached final_status: completed but with output_text: None, and llc.services.replay_service.record_run logged an error ("record_run failed … skipping") for each run. Worth a look on its own: an agent run completing without captured output, and run-recording erroring, may be a distinct defect from this provisioning gap. Not filed pending confirmation.

Environment

install.sh deploy at /opt/autobot; agents execute as the autobot systemd service (PATH …/venv/bin:/usr/local/bin:/usr/bin:/bin). Adapter claude_code, claude CLI 2.1.219. Reproduced live; manual /usr/local/bin install confirmed the fix. Related: #12478 (adapter message/escape-hatch), #12210 (trigger), #12309 (transitions).

Activity

  1. mrveiss commented on Aug 22, 2026

    @mrveiss
    OwnerAuthor

    Assessment — the title is a misnomer, and half of this is already done

    Scoped before implementing. Correcting the framing so the remaining work is
    visible.

    No wizard selects an LLM provider

    • SLM wizard steps (api/setup_wizard.py:44-54): welcome → add_nodes →
      test_connections → enroll_agents → assign_roles → configure_secrets →
      provision_fleet → verify_health. configure_secrets collects exactly one
      value: hf_token.
    • OnboardingWizard.vue: doctor → preset → apply. It recommends an
      llm_tier; it installs nothing.
    • The adapter is chosen at hire time — HireAgentModal.vue →
      AdapterTypeSelect.vue → GET /api/llc/adapters.

    So this is not a gap in provider selection; it is a gap between hiring an agent
    and that agent being able to run.

    Already solved — recommendations 3 and 4

    llc/api/adapters.py:21-50 computes available by calling
    adapter.is_cli_available() inside the backend process — i.e. as the
    autobot service account, which is exactly the check that was missing — via
    resolve_cli_binary (configured override → PATH → ~/.local/bin,
    /usr/local/bin, npm global bin). AdapterTypeSelect.vue disables unavailable
    adapters and labels them "(CLI not installed)".

    Ollama also already has both a runtime pull API
    (api/adapters.py:108-137, streaming) and an Ansible pull loop
    (roles/llm/tasks/main.yml:162-171).

    What is actually left

    Independent of secrets entirely:

    1. Nothing installs the claude CLI onto the service PATH. The only
      @anthropic-ai/claude-code install in the tree is in
      .github/workflows/ai-security-review.yml — CI only. This is the literal
      thing the reporter had to do by hand.
    2. Hire accepts an unavailable adapter. llc/api/agent_hires.py:365-372
      validates only that the type is registered, never that it is available.
      The UI greys it out; the API does not — this is exactly how a hired agent
      ends up unable to run.
    3. The skip reason is one field from visible. It is persisted
      (heartbeat_scheduler.py:522-547 writes status=SKIPPED, error=exc.reason)
      and returned (llc/api/runs.py:47), but HeartbeatMonitor.vue never renders
      it. Cheapest fix here by a wide margin.
    4. Ollama's pull list is disconnected from any UI selection — it comes from
      role defaults.

    Depends on secrets, but on much less than #12684:

    1. No provider credential ever reaches an adapter subprocess.
      subprocess_support.py:265-278 injects only AUTOBOT_LLC_API_KEY and
      AUTOBOT_LLC_API_BASE. A key entered in any of the six secrets UIs never
      reaches an agent run — it works only if already in the backend service's own
      environment.

      This does not need store consolidation: provider_key_vault.resolve_provider_key
      already does env-first/vault-fallback and is already used by the in-process
      LLM path. The fix is calling that same resolver at the adapter env-injection
      seam. It does need AUTOBOT_SECRETS_ROOT_KEY is generated by nothing — the canonical secret store is dark on every provisioned deployment #14758 (the root key), or the vault half is
      permanently empty.

    Sequencing that matters

    Do not land item 2 before item 1. Today you can hire and hand-install the
    CLI. Rejecting the hire first makes deployments strictly worse — you cannot
    hire at all until someone installs it. Land the install, or ship the rejection
    with an explicit override.

    Decide the claude auth model before item 5. The claude CLI may
    authenticate by subscription login rather than API key; injecting
    ANTHROPIC_API_KEY changes billing and behaviour. That is a decision, not an
    implementation detail.

    Item 1 carries supply-chain surface — pulling an npm global package onto
    nodes at provision time. Pin the version, gate on the adapter being wanted, and
    report rather than fail when offline.

    Also worth splitting out

    The "secondary observation" in this issue (output_text: None on a completed
    run, replay_service.record_run erroring per run) is a distinct evidence-loss
    defect. Part of it is already fixed — see #13614 / PR #14755, where the
    transcript path was resolved from a placeholder run id that never matched.

  2. mrveiss commented on Aug 22, 2026

    @mrveiss
    OwnerAuthor

    Item 3 delivered — #14761 merged. Issue stays open.

    Verified on origin/Dev_new_gui, not from the PR branch:

    Check Result
    AgentRun.error declared present
    Reason row rendered (.run-reason) present
    .status-skipped styling present
    HeartbeatMonitor.skipReason.test.ts present
    Locales carrying both new keys 11/11

    frontend-tests passed on the merged head — the component tests are verified by
    CI, since I could not run the frontend suite locally.

    What was actually wrong

    Nothing was missing from the pipeline. HeartbeatDispatchSkipped is already
    handled deliberately — the scheduler records SKIPPED and stores exc.reason —
    and _run_to_dict already returned it. The reason reached the client and was then
    dropped one line short of the screen, because the component's AgentRun interface
    never declared the field.

    So every skip looked identical: a missing adapter binary, an active cooldown, and
    an agent with no work were indistinguishable without querying the database. That is
    the "fails invisibly" symptom this issue reports.

    SKIPPED also had no status colour defined at all, while queued, running,
    failed, timed_out and unknown all did — the one status whose entire meaning is
    "this did not run" rendered as unstyled text.

    A review pass caught one more thing worth recording: in Persian I had used رد شد,
    which is the phrasing this codebase already uses for denied
    (common.permissionDenied.title = "دسترسی رد شد"). A Persian operator would have
    read a skipped run as a rejected one — the exact confusion this change exists to
    remove, defeating its own purpose in one locale. Corrected before merge.

    Still open — items 1 and 2

    1. Nothing installs the claude CLI onto the service PATH.
    2. llc/api/agent_hires.py:365-372 accepts an unavailable adapter — it validates
      only that the type is registered, never that it is available, which is how a
      hired agent ends up unable to run.

    The sequencing recorded earlier still holds: do not land item 2 before item 1.
    Today an operator can hire and hand-install the CLI afterwards. Rejecting the hire
    first, while nothing installs the binary, makes provisioning strictly worse.

    Item 3 was independent of that ordering, which is why it went first.

    Related and now in flight: #14784 provisions AUTOBOT_SECRETS_ROOT_KEY, without
    which the canonical secret store is unreachable — the same silent-degradation
    pattern, and a prerequisite for provider keys having one place to live.

  3. github-actions commented on Aug 22, 2026

    @github-actions
    Contributor

    PR #14761 (merged to Dev_new_gui) references this issue with a close keyword.

    fix(llc): surface the persisted skip reason in the heartbeat monitor (#12681)

    If this issue is fully resolved, close it manually. If work remains, no action is needed.

  4. mrveiss commented on Aug 22, 2026

    @mrveiss
    OwnerAuthor

    All three items delivered

    item PR what it was
    1 — nothing installs the agent CLIs #14790 ansible installed neither claude nor gh; searching the whole ansible tree for "claude" returned zero files
    2 — hire accepts an unavailable adapter #14797 the hire path never consulted the availability probe that already existed
    3 — the skip reason is never rendered #14761 the reason was persisted and served, and died one line short of the screen

    Verified on origin/Dev_new_gui, not from the PR branches:

    • ansible installs claude-code and proves it resolves on the host
    • _adapter_unavailable_reason is wired into hire_agent, including the
      unimplemented-adapter branch
    • the heartbeat monitor renders the reason, with keys in all 11 locales

    The sequencing mattered and was kept

    Item 3 first, because it makes the failure legible while it still exists. Then item
    1, because it installs the binaries. Then item 2 — rejecting a hire while nothing
    installed the CLI would have made provisioning strictly worse, since an operator
    could previously hire and hand-install afterwards.

    What each turned out to be

    None of the three was a missing feature. Every one was something already built that
    did not reach the next hop:

    • the CLI install existed in the container image and had no ansible equivalent
    • the availability probe existed and powered GET /adapters, and the hire path
      never asked it
    • the skip reason existed in the database and in the API response, and the
      component's interface never declared the field

    That is the same defect shape three times, which is why the issue read as
    "provisioning fails invisibly" rather than as three separate bugs.

    Not closed here

    #14800 — PortabilityService._import_agent creates agent_org_nodes rows with
    an adapter_type from an imported template, with neither the registration nor the
    availability check. So the item-2 gate currently covers one of the two paths
    that create runnable agents. Pre-existing, and worth saying plainly rather than
    letting the gate look complete.

    Closing this issue; #14800 tracks the remaining path.

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

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions