1 line
57 KiB
JSON
1 line
57 KiB
JSON
{"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"}}}} |