Files

1 line
57 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "agent-squid/squid", "files": {"docs/decisions/0028-harness-provider-separation.md": {"d437b95243a42eae0f59e8417d9ed439d69dd159": {"t": 1785746503.321385, "n": 14347, "h": "a7cfb7add5727ef1", "c": "---\nstatus: accepted\ndate: 2026-07-12\nupdated: 2026-07-12\n---\n# ADR-0028: Harness/Provider Separation\n\n> Supersedes [ADR-0027](0027-backend-naming-convention.md)'s\n> `{harness}-{provider}` composite backend-ID convention — see Consequences.\n\n## Context\n\nADR-0024 defined a **driver** as the coded CLI integration (`claude`, `codex`,\n`cursor`, `opencode`, `pi`) and a **backend** as a YAML-configured instance of\none driver plus connection details (`provider`, `api_key`, `base_url`, `gauge`,\n...). ADR-0027 layered a naming convention on top — `{harness}-{provider}` —\nusing \"harness\" as the human name for what the code still calls `driver`, and\nestablished that a backend is really a harness × provider pair.\n\nThree problems remain past naming:\n\n1. **\"Driver\" undersells what the field is.** It's not just wiring code, it's\n the coding-agent product itself (Claude Code, Codex, Cursor, OpenCode, Pi).\n ADR-0027 already talks in terms of harness/provider; the code and config\n should say the same word.\n2. **Provider is a string, not a config surface.** `provider`, `base_url`,\n `gauge`, and the implicit \"does this need an api_key or a browser login\"\n question are currently set per-backend, duplicated across every backend\n that happens to hit the same endpoint. There's no place to say once \"NIM's\n quota adapter is X and its model list is Y\" and have every `*-nvidia` backend\n pick it up.\n3. **Backend is a redundant layer once (1) and (2) are fixed.** A named agent\n already stores `(backend, model, cwd)`. Once a backend is nothing but\n \"which harness, which provider,\" using a Codex+NIM combination the first\n time requires adding a `backends:` entry that exists only to name a\n permutation the agent record already has to state. Not every harness ×\n provider pairing works, either — some combinations are untested or simply\n incompatible (e.g. a subscription-only harness can't take an arbitrary\n `base_url`) — so a fixed named-backend list was never a validation\n mechanism anyway, just an extra step.\n\nThis ADR promotes provider to a first-class config section, finishes the\ndriver → harness rename ADR-0027 started in spirit, and retires `backend` as\na named entity in favor of agents selecting harness + provider directly.\n\n## Decision\n\n### Harness\n\nThe primary runtime term is `harness`: `agent/harnesses.py` owns the supported\nharness set, protocol defaults, install paths, install commands, default\nproviders, and API compatibility metadata. Same five values, now spelled out\ninstead of abbreviated in code (`claudecode`, `codex`, `cursor`, `opencode`,\n`pi`); the `cc`/`cx`/`cr`/`oc`/`pi` short forms from ADR-0027 remain an\nID-prefix convention (agent IDs, gauge labels), not the internal field value.\nLegacy `driver` names still exist in compatibility code while\n`agent/backends.py` and older API fields are being retired.\n\nThe Claude Code harness is renamed from `claude` to `claudecode` — matching\nthe existing `opencode` pattern — because `claude` also names a provider\n(Anthropic's models). ADR-0027 already flagged this exact collision for\nbackend IDs (`pi-claude` = Pi harness + Claude provider); giving the harness\nits own non-overlapping identifier removes the ambiguity at the source\ninstead of relying on position-in-string to disambiguate it.\n\nProtocol is a harness property, not something a provider or agent chooses\nindependently — it's dictated by what the CLI itself supports. Only\n`claudecode` has a true persistent structured stdin/stdout protocol, so it's\nthe only harness offering `interactive-cli`; every other harness is\n`oneshot-cli` only, unchanged from `SUPPORTED_PROTOCOLS_BY_DRIVER` /\n`DEFAULT_PROTOCOL_BY_DRIVER` today. The interactive idle-timeout default\n(`DEFAULT_INTERACTIVE_IDLE_TIMEOUT_SECONDS`) is likewise a harness-level\nconstant rather than a per-agent override — today's per-backend\n`interactive.idle_timeout_seconds` override (exercised by exactly one test\nbackend) is dropped for simplicity. If per-agent tuning turns out to be\nneeded later, it can be added back as an agent field without touching this\nADR's harness/provider split.\n\nSupported harnesses and default provider. Install commands are unchanged and\nstay defined once in `agent/harnesses.py`, matching the README table — not\nduplicated here:\n\n| Harness | Value | ID prefix | Protocol | Default provider |\n|---|---|---|---|---|\n| Claude Code | `claudecode` | `cc` | oneshot-cli | Claude (anthropic) |\n| Codex | `codex` | `cx` | oneshot-cli | GPT (openai) |\n| Cursor | `cursor` | `cr` | oneshot-cli | Cursor |\n| OpenCode | `opencode` | `oc` | oneshot-cli | NVIDIA NIM (`nvidia`) |\n| Pi | `pi` | `pi` | oneshot-cli | NVIDIA NIM (`nvidia`) |\n\n\"Installed\" reuses the existing `shutil.which()` check in `config.py`\n(`CLAUDE_PATH`, `CODEX_PATH`, ...) — no new detection mechanism. This is\nqueryable through `/health`: its `harnesses` payload contains\n`{id, label, install_cmd, installed, protocol, interactive,\ndefault_provider, supported_apis, compatible_providers}` for the settings UI.\nThe startup `_check_deps()` warning is a thin wrapper around that same\nharness data. There is no separate `/config/harnesses` endpoint in the\ncurrent implementation.\n\n### Provider\n\n`providers:` becomes a top-level config section. A provider owns everything\nabout the API endpoint that doesn't depend on which harness is talking to\nit — including `color`, since color exists to show at a glance which quota\npool a message drew from, which is a provider fact, not a harness fact:\n\n```yaml\nproviders:\n anthropic:\n label: Claude\n color: \"#AE5332\"\n auth: {type: subscription}\n openai:\n label: GPT\n color: \"#7070A0\"\n auth: {type: subscription}\n cursor:\n label: Cursor\n color: \"#8F8D8D\"\n auth: {type: subscription}\n deepseek:\n label: DeepSeek\n color: \"#4D9DE0\"\n base_url: \"https://api.deepseek.com/anthropic\"\n auth: {type: api_key, api_key: {env: DEEPSEEK_API_KEY}}\n gauge: deepseek\n models: [deepseek-chat, deepseek-reasoner]\n nvidia:\n label: NVIDIA\n color: \"#76B900\"\n base_url: \"https://integrate.api.nvidia.com/v1\"\n auth: {type: api_key, api_key: \"nvapi-...\"}\n gauge: {type: static, text: \"NIM-managed\"}\n models: [nvidia/nemotron-4-340b, meta/llama-3.1-405b, ...]\n```\n\n`api_key` lives under `auth`, not as a sibling field — it's part of how the\nprovider authenticates, not a connection detail like `base_url`. When\n`auth.type` is `api_key`, `auth.api_key` is required — either a literal\nstring or `{env: NAME}` to read from an environment variable at execution\ntime, unchanged from ADR-0024. It's never returned in provider metadata sent\nto the UI. When `auth.type` is `subscription`, `auth.api_key` isn't read at\nall, even if present.\n\nA provider id can be declared more than once for the same underlying vendor\nto hold separate credentials — e.g. `deepseek` and `deepseek-team2`, each\nwith its own `auth.api_key` and `color`. This is also the new home for the old\nbackend-level `env`/`settings`/`args` escape hatches: they move to the\nprovider, since they describe how to reach an endpoint, not which harness is\nasking.\n\nSquid ships only zero-config providers in code defaults\n(`anthropic`, `openai`, `cursor`, `opencode`). Providers requiring user\ncredentials, such as `nvidia` and `deepseek`, are documented in\n`config/squid.yaml.example` rather than auto-seeded for existing installs.\n`nvidia` is the canonical provider id for NVIDIA NIM because both Pi and\nOpenCode pass models using a `provider/model` prefix, so the provider id must\nmatch the CLIs' native provider name.\n\n**Direct vs. aggregator providers.** No new field for this — it falls out of\n`models`. A direct provider (`deepseek`, `anthropic`, `openai`) serves its own\nmodel line and typically has a short or empty `models` list. An aggregator\nprovider (`nvidia`) fronts many vendors' models behind one endpoint and has a\nlong `models` list. Both are configured the same way.\n\n### Auth type\n\n`auth.type` is `api_key` or `subscription`:\n\n- `subscription` — the harness CLI handles its own browser login (Claude Code\n max/pro, Codex ChatGPT login, Cursor login). Squid never asks for or stores\n a credential; `base_url`/`auth.api_key` are meaningless and missing-secret\n checks never fire for these agents.\n- `api_key` — Squid resolves `auth.api_key` (literal or `{env: NAME}`,\n unchanged from ADR-0024) and injects it plus `base_url` into the harness's\n native env/settings, same translation `execution_env()`/`driver_settings()`\n do today.\n\n### Backend is retired — agents select harness + provider directly\n\nThere is no `backends:` config section. A named agent extends ADR-0024's\n`(backend, model, cwd)` to `(harness, provider, model, cwd)`:\n\n```\nname: cc-deepseek-work\nharness: claudecode\nprovider: deepseek\nmodel: deepseek-chat\ncwd: ~/Work/squid\n```\n\nThe agent's own name already serves as its display label — no separate\n`label` field is needed at the agent level. `label` on a provider (above) is\nstill needed for the two places a backend's label shows up in the UI today\n(`ui/app.js`): the provider-picker dropdown when creating/editing an agent,\nand the quota status row, where it's paired with `color` as one dot-plus-name\nunit identifying which quota pool a gauge belongs to.\n\nNot every harness × provider pair is expected to work, so the UI doesn't\noffer a raw cross-product. The current compatibility list is derived from\nAPI-shape overlap: each harness declares `supported_apis`, each provider\ndeclares `supported_apis`, and `/health.harnesses[].compatible_providers`\ncontains providers whose sets intersect, plus the harness default provider\nwhen present. The legacy `compatible_providers` harness config key is still\naccepted as an explicit additive override. Anything outside the list remains\nreachable through the existing freeform provider path in config/API usage;\nthe UI uses the list as guidance, not as server-side model validation.\nLive \"does this combination actually work\" validation (e.g. a test-connection\naction) is out of scope for this ADR and can be layered on later without\nchanging this shape.\n\n### Model catalog stays local, no hosted sync\n\n`models:` on a provider is a **suggestion list for the UI**, not a validated\nenum. The agent-creation model field renders it as a dropdown with a\nfreeform-text fallback (`<datalist>`-style): pick from the list, or type any\nmodel string the provider actually supports. Squid does not validate the\nmodel against the list server-side.\n\nA centrally-hosted, auto-updating provider/model catalog was considered and\nrejected for now: it adds an online dependency and a staleness/versioning\nproblem (new models ship faster than any catalog sync would) to solve a UX\nnicety that a plain freeform input already handles. Revisit only if\nhand-maintaining `models:` lists in `squid.yaml` becomes a recurring\ncomplaint — the local-list-plus-freeform design doesn't foreclose that,\nsince a hosted catalog could later populate the same field.\n\n## Consequences\n\n- ADR-0027 is superseded. Its `{harness}-{provider}` convention existed to\n disambiguate a single composite `backends:` YAML key; with `backend` gone\n and harness/provider now separate structured fields on the agent, there is\n no composite ID left to disambiguate. Its collision reasoning (`claude` as\n both harness and provider) is resolved more directly here, by renaming the\n harness to `claudecode` rather than relying on string position.\n- `harness` is the primary config/API field. Legacy `driver` naming still\n appears in compatibility code and old tests until `agent/backends.py` and\n legacy backend surfaces are fully removed. For current `squid.yaml`, this\n is a breaking config change: `driver:` → `harness:` wherever it was set.\n- Per the precedent set for the `deepcla` → `deepseek` cleanup, there is no\n automatic migration or aliasing. Users update `squid.yaml` by hand;\n `config/squid.yaml.example` ships the new shape.\n- `providers:` is a new top-level config section. Loading uses\n `_configured_providers()` plus a zero-config default providers table\n (`anthropic`, `openai`, `cursor`, `opencode`). API-key providers such as\n `nvidia` and `deepseek` live in `config/squid.yaml.example`.\n- `backends:` is removed from the primary config shape. New execution uses\n `agent/harnesses.py`, `agent/providers.py`, and `agent/resolve.py`; the\n resolved runtime is ephemeral and exposes the same execution surface callers\n used to get from `Backend` (`execution_env()`, `driver_settings()`, `args`,\n `protocol`, `interactive`, `fingerprint`, availability, gauge, color).\n `agent/backends.py` remains as compatibility code while older tests and API\n surfaces are retired.\n- SQLite stores structured `harness`/`provider` fields directly. It does not\n store `backend` or `last_backend`; API responses compute those compatibility\n refs as `harness` or `harness:provider` when older UI/client surfaces still\n need them. `AgentRequest` accepts `harness`/`provider` first and falls back\n through legacy `backend` parsing (`split_agent_ref`) when needed.\n- Runtime attribution follows the same rule: logs and durable stats should\n include `harness` and `provider`; `backend` may be emitted alongside them\n only as the legacy runtime reference.\n- Session-resume fingerprinting (ADR-0024's \"backend configuration\n fingerprint\") is computed from the resolved harness + provider + model +\n protocol at execution time instead of `Backend.fingerprint()`. A change to\n a provider's `base_url`/`auth.api_key`/`env` still starts a fresh session\n for every agent using it, same as today.\n- Harness metadata is exposed through `/health.harnesses`; provider metadata\n is exposed through `/health.providers`. `_check_deps()` includes Pi and uses\n the same harness install-check data instead of a separate table.\n- Pre-launch schema compaction removed migration/backfill code and legacy\n backend storage columns. Existing local SQLite files may still contain extra\n columns from earlier development builds, but current code no longer reads or\n writes them.\n- This ADR is implemented in the current code for config loading, runtime\n resolution, harness/provider metadata, and the settings UI. Remaining work\n is cleanup: retire `agent/backends.py` and remove legacy computed `backend`\n API fields once dependent UI/tests are updated.\n"}, "f00032159a19d0a39f81c6226c629ee8a81c7833": {"t": 1785765018.0646238, "n": 14347, "h": "a7cfb7add5727ef1", "c": "---\nstatus: accepted\ndate: 2026-07-12\nupdated: 2026-07-12\n---\n# ADR-0028: Harness/Provider Separation\n\n> Supersedes [ADR-0027](0027-backend-naming-convention.md)'s\n> `{harness}-{provider}` composite backend-ID convention — see Consequences.\n\n## Context\n\nADR-0024 defined a **driver** as the coded CLI integration (`claude`, `codex`,\n`cursor`, `opencode`, `pi`) and a **backend** as a YAML-configured instance of\none driver plus connection details (`provider`, `api_key`, `base_url`, `gauge`,\n...). ADR-0027 layered a naming convention on top — `{harness}-{provider}` —\nusing \"harness\" as the human name for what the code still calls `driver`, and\nestablished that a backend is really a harness × provider pair.\n\nThree problems remain past naming:\n\n1. **\"Driver\" undersells what the field is.** It's not just wiring code, it's\n the coding-agent product itself (Claude Code, Codex, Cursor, OpenCode, Pi).\n ADR-0027 already talks in terms of harness/provider; the code and config\n should say the same word.\n2. **Provider is a string, not a config surface.** `provider`, `base_url`,\n `gauge`, and the implicit \"does this need an api_key or a browser login\"\n question are currently set per-backend, duplicated across every backend\n that happens to hit the same endpoint. There's no place to say once \"NIM's\n quota adapter is X and its model list is Y\" and have every `*-nvidia` backend\n pick it up.\n3. **Backend is a redundant layer once (1) and (2) are fixed.** A named agent\n already stores `(backend, model, cwd)`. Once a backend is nothing but\n \"which harness, which provider,\" using a Codex+NIM combination the first\n time requires adding a `backends:` entry that exists only to name a\n permutation the agent record already has to state. Not every harness ×\n provider pairing works, either — some combinations are untested or simply\n incompatible (e.g. a subscription-only harness can't take an arbitrary\n `base_url`) — so a fixed named-backend list was never a validation\n mechanism anyway, just an extra step.\n\nThis ADR promotes provider to a first-class config section, finishes the\ndriver → harness rename ADR-0027 started in spirit, and retires `backend` as\na named entity in favor of agents selecting harness + provider directly.\n\n## Decision\n\n### Harness\n\nThe primary runtime term is `harness`: `agent/harnesses.py` owns the supported\nharness set, protocol defaults, install paths, install commands, default\nproviders, and API compatibility metadata. Same five values, now spelled out\ninstead of abbreviated in code (`claudecode`, `codex`, `cursor`, `opencode`,\n`pi`); the `cc`/`cx`/`cr`/`oc`/`pi` short forms from ADR-0027 remain an\nID-prefix convention (agent IDs, gauge labels), not the internal field value.\nLegacy `driver` names still exist in compatibility code while\n`agent/backends.py` and older API fields are being retired.\n\nThe Claude Code harness is renamed from `claude` to `claudecode` — matching\nthe existing `opencode` pattern — because `claude` also names a provider\n(Anthropic's models). ADR-0027 already flagged this exact collision for\nbackend IDs (`pi-claude` = Pi harness + Claude provider); giving the harness\nits own non-overlapping identifier removes the ambiguity at the source\ninstead of relying on position-in-string to disambiguate it.\n\nProtocol is a harness property, not something a provider or agent chooses\nindependently — it's dictated by what the CLI itself supports. Only\n`claudecode` has a true persistent structured stdin/stdout protocol, so it's\nthe only harness offering `interactive-cli`; every other harness is\n`oneshot-cli` only, unchanged from `SUPPORTED_PROTOCOLS_BY_DRIVER` /\n`DEFAULT_PROTOCOL_BY_DRIVER` today. The interactive idle-timeout default\n(`DEFAULT_INTERACTIVE_IDLE_TIMEOUT_SECONDS`) is likewise a harness-level\nconstant rather than a per-agent override — today's per-backend\n`interactive.idle_timeout_seconds` override (exercised by exactly one test\nbackend) is dropped for simplicity. If per-agent tuning turns out to be\nneeded later, it can be added back as an agent field without touching this\nADR's harness/provider split.\n\nSupported harnesses and default provider. Install commands are unchanged and\nstay defined once in `agent/harnesses.py`, matching the README table — not\nduplicated here:\n\n| Harness | Value | ID prefix | Protocol | Default provider |\n|---|---|---|---|---|\n| Claude Code | `claudecode` | `cc` | oneshot-cli | Claude (anthropic) |\n| Codex | `codex` | `cx` | oneshot-cli | GPT (openai) |\n| Cursor | `cursor` | `cr` | oneshot-cli | Cursor |\n| OpenCode | `opencode` | `oc` | oneshot-cli | NVIDIA NIM (`nvidia`) |\n| Pi | `pi` | `pi` | oneshot-cli | NVIDIA NIM (`nvidia`) |\n\n\"Installed\" reuses the existing `shutil.which()` check in `config.py`\n(`CLAUDE_PATH`, `CODEX_PATH`, ...) — no new detection mechanism. This is\nqueryable through `/health`: its `harnesses` payload contains\n`{id, label, install_cmd, installed, protocol, interactive,\ndefault_provider, supported_apis, compatible_providers}` for the settings UI.\nThe startup `_check_deps()` warning is a thin wrapper around that same\nharness data. There is no separate `/config/harnesses` endpoint in the\ncurrent implementation.\n\n### Provider\n\n`providers:` becomes a top-level config section. A provider owns everything\nabout the API endpoint that doesn't depend on which harness is talking to\nit — including `color`, since color exists to show at a glance which quota\npool a message drew from, which is a provider fact, not a harness fact:\n\n```yaml\nproviders:\n anthropic:\n label: Claude\n color: \"#AE5332\"\n auth: {type: subscription}\n openai:\n label: GPT\n color: \"#7070A0\"\n auth: {type: subscription}\n cursor:\n label: Cursor\n color: \"#8F8D8D\"\n auth: {type: subscription}\n deepseek:\n label: DeepSeek\n color: \"#4D9DE0\"\n base_url: \"https://api.deepseek.com/anthropic\"\n auth: {type: api_key, api_key: {env: DEEPSEEK_API_KEY}}\n gauge: deepseek\n models: [deepseek-chat, deepseek-reasoner]\n nvidia:\n label: NVIDIA\n color: \"#76B900\"\n base_url: \"https://integrate.api.nvidia.com/v1\"\n auth: {type: api_key, api_key: \"nvapi-...\"}\n gauge: {type: static, text: \"NIM-managed\"}\n models: [nvidia/nemotron-4-340b, meta/llama-3.1-405b, ...]\n```\n\n`api_key` lives under `auth`, not as a sibling field — it's part of how the\nprovider authenticates, not a connection detail like `base_url`. When\n`auth.type` is `api_key`, `auth.api_key` is required — either a literal\nstring or `{env: NAME}` to read from an environment variable at execution\ntime, unchanged from ADR-0024. It's never returned in provider metadata sent\nto the UI. When `auth.type` is `subscription`, `auth.api_key` isn't read at\nall, even if present.\n\nA provider id can be declared more than once for the same underlying vendor\nto hold separate credentials — e.g. `deepseek` and `deepseek-team2`, each\nwith its own `auth.api_key` and `color`. This is also the new home for the old\nbackend-level `env`/`settings`/`args` escape hatches: they move to the\nprovider, since they describe how to reach an endpoint, not which harness is\nasking.\n\nSquid ships only zero-config providers in code defaults\n(`anthropic`, `openai`, `cursor`, `opencode`). Providers requiring user\ncredentials, such as `nvidia` and `deepseek`, are documented in\n`config/squid.yaml.example` rather than auto-seeded for existing installs.\n`nvidia` is the canonical provider id for NVIDIA NIM because both Pi and\nOpenCode pass models using a `provider/model` prefix, so the provider id must\nmatch the CLIs' native provider name.\n\n**Direct vs. aggregator providers.** No new field for this — it falls out of\n`models`. A direct provider (`deepseek`, `anthropic`, `openai`) serves its own\nmodel line and typically has a short or empty `models` list. An aggregator\nprovider (`nvidia`) fronts many vendors' models behind one endpoint and has a\nlong `models` list. Both are configured the same way.\n\n### Auth type\n\n`auth.type` is `api_key` or `subscription`:\n\n- `subscription` — the harness CLI handles its own browser login (Claude Code\n max/pro, Codex ChatGPT login, Cursor login). Squid never asks for or stores\n a credential; `base_url`/`auth.api_key` are meaningless and missing-secret\n checks never fire for these agents.\n- `api_key` — Squid resolves `auth.api_key` (literal or `{env: NAME}`,\n unchanged from ADR-0024) and injects it plus `base_url` into the harness's\n native env/settings, same translation `execution_env()`/`driver_settings()`\n do today.\n\n### Backend is retired — agents select harness + provider directly\n\nThere is no `backends:` config section. A named agent extends ADR-0024's\n`(backend, model, cwd)` to `(harness, provider, model, cwd)`:\n\n```\nname: cc-deepseek-work\nharness: claudecode\nprovider: deepseek\nmodel: deepseek-chat\ncwd: ~/Work/squid\n```\n\nThe agent's own name already serves as its display label — no separate\n`label` field is needed at the agent level. `label` on a provider (above) is\nstill needed for the two places a backend's label shows up in the UI today\n(`ui/app.js`): the provider-picker dropdown when creating/editing an agent,\nand the quota status row, where it's paired with `color` as one dot-plus-name\nunit identifying which quota pool a gauge belongs to.\n\nNot every harness × provider pair is expected to work, so the UI doesn't\noffer a raw cross-product. The current compatibility list is derived from\nAPI-shape overlap: each harness declares `supported_apis`, each provider\ndeclares `supported_apis`, and `/health.harnesses[].compatible_providers`\ncontains providers whose sets intersect, plus the harness default provider\nwhen present. The legacy `compatible_providers` harness config key is still\naccepted as an explicit additive override. Anything outside the list remains\nreachable through the existing freeform provider path in config/API usage;\nthe UI uses the list as guidance, not as server-side model validation.\nLive \"does this combination actually work\" validation (e.g. a test-connection\naction) is out of scope for this ADR and can be layered on later without\nchanging this shape.\n\n### Model catalog stays local, no hosted sync\n\n`models:` on a provider is a **suggestion list for the UI**, not a validated\nenum. The agent-creation model field renders it as a dropdown with a\nfreeform-text fallback (`<datalist>`-style): pick from the list, or type any\nmodel string the provider actually supports. Squid does not validate the\nmodel against the list server-side.\n\nA centrally-hosted, auto-updating provider/model catalog was considered and\nrejected for now: it adds an online dependency and a staleness/versioning\nproblem (new models ship faster than any catalog sync would) to solve a UX\nnicety that a plain freeform input already handles. Revisit only if\nhand-maintaining `models:` lists in `squid.yaml` becomes a recurring\ncomplaint — the local-list-plus-freeform design doesn't foreclose that,\nsince a hosted catalog could later populate the same field.\n\n## Consequences\n\n- ADR-0027 is superseded. Its `{harness}-{provider}` convention existed to\n disambiguate a single composite `backends:` YAML key; with `backend` gone\n and harness/provider now separate structured fields on the agent, there is\n no composite ID left to disambiguate. Its collision reasoning (`claude` as\n both harness and provider) is resolved more directly here, by renaming the\n harness to `claudecode` rather than relying on string position.\n- `harness` is the primary config/API field. Legacy `driver` naming still\n appears in compatibility code and old tests until `agent/backends.py` and\n legacy backend surfaces are fully removed. For current `squid.yaml`, this\n is a breaking config change: `driver:` → `harness:` wherever it was set.\n- Per the precedent set for the `deepcla` → `deepseek` cleanup, there is no\n automatic migration or aliasing. Users update `squid.yaml` by hand;\n `config/squid.yaml.example` ships the new shape.\n- `providers:` is a new top-level config section. Loading uses\n `_configured_providers()` plus a zero-config default providers table\n (`anthropic`, `openai`, `cursor`, `opencode`). API-key providers such as\n `nvidia` and `deepseek` live in `config/squid.yaml.example`.\n- `backends:` is removed from the primary config shape. New execution uses\n `agent/harnesses.py`, `agent/providers.py`, and `agent/resolve.py`; the\n resolved runtime is ephemeral and exposes the same execution surface callers\n used to get from `Backend` (`execution_env()`, `driver_settings()`, `args`,\n `protocol`, `interactive`, `fingerprint`, availability, gauge, color).\n `agent/backends.py` remains as compatibility code while older tests and API\n surfaces are retired.\n- SQLite stores structured `harness`/`provider` fields directly. It does not\n store `backend` or `last_backend`; API responses compute those compatibility\n refs as `harness` or `harness:provider` when older UI/client surfaces still\n need them. `AgentRequest` accepts `harness`/`provider` first and falls back\n through legacy `backend` parsing (`split_agent_ref`) when needed.\n- Runtime attribution follows the same rule: logs and durable stats should\n include `harness` and `provider`; `backend` may be emitted alongside them\n only as the legacy runtime reference.\n- Session-resume fingerprinting (ADR-0024's \"backend configuration\n fingerprint\") is computed from the resolved harness + provider + model +\n protocol at execution time instead of `Backend.fingerprint()`. A change to\n a provider's `base_url`/`auth.api_key`/`env` still starts a fresh session\n for every agent using it, same as today.\n- Harness metadata is exposed through `/health.harnesses`; provider metadata\n is exposed through `/health.providers`. `_check_deps()` includes Pi and uses\n the same harness install-check data instead of a separate table.\n- Pre-launch schema compaction removed migration/backfill code and legacy\n backend storage columns. Existing local SQLite files may still contain extra\n columns from earlier development builds, but current code no longer reads or\n writes them.\n- This ADR is implemented in the current code for config loading, runtime\n resolution, harness/provider metadata, and the settings UI. Remaining work\n is cleanup: retire `agent/backends.py` and remove legacy computed `backend`\n API fields once dependent UI/tests are updated.\n"}, "cf147d5007fa74f25591c9112f10004b8624b936": {"t": 1785765019.5012429, "n": 14351, "h": "67b1cf9254aa2233", "c": "---\nstatus: accepted\ndate: 2026-07-12\nupdated: 2026-07-12\n---\n# ADR-0028: Harness/Provider Separation\n\n> Supersedes [ADR-0027](0027-backend-naming-convention.md)'s\n> `{harness}-{provider}` composite backend-ID convention — see Consequences.\n\n## Context\n\nADR-0024 defined a **driver** as the coded CLI integration (`claude`, `codex`,\n`cursor`, `opencode`, `pi`) and a **backend** as a YAML-configured instance of\none driver plus connection details (`provider`, `api_key`, `base_url`, `gauge`,\n...). ADR-0027 layered a naming convention on top — `{harness}-{provider}` —\nusing \"harness\" as the human name for what the code still calls `driver`, and\nestablished that a backend is really a harness × provider pair.\n\nThree problems remain past naming:\n\n1. **\"Driver\" undersells what the field is.** It's not just wiring code, it's\n the coding-agent product itself (Claude Code, Codex, Cursor, OpenCode, Pi).\n ADR-0027 already talks in terms of harness/provider; the code and config\n should say the same word.\n2. **Provider is a string, not a config surface.** `provider`, `base_url`,\n `gauge`, and the implicit \"does this need an api_key or a browser login\"\n question are currently set per-backend, duplicated across every backend\n that happens to hit the same endpoint. There's no place to say once \"NIM's\n quota adapter is X and its model list is Y\" and have every `*-nvidia` backend\n pick it up.\n3. **Backend is a redundant layer once (1) and (2) are fixed.** A named agent\n already stores `(backend, model, cwd)`. Once a backend is nothing but\n \"which harness, which provider,\" using a Codex+NIM combination the first\n time requires adding a `backends:` entry that exists only to name a\n permutation the agent record already has to state. Not every harness ×\n provider pairing works, either — some combinations are untested or simply\n incompatible (e.g. a subscription-only harness can't take an arbitrary\n `base_url`) — so a fixed named-backend list was never a validation\n mechanism anyway, just an extra step.\n\nThis ADR promotes provider to a first-class config section, finishes the\ndriver → harness rename ADR-0027 started in spirit, and retires `backend` as\na named entity in favor of agents selecting harness + provider directly.\n\n## Decision\n\n### Harness\n\nThe primary runtime term is `harness`: `agent/harnesses.py` owns the supported\nharness set, protocol defaults, install paths, install commands, default\nproviders, and API compatibility metadata. Same five values, now spelled out\ninstead of abbreviated in code (`claudecode`, `codex`, `cursor`, `opencode`,\n`pi`); the `cc`/`cx`/`cr`/`oc`/`pi` short forms from ADR-0027 remain an\nID-prefix convention (agent IDs, gauge labels), not the internal field value.\nLegacy `driver` names still exist in compatibility code while\n`agent/backends.py` and older API fields are being retired.\n\nThe Claude Code harness is renamed from `claude` to `claudecode` — matching\nthe existing `opencode` pattern — because `claude` also names a provider\n(Anthropic's models). ADR-0027 already flagged this exact collision for\nbackend IDs (`pi-claude` = Pi harness + Claude provider); giving the harness\nits own non-overlapping identifier removes the ambiguity at the source\ninstead of relying on position-in-string to disambiguate it.\n\nProtocol is a harness property, not something a provider or agent chooses\nindependently — it's dictated by what the CLI itself supports. Only\n`claudecode` has a true persistent structured stdin/stdout protocol, so it's\nthe only harness offering `interactive-cli`; every other harness is\n`oneshot-cli` only, unchanged from `SUPPORTED_PROTOCOLS_BY_DRIVER` /\n`DEFAULT_PROTOCOL_BY_DRIVER` today. The interactive idle-timeout default\n(`DEFAULT_INTERACTIVE_IDLE_TIMEOUT_SECONDS`) is likewise a harness-level\nconstant rather than a per-agent override — today's per-backend\n`interactive.idle_timeout_seconds` override (exercised by exactly one test\nbackend) is dropped for simplicity. If per-agent tuning turns out to be\nneeded later, it can be added back as an agent field without touching this\nADR's harness/provider split.\n\nSupported harnesses and default provider. Install commands are unchanged and\nstay defined once in `agent/harnesses.py`, matching the README table — not\nduplicated here:\n\n| Harness | Value | ID prefix | Protocol | Default provider |\n|---|---|---|---|---|\n| Claude Code | `claudecode` | `cc` | interactive-cli | Claude (anthropic) |\n| Codex | `codex` | `cx` | oneshot-cli | GPT (openai) |\n| Cursor | `cursor` | `cr` | oneshot-cli | Cursor |\n| OpenCode | `opencode` | `oc` | oneshot-cli | NVIDIA NIM (`nvidia`) |\n| Pi | `pi` | `pi` | oneshot-cli | NVIDIA NIM (`nvidia`) |\n\n\"Installed\" reuses the existing `shutil.which()` check in `config.py`\n(`CLAUDE_PATH`, `CODEX_PATH`, ...) — no new detection mechanism. This is\nqueryable through `/health`: its `harnesses` payload contains\n`{id, label, install_cmd, installed, protocol, interactive,\ndefault_provider, supported_apis, compatible_providers}` for the settings UI.\nThe startup `_check_deps()` warning is a thin wrapper around that same\nharness data. There is no separate `/config/harnesses` endpoint in the\ncurrent implementation.\n\n### Provider\n\n`providers:` becomes a top-level config section. A provider owns everything\nabout the API endpoint that doesn't depend on which harness is talking to\nit — including `color`, since color exists to show at a glance which quota\npool a message drew from, which is a provider fact, not a harness fact:\n\n```yaml\nproviders:\n anthropic:\n label: Claude\n color: \"#AE5332\"\n auth: {type: subscription}\n openai:\n label: GPT\n color: \"#7070A0\"\n auth: {type: subscription}\n cursor:\n label: Cursor\n color: \"#8F8D8D\"\n auth: {type: subscription}\n deepseek:\n label: DeepSeek\n color: \"#4D9DE0\"\n base_url: \"https://api.deepseek.com/anthropic\"\n auth: {type: api_key, api_key: {env: DEEPSEEK_API_KEY}}\n gauge: deepseek\n models: [deepseek-chat, deepseek-reasoner]\n nvidia:\n label: NVIDIA\n color: \"#76B900\"\n base_url: \"https://integrate.api.nvidia.com/v1\"\n auth: {type: api_key, api_key: \"nvapi-...\"}\n gauge: {type: static, text: \"NIM-managed\"}\n models: [nvidia/nemotron-4-340b, meta/llama-3.1-405b, ...]\n```\n\n`api_key` lives under `auth`, not as a sibling field — it's part of how the\nprovider authenticates, not a connection detail like `base_url`. When\n`auth.type` is `api_key`, `auth.api_key` is required — either a literal\nstring or `{env: NAME}` to read from an environment variable at execution\ntime, unchanged from ADR-0024. It's never returned in provider metadata sent\nto the UI. When `auth.type` is `subscription`, `auth.api_key` isn't read at\nall, even if present.\n\nA provider id can be declared more than once for the same underlying vendor\nto hold separate credentials — e.g. `deepseek` and `deepseek-team2`, each\nwith its own `auth.api_key` and `color`. This is also the new home for the old\nbackend-level `env`/`settings`/`args` escape hatches: they move to the\nprovider, since they describe how to reach an endpoint, not which harness is\nasking.\n\nSquid ships only zero-config providers in code defaults\n(`anthropic`, `openai`, `cursor`, `opencode`). Providers requiring user\ncredentials, such as `nvidia` and `deepseek`, are documented in\n`config/squid.yaml.example` rather than auto-seeded for existing installs.\n`nvidia` is the canonical provider id for NVIDIA NIM because both Pi and\nOpenCode pass models using a `provider/model` prefix, so the provider id must\nmatch the CLIs' native provider name.\n\n**Direct vs. aggregator providers.** No new field for this — it falls out of\n`models`. A direct provider (`deepseek`, `anthropic`, `openai`) serves its own\nmodel line and typically has a short or empty `models` list. An aggregator\nprovider (`nvidia`) fronts many vendors' models behind one endpoint and has a\nlong `models` list. Both are configured the same way.\n\n### Auth type\n\n`auth.type` is `api_key` or `subscription`:\n\n- `subscription` — the harness CLI handles its own browser login (Claude Code\n max/pro, Codex ChatGPT login, Cursor login). Squid never asks for or stores\n a credential; `base_url`/`auth.api_key` are meaningless and missing-secret\n checks never fire for these agents.\n- `api_key` — Squid resolves `auth.api_key` (literal or `{env: NAME}`,\n unchanged from ADR-0024) and injects it plus `base_url` into the harness's\n native env/settings, same translation `execution_env()`/`driver_settings()`\n do today.\n\n### Backend is retired — agents select harness + provider directly\n\nThere is no `backends:` config section. A named agent extends ADR-0024's\n`(backend, model, cwd)` to `(harness, provider, model, cwd)`:\n\n```\nname: cc-deepseek-work\nharness: claudecode\nprovider: deepseek\nmodel: deepseek-chat\ncwd: ~/Work/squid\n```\n\nThe agent's own name already serves as its display label — no separate\n`label` field is needed at the agent level. `label` on a provider (above) is\nstill needed for the two places a backend's label shows up in the UI today\n(`ui/app.js`): the provider-picker dropdown when creating/editing an agent,\nand the quota status row, where it's paired with `color` as one dot-plus-name\nunit identifying which quota pool a gauge belongs to.\n\nNot every harness × provider pair is expected to work, so the UI doesn't\noffer a raw cross-product. The current compatibility list is derived from\nAPI-shape overlap: each harness declares `supported_apis`, each provider\ndeclares `supported_apis`, and `/health.harnesses[].compatible_providers`\ncontains providers whose sets intersect, plus the harness default provider\nwhen present. The legacy `compatible_providers` harness config key is still\naccepted as an explicit additive override. Anything outside the list remains\nreachable through the existing freeform provider path in config/API usage;\nthe UI uses the list as guidance, not as server-side model validation.\nLive \"does this combination actually work\" validation (e.g. a test-connection\naction) is out of scope for this ADR and can be layered on later without\nchanging this shape.\n\n### Model catalog stays local, no hosted sync\n\n`models:` on a provider is a **suggestion list for the UI**, not a validated\nenum. The agent-creation model field renders it as a dropdown with a\nfreeform-text fallback (`<datalist>`-style): pick from the list, or type any\nmodel string the provider actually supports. Squid does not validate the\nmodel against the list server-side.\n\nA centrally-hosted, auto-updating provider/model catalog was considered and\nrejected for now: it adds an online dependency and a staleness/versioning\nproblem (new models ship faster than any catalog sync would) to solve a UX\nnicety that a plain freeform input already handles. Revisit only if\nhand-maintaining `models:` lists in `squid.yaml` becomes a recurring\ncomplaint — the local-list-plus-freeform design doesn't foreclose that,\nsince a hosted catalog could later populate the same field.\n\n## Consequences\n\n- ADR-0027 is superseded. Its `{harness}-{provider}` convention existed to\n disambiguate a single composite `backends:` YAML key; with `backend` gone\n and harness/provider now separate structured fields on the agent, there is\n no composite ID left to disambiguate. Its collision reasoning (`claude` as\n both harness and provider) is resolved more directly here, by renaming the\n harness to `claudecode` rather than relying on string position.\n- `harness` is the primary config/API field. Legacy `driver` naming still\n appears in compatibility code and old tests until `agent/backends.py` and\n legacy backend surfaces are fully removed. For current `squid.yaml`, this\n is a breaking config change: `driver:` → `harness:` wherever it was set.\n- Per the precedent set for the `deepcla` → `deepseek` cleanup, there is no\n automatic migration or aliasing. Users update `squid.yaml` by hand;\n `config/squid.yaml.example` ships the new shape.\n- `providers:` is a new top-level config section. Loading uses\n `_configured_providers()` plus a zero-config default providers table\n (`anthropic`, `openai`, `cursor`, `opencode`). API-key providers such as\n `nvidia` and `deepseek` live in `config/squid.yaml.example`.\n- `backends:` is removed from the primary config shape. New execution uses\n `agent/harnesses.py`, `agent/providers.py`, and `agent/resolve.py`; the\n resolved runtime is ephemeral and exposes the same execution surface callers\n used to get from `Backend` (`execution_env()`, `driver_settings()`, `args`,\n `protocol`, `interactive`, `fingerprint`, availability, gauge, color).\n `agent/backends.py` remains as compatibility code while older tests and API\n surfaces are retired.\n- SQLite stores structured `harness`/`provider` fields directly. It does not\n store `backend` or `last_backend`; API responses compute those compatibility\n refs as `harness` or `harness:provider` when older UI/client surfaces still\n need them. `AgentRequest` accepts `harness`/`provider` first and falls back\n through legacy `backend` parsing (`split_agent_ref`) when needed.\n- Runtime attribution follows the same rule: logs and durable stats should\n include `harness` and `provider`; `backend` may be emitted alongside them\n only as the legacy runtime reference.\n- Session-resume fingerprinting (ADR-0024's \"backend configuration\n fingerprint\") is computed from the resolved harness + provider + model +\n protocol at execution time instead of `Backend.fingerprint()`. A change to\n a provider's `base_url`/`auth.api_key`/`env` still starts a fresh session\n for every agent using it, same as today.\n- Harness metadata is exposed through `/health.harnesses`; provider metadata\n is exposed through `/health.providers`. `_check_deps()` includes Pi and uses\n the same harness install-check data instead of a separate table.\n- Pre-launch schema compaction removed migration/backfill code and legacy\n backend storage columns. Existing local SQLite files may still contain extra\n columns from earlier development builds, but current code no longer reads or\n writes them.\n- This ADR is implemented in the current code for config loading, runtime\n resolution, harness/provider metadata, and the settings UI. Remaining work\n is cleanup: retire `agent/backends.py` and remove legacy computed `backend`\n API fields once dependent UI/tests are updated.\n"}, "a6c618831f8f8eb4dff6f5fb7df4887af73d06ef": {"t": 1785765021.0366614, "n": 13667, "h": "75f43eccf62d4fe3", "c": "---\nstatus: accepted\ndate: 2026-07-12\n---\n# ADR-0028: Harness/Provider Separation\n\n> Supersedes [ADR-0027](0027-backend-naming-convention.md)'s\n> `{harness}-{provider}` composite backend-ID convention — see Consequences.\n\n## Context\n\nADR-0024 defined a **driver** as the coded CLI integration (`claude`, `codex`,\n`cursor`, `opencode`, `pi`) and a **backend** as a YAML-configured instance of\none driver plus connection details (`provider`, `api_key`, `base_url`, `gauge`,\n...). ADR-0027 layered a naming convention on top — `{harness}-{provider}` —\nusing \"harness\" as the human name for what the code still calls `driver`, and\nestablished that a backend is really a harness × provider pair.\n\nThree problems remain past naming:\n\n1. **\"Driver\" undersells what the field is.** It's not just wiring code, it's\n the coding-agent product itself (Claude Code, Codex, Cursor, OpenCode, Pi).\n ADR-0027 already talks in terms of harness/provider; the code and config\n should say the same word.\n2. **Provider is a string, not a config surface.** `provider`, `base_url`,\n `gauge`, and the implicit \"does this need an api_key or a browser login\"\n question are currently set per-backend, duplicated across every backend\n that happens to hit the same endpoint. There's no place to say once \"NIM's\n quota adapter is X and its model list is Y\" and have every `*-nim` backend\n pick it up.\n3. **Backend is a redundant layer once (1) and (2) are fixed.** A named agent\n already stores `(backend, model, cwd)`. Once a backend is nothing but\n \"which harness, which provider,\" using a Codex+NIM combination the first\n time requires adding a `backends:` entry that exists only to name a\n permutation the agent record already has to state. Not every harness ×\n provider pairing works, either — some combinations are untested or simply\n incompatible (e.g. a subscription-only harness can't take an arbitrary\n `base_url`) — so a fixed named-backend list was never a validation\n mechanism anyway, just an extra step.\n\nThis ADR promotes provider to a first-class config section, finishes the\ndriver → harness rename ADR-0027 started in spirit, and retires `backend` as\na named entity in favor of agents selecting harness + provider directly.\n\n## Decision\n\n### Harness\n\n`driver` is renamed `harness` everywhere: the `SUPPORTED_DRIVERS` constant,\nthe `Backend.driver` field, the YAML `driver:` key, and every `driver=`\nreference in runners/server code. Same five values, now spelled out instead\nof abbreviated in code (`claudecode`, `codex`, `cursor`, `opencode`, `pi`); the\n`cc`/`cx`/`cr`/`oc`/`pi` short forms from ADR-0027 remain an ID-prefix\nconvention (agent IDs, gauge labels), not the internal field value.\n\nThe Claude Code harness is renamed from `claude` to `claudecode` — matching\nthe existing `opencode` pattern — because `claude` also names a provider\n(Anthropic's models). ADR-0027 already flagged this exact collision for\nbackend IDs (`pi-claude` = Pi harness + Claude provider); giving the harness\nits own non-overlapping identifier removes the ambiguity at the source\ninstead of relying on position-in-string to disambiguate it.\n\nProtocol is a harness property, not something a provider or agent chooses\nindependently — it's dictated by what the CLI itself supports. Only\n`claudecode` has a true persistent structured stdin/stdout protocol, so it's\nthe only harness offering `interactive-cli`; every other harness is\n`oneshot-cli` only, unchanged from `SUPPORTED_PROTOCOLS_BY_DRIVER` /\n`DEFAULT_PROTOCOL_BY_DRIVER` today. The interactive idle-timeout default\n(`DEFAULT_INTERACTIVE_IDLE_TIMEOUT_SECONDS`) is likewise a harness-level\nconstant rather than a per-agent override — today's per-backend\n`interactive.idle_timeout_seconds` override (exercised by exactly one test\nbackend) is dropped for simplicity. If per-agent tuning turns out to be\nneeded later, it can be added back as an agent field without touching this\nADR's harness/provider split.\n\nSupported harnesses and default provider. Install commands are unchanged and\nstay defined once, in the README / `_check_deps` table — not duplicated here:\n\n| Harness | Value | ID prefix | Protocol | Default provider |\n|---|---|---|---|---|\n| Claude Code | `claudecode` | `cc` | interactive-cli | Claude (anthropic) |\n| Codex | `codex` | `cx` | oneshot-cli | GPT (openai) |\n| Cursor | `cursor` | `cr` | oneshot-cli | Cursor |\n| OpenCode | `opencode` | `oc` | oneshot-cli | NIM (nvidia) |\n| Pi | `pi` | `pi` | oneshot-cli | NIM (nvidia) |\n\n\"Installed\" reuses the existing `shutil.which()` check in `config.py`\n(`CLAUDE_PATH`, `CODEX_PATH`, ...) — no new detection mechanism. This becomes\nqueryable, not just startup-logged: `GET /config/harnesses` returns\n`[{id, label, install_cmd, installed}]` for the settings UI to render as an\ninstall checklist, replacing the log-only `_check_deps` warning.\n\n### Provider\n\n`providers:` becomes a top-level config section. A provider owns everything\nabout the API endpoint that doesn't depend on which harness is talking to\nit — including `color`, since color exists to show at a glance which quota\npool a message drew from, which is a provider fact, not a harness fact:\n\n```yaml\nproviders:\n anthropic:\n label: Claude\n color: \"#AE5332\"\n auth: {type: subscription}\n openai:\n label: GPT\n color: \"#7070A0\"\n auth: {type: subscription}\n cursor:\n label: Cursor\n color: \"#8F8D8D\"\n auth: {type: subscription}\n deepseek:\n label: DeepSeek\n color: \"#4D9DE0\"\n base_url: \"https://api.deepseek.com/anthropic\"\n auth: {type: api_key, api_key: {env: DEEPSEEK_API_KEY}}\n gauge: deepseek\n models: [deepseek-chat, deepseek-reasoner]\n nim:\n label: NVIDIA NIM\n color: \"#76B900\"\n base_url: \"https://integrate.api.nvidia.com/v1\"\n auth: {type: api_key, api_key: \"nvapi-...\"}\n gauge: {type: static, text: \"NIM-managed\"}\n models: [nvidia/nemotron-4-340b, meta/llama-3.1-405b, ...]\n```\n\n`api_key` lives under `auth`, not as a sibling field — it's part of how the\nprovider authenticates, not a connection detail like `base_url`. When\n`auth.type` is `api_key`, `auth.api_key` is required — either a literal\nstring or `{env: NAME}` to read from an environment variable at execution\ntime, unchanged from ADR-0024. It's never returned in provider metadata sent\nto the UI. When `auth.type` is `subscription`, `auth.api_key` isn't read at\nall, even if present.\n\nA provider id can be declared more than once for the same underlying vendor\nto hold separate credentials — e.g. `deepseek` and `deepseek-team2`, each\nwith its own `auth.api_key` and `color`. This is also the new home for the old\nbackend-level `env`/`settings`/`args` escape hatches: they move to the\nprovider, since they describe how to reach an endpoint, not which harness is\nasking.\n\n**Direct vs. aggregator providers.** No new field for this — it falls out of\n`models`. A direct provider (`deepseek`, `anthropic`, `openai`) serves its own\nmodel line and typically has a short or empty `models` list. An aggregator\nprovider (`nim`) fronts many vendors' models behind one endpoint and has a\nlong `models` list. Both are configured the same way.\n\n### Auth type\n\n`auth.type` is `api_key` or `subscription`:\n\n- `subscription` — the harness CLI handles its own browser login (Claude Code\n max/pro, Codex ChatGPT login, Cursor login). Squid never asks for or stores\n a credential; `base_url`/`auth.api_key` are meaningless and missing-secret\n checks never fire for these agents.\n- `api_key` — Squid resolves `auth.api_key` (literal or `{env: NAME}`,\n unchanged from ADR-0024) and injects it plus `base_url` into the harness's\n native env/settings, same translation `execution_env()`/`driver_settings()`\n do today.\n\n### Backend is retired — agents select harness + provider directly\n\nThere is no `backends:` config section. A named agent extends ADR-0024's\n`(backend, model, cwd)` to `(harness, provider, model, cwd)`:\n\n```\nname: cc-deepseek-work\nharness: claudecode\nprovider: deepseek\nmodel: deepseek-chat\ncwd: ~/Work/squid\n```\n\nThe agent's own name already serves as its display label — no separate\n`label` field is needed at the agent level. `label` on a provider (above) is\nstill needed for the two places a backend's label shows up in the UI today\n(`ui/app.js`): the provider-picker dropdown when creating/editing an agent,\nand the quota status row, where it's paired with `color` as one dot-plus-name\nunit identifying which quota pool a gauge belongs to.\n\nNot every harness × provider pair is expected to work, so the UI doesn't\noffer a raw cross-product. Squid ships a small, hand-curated compatibility\nseed list (harness → known-good providers) that filters the provider picker\nonce a harness is chosen — this is a generalization of the \"default\nprovider\" column above into a short allow-list per harness, not just one\nentry. Anything outside the seed list is still reachable through a freeform\nprovider field, same escape hatch already decided for `models:` — unvalidated\nand unblocked, not hidden. The seed list is hand-maintained, not\nauto-discovered or tested at save time; live \"does this combination actually\nwork\" validation (e.g. a test-connection action) is out of scope for this\nADR and can be layered on later without changing this shape.\n\n### Model catalog stays local, no hosted sync\n\n`models:` on a provider is a **suggestion list for the UI**, not a validated\nenum. The agent-creation model field renders it as a dropdown with a\nfreeform-text fallback (`<datalist>`-style): pick from the list, or type any\nmodel string the provider actually supports. Squid does not validate the\nmodel against the list server-side.\n\nA centrally-hosted, auto-updating provider/model catalog was considered and\nrejected for now: it adds an online dependency and a staleness/versioning\nproblem (new models ship faster than any catalog sync would) to solve a UX\nnicety that a plain freeform input already handles. Revisit only if\nhand-maintaining `models:` lists in `squid.yaml` becomes a recurring\ncomplaint — the local-list-plus-freeform design doesn't foreclose that,\nsince a hosted catalog could later populate the same field.\n\n## Consequences\n\n- ADR-0027 is superseded. Its `{harness}-{provider}` convention existed to\n disambiguate a single composite `backends:` YAML key; with `backend` gone\n and harness/provider now separate structured fields on the agent, there is\n no composite ID left to disambiguate. Its collision reasoning (`claude` as\n both harness and provider) is resolved more directly here, by renaming the\n harness to `claudecode` rather than relying on string position.\n- `driver` no longer appears in code, config, or API responses — `harness`\n does. This is a breaking config change: `driver:` → `harness:` wherever it\n was set.\n- Per the precedent set for the `deepcla` → `deepseek` cleanup, there is no\n automatic migration or aliasing. Users update `squid.yaml` by hand;\n `config/squid.yaml.example` ships the new shape.\n- `providers:` is a new top-level config section; loading needs a\n `_configured_providers()` plus a default providers table (`anthropic`,\n `openai`, `cursor`, `deepseek`, `nim` at minimum) for fresh installs,\n mirroring today's `_DEFAULT_BACKENDS`.\n- `backends:` and the `Backend` dataclass go away. `agent/backends.py`'s\n responsibilities split into harness lookup (protocol, install path) and\n provider resolution (base_url, auth, gauge, models, color). The `agents`\n SQLite table's `backend TEXT` column becomes `harness TEXT` + `provider\n TEXT`; `upsert_agent()`, `get_agent()`, and `get_default_agent()` (which\n today falls back through `for backend in BACKENDS`) change signature to\n match. `AgentRequest` in `agent/server.py` drops `backend: str = \"auto\"` in\n favor of `harness`/`provider` fields, and `create_agent()`'s `backend ==\n \"auto\"` resolution logic moves to picking a harness/provider pair instead\n of a backend id.\n- Session-resume fingerprinting (ADR-0024's \"backend configuration\n fingerprint\") is computed from the resolved harness + provider + model +\n protocol at execution time instead of `Backend.fingerprint()`. A change to\n a provider's `base_url`/`auth.api_key`/`env` still starts a fresh session\n for every agent using it, same as today.\n- `GET /config/harnesses` is a new endpoint; `_check_deps()`'s startup log\n becomes a thin wrapper around the same install-check data instead of a\n separate code path. Pi is currently missing from `_check_deps` entirely —\n fold it in while touching this.\n- **Historical stats data is backfilled once, not kept dual-column\n forever.** Squid has no real users yet, so there's no need for the\n permanent `COALESCE(agent, backend)`-style legacy fallback the `agents`\n table already carries from an earlier migration. `messages`,\n `topic_sessions`, `agents`, and `topics` gain `harness`/`provider`\n columns; a one-time backfill script decomposes every existing `backend`\n value via a hardcoded lookup built from today's known backend ids (e.g.\n `claude` → `(claudecode, anthropic)`, `cc-deepseek` → `(claudecode,\n deepseek)`, the old default-backends fallback `deepseek` → `(claudecode,\n deepseek)`). Any `backend` value that doesn't match a known id is not\n dropped — the raw string is stashed in `harness` as-is with `provider`\n left null, so old rows degrade to an unrecognized-harness display rather\n than losing data. The old `backend` column is dropped after backfill runs.\n- This ADR is the design; it does not itself change code. Implementation is\n a follow-up pass through `agent/config.py`, `agent/backends.py`,\n `agent/stats_db.py`, `agent/server.py`, `config/squid.yaml.example`, and\n the settings UI.\n"}}}}