Single provider seam cleanup

Task 7487 · Done · Coder · created · updated

Child C8 of epic 7425 (ACP runtime: every provider lane on ACP). Planned by Task 7428. ## Requirement (verbatim) Epic goal: "... and the provider seam has one home. ... While a legacy path still exists, it keeps working." Outcome 6 (A12–A14): "One provider seam: one `adapter.prepare` per launch, `_login_vendor` fails closed, the console tab's trust seeding goes through the adapter, and `probe_account` reads `_ACCOUNT_LAYER_READERS`." Current behaviour (PRD §3): A12 — the legacy branch of `launch_runner` calls `adapter.prepare` **twice**: `fleet_dispatch.py:1291` (a raise aborts) and `:1316` (inside a try, beside `register_mcp`; a raise is logged and the launch continues). `accounts.py::_login_vendor` (`:1403-1417`) falls back to `_CLAUDE_LOGIN` (`:1416`). A13 — `clauded_wt.py:527-529` runs `ctl vis-trust` for **every** provider, which seeds Claude's `.claude.json` via `fleet_launch.vis_trust`. A14 — `collector.py:729` tests `agent_providers.account_layer(provider) != "omc"` inline. The table `provider_health._ACCOUNT_LAYER_READERS` is at `:948-950`. Decided (PRD R9.2): abort on a `prepare` raise, because a refused trust write parks the runner on the trust dialog, and the first call already aborts today. ## Approach (architecture §8, verbatim) - **R9:** in `fleet_dispatch.launch_runner`, the legacy path calls `adapter.prepare` at :1289-1291 (aborting) and again at :1312-1317 (logged). Delete the second call. Keep `register_mcp` and `api_key_prepare` in their non-aborting try blocks. - **R9.2 on ACP:** `apps/jaahd/acp_runtime.py::run_prepare` (:76-84) swallows every exception today and starts the host anyway. Change it to **raise**, and drop the "never raising" docstring. Its caller (`fleet_dispatch.py:1294`) lets the raise take the same launch-error path the legacy first call takes: the attempt is reported failed, no pane spawns, and the slot follows the existing launch-failure handling. - **Tests:** a counting, raising stub adapter on both the legacy and the ACP path, asserting `prepare` is called once and that a raise produces no spawn and an attempt-end error. - **R10:** `accounts._login_vendor` (:1403-1417): replace `or _CLAUDE_LOGIN` with a raise, `ValueError(f"no login adapter for provider {provider!r}")`, in the module's existing error style. Audit the eight `_CLAUDE_LOGIN if params is None` sites (:546, :560, :695, :751, :888, :917, :1445, :1477). Keep the default for a Claude caller. A site reachable with a non-Claude provider resolves through `_login_vendor`, so it fails closed. - **R11:** replace `ctl vis-trust` in `clauded_wt.py:527-529` with a new `ctl prepare-trust <wt> <account_dir> <provider>`, which runs `agents.get_adapter(provider).prepare(worktree=wt, config_dir=adir, project=…)`. `ClaudeAdapter.prepare` already calls `fleet_launch.vis_trust` (`claude.py:165-171`), so Claude and DeepSeek write the same record, and Gemini and Codex write no `.claude.json`. Find where `clauded_wt` resolves the tab account's provider (the tab-settings code at :123-154 has it) and pass it. Keep `ctl vis-trust` only if another caller uses it; otherwise delete it. - **R12:** `collector.py:729` becomes `provider_health._ACCOUNT_LAYER_READERS.get(agent_providers.account_layer(provider)) is None` (or a small `has_account_reader(provider)` exported from `provider_health`, to avoid importing a private name — preferred). Behaviour is unchanged. Seam (architecture §9): this child owns the prepare policy in `fleet_dispatch.launch_runner`: exactly one `prepare` per launch on both paths, and a raise aborts via the launch-error path. C5 relies on the `:1299` auth blanking (leave it unchanged); C6 edits the Bedrock lines only. Credentials & reach: - Replay, unit, API and golden tests; `check_provider_branches.py`; `./dc check full` — Nothing beyond the worktree — verified - Dev control plane — `app.dev.jaah.ai` — verified; Dev console login (operator) — `jaah/debug/main/operator-login` — verified - Claude console tab, no trust prompt (R11.2 dev) — dev Claude accounts (several stored Anthropic API-key accounts, e.g. `t7281-claude-key`, plus Claude Code rows) — verified: present on dev Accounts at probe time Out of scope: "Other inline `account_layer(...) == \"omc\"` sites (`reconcile.py:2122`, `vendor_turn_errors.py:138`, `microvm_session.py:723,877`, `fleet_dispatch.py:1222,1243`). Only `collector.probe_account` is in scope. The rest are a deferred follow-up." Do NOT touch the Bedrock lines of `launch_runner` (C6) or the `build_auth_lines` blanking at :1299. Do NOT change `register_mcp`/`api_key_prepare` failure behaviour. Do NOT touch the chat ↔ terminal hand-off (Task 7370: `Console.toggle`, `_CHAT_AND_TERMINAL`, `app/routes/console_runtime.py`, `ChatView.tsx`), registry and SDK v2 agents (Task 7429), billing rules. "No secret in any persisted surface". "No new provider-name branches (`tools/provider_branches.toml` allowlist growth)." Done means: - R9.1 Each launch calls `adapter.prepare` exactly once, legacy and ACP alike. The legacy branch at `fleet_dispatch.py:1291`/`:1316` collapses to one call. — verify: a test with a counting stub adapter observes `prepare` called once per `launch_runner` for a legacy and an ACP launch. - R9.2 One failure policy: a raise from `prepare` aborts the launch before the pane spawns and reports the attempt failed. It never leaves a leased slot without a runner and an unreported attempt. `register_mcp` and the API-key settings step keep their current non-aborting behaviour. — verify: a test whose stub `prepare` raises observes no spawn and an attempt-end error, on both the legacy and the ACP (`run_prepare`) path. - R9.3 Claude and DeepSeek launches write the same `~/.claude.json` content as before. — verify: the existing Claude and DeepSeek launch tests and goldens pass unmodified. - R10.1 `accounts._login_vendor` for a provider with no login row raises an actionable error naming the provider and never returns `_CLAUDE_LOGIN`. — verify: a unit test with an unknown provider observes the raise. The Claude login test still resolves Claude's row. - R10.2 The other `_CLAUDE_LOGIN if params is None` defaults in `accounts.py` behave the same for Claude. Any that resolve a non-Claude provider to Claude's login also fail closed. — verify: existing accounts and login tests pass, plus one test for a non-Claude provider. - R11.1 A console tab (`clauded_wt.py`) seeds workspace trust by calling the account provider's adapter `prepare` (or the adapter-owned trust step), not by an unconditional `ctl vis-trust` that writes Claude's `.claude.json`. — verify: a test opens a console tab for a Gemini or Codex account and observes no `.claude.json` written. For a Claude account, the same trust record is written as today. - R11.2 A Claude or DeepSeek console tab opens without a trust dialog, as today. — verify: the existing clauded_wt and ctl tests pass. On dev (`app.dev.jaah.ai`), open a console tab on a Claude account: no trust prompt. - R12.1 `collector.probe_account` decides the account-layer branch by looking up `provider_health._ACCOUNT_LAYER_READERS`, not an inline `!= "omc"`. — verify: `grep -n 'account_layer(provider) != "omc"' apps/jaahd/collector.py` is empty, and the collector tests pass. - R12.2 Behaviour unchanged for Claude, DeepSeek, Codex and Gemini probes. — verify: existing `test_collector*` tests pass unmodified. - T.5 `python3 tools/check_provider_branches.py` passes, and no provider-name branch is added to its allowlist. — verify: run it and observe "OK"; `git diff tools/provider_branches.toml` adds no `[[allow]]`. Required checks: ./dc check full

Outcome

Single provider seam (R9–R12) merged in PR #6061: one prepare per launch, abort on raise (legacy and ACP), _login_vendor fails closed, console tab trusts via ctl prepare-trust, collector uses has_account_reader. Quick gate and reviews passed. Dev tab check not run (deploy superseded). Draft #7519 filed.

Pull request

Artifacts

Activity

  • Draft
  • Ready
  • Runner started
  • Runner ended (done)