Account link: one model config, auth as a reversible transform, policy separate
- Dominant language
- Rust
- Stars
- 41k
- Forks
- 3.6k
- Avg merge
- 13h 59m
- Merged PRs (30d)
- 299
Description
## Problem
Codewhale currently gives three different answers to "which models do I have".
| Surface | Answer today |
| -- | -- |
| Codewhale API `/v1/models` | every model of every connected provider |
| Composer picker (web/desktop) | the account Fleet, or everything when the fleet is empty |
| Local folder Work | Core's own `~/.codewhale` keys — account models are unreachable |
The Models page renders these as three separate connection *origins* (`account`, `runtime`, `desktop`), each with its own card and its own reach sentence. Before this issue, that sentence read:
> "Cloud Work and the Codewhale API — Chat needs a connected Runtime; not local folder Work"
That is a reach, a caveat and a transport rule in one line, and it documents a gap rather than closing it. Six user-facing nouns describe what is really one axis — where does this run, your computer or a rented one. Founder, 2026-09-09: *"we actually want our codewhale apps and codewhale itself api keys and configurations to be completely linked."*
## Why local folder Work cannot see account models
Not an architectural gap. Every part already exists:
* Core already has a `codewhale` provider pointing at `https://api.codewhale.net/v1`, env `CODEWHALE_API_KEY` (`crates/config/src/route/providers-export.golden.json`).
* Core already mints account API keys: `codewhale account api-keys create`.
* The Codewhale API already serves every connected model from one key (`services/control-plane/src/account-api/model-api.mjs`).
* Core already pushes local keys up: `codewhale account keys set --from-local`.
Nothing wires them together. After `codewhale login` the user is pointed at `app.codewhale.net/settings?section=api` to paste a key by hand. That manual step is the entire reason account models do not reach local folder Work.
## Design, following OpenCode
Taken from `refs/opencode/opencode/specs/v2/`. Three things they do that we should:
**1. One provider list; credential source is a field, not a category.** (`provider-model.md`)
```ts
enabled: false
| { via: "env", name: string }
| { via: "account", service: string }
| { via: "custom", data: {...} }
```
One provider, whose `enabled.via` records where the credential came from — instead of three parallel origins each rendered as its own kind of thing.
**2. Auth is a reversible config transform, not a parallel catalog.** (`catalog-config-plugin-lifecycle.md`)
```
Account.switched(providerID) → AuthPlugin.refresh() → transform(applyAuthToConfig) → Reload.all()
```
> "[models.dev]() and auth become config transforms rather than catalog dependencies."
> "Plugin disablement removes its config transform and lets services rematerialize without manual undo."
Signing in rewrites the one config. Signing out lifts the transform. No second store to reconcile, no sync to maintain, no manual undo. This is strictly better than a one-way "mint a key and write it into config" copy, which leaves residue.
**3. Config and policy are separate systems.** (`provider-policy.md`)
> "A provider can be correctly configured and have valid credentials while policy still denies its use."
> "Policies do not configure endpoints, credentials, models, or provider options."
> "Policies do not make unusable resources usable."
Layered `user > repo > (later) organization-managed`, `action: provider.use`, wildcard resources. This settles where enterprise restriction belongs in Codewhale: a policy layer an org admin owns — **not** the Fleet.
## The three concepts, kept apart
* **Capability** — your keys. What the Codewhale API serves. Never narrowed by a settings click; that is the one thing the API offers over calling providers directly.
* **Fleet** — the curated short menu, synced across web/desktop/mobile. Ordering and attention only.
* **Policy** — allow/deny, layered, org-ownable. Where restriction lives.
## Work
**Core (**`codewhale`**)** — see the handoff prompt in `codewhale-ops/prompts/`.
1. On successful `codewhale account login`, apply an account auth transform that configures the `codewhale` provider from the signed-in account, so every account model is immediately available in local folder Work.
2. The provider secret must never reach the client. Core holds a `cwc_` Codewhale key, not the user's [Z.ai/DeepSeek]() key. This preserves the existing credential boundary rather than weakening it.
3. Sign-out lifts the transform; no manual undo, no orphaned config.
4. Idempotent: repeated logins on the same machine reuse the existing device key rather than minting a new one each time.
**Apps (**`codewhale-apps`**)**
1. Idempotent machine-link endpoint returning a scoped, device-named account API key — reuse an existing live key for the same device instead of minting duplicates. This is what keeps Core's transform reversible and stops key litter.
2. Auto-provisioned device keys are visible and revocable in the UI, labelled by device.
3. Longer term: collapse the three connection origins onto one provider with a `via` field, and retire the "Cloud Work" / "local folder Work" / "This computer" vocabulary down to one axis.
## Done already (2026-09-09, commit `00d10b2f`)
* `GET /v1/models` now **reports** fleet membership as a flag rather than being gated by it. Capability stays a question about keys.
* The reach copy states only what a connection reaches. The caveat sentence was deleted rather than rewritten — UI copy that explains a gap is how a gap becomes permanent.
* `npm test` 3298 pass / 0 fail / 90 skipped; `npm run check:web` exit 0.
## Acceptance
* After `codewhale login` on a clean machine, an account model runs local folder Work with no manual key paste.
* No provider secret is ever written to the client; only a `cwc_` key.
* `codewhale logout` leaves no account-derived provider config behind.
* Two logins on one machine produce one device key, not two.
* Restricting model access is expressible as policy, and the Fleet still cannot narrow what the Codewhale API serves.
---
## Progress — 2026-09-09
**Apps item 1 is done** (`578aab2e`). `POST /api/account/device-link` mints a `models:infer` account key carrying a `device` context, so Core holds a `cwc_` credential and the provider secret never leaves the control plane.
One live key per device, however many times you log in. A minted secret exists only in the response that created it, so "reuse the existing key" cannot mean handing the same secret back later — linking therefore **rotates**. Revoke runs *before* mint, because minting first would leave two live keys for one device if the revoke failed, which is the litter this exists to prevent. Attribution follows the cloud-sandbox plugins key in `sandbox/account-plugins.mjs`, including its discipline of taking a key back when the store cannot persist the context.
`npm test` 3305 pass / 0 fail / 90 skipped (3395 total, 7 new); `npm run check:web` exit 0.
**Core handoff prompt written**: `codewhale-ops/prompts/CORE-ACCOUNT-LINK-20260910.md`. Core calls the endpoint above with a stable `deviceId`; it is live and tested.
**Still open**
* Core: apply the account auth transform at login, lift it at logout.
* Apps: surface device keys in the UI, labelled by machine and revocable.
* Apps: collapse the three connection origins onto one provider with a `via` field, and retire the "Cloud Work" / "local folder Work" / "This computer" vocabulary.
Also corrected in this pass: the project description and `AGENTS.md` both claimed the free issue limit prevented new tickets. Creating this issue disproved it; both were updated.
Contributor guide
Research direction
Start with codewhale-ops/prompts/CORE-ACCOUNT-LINK-20260910.md and the existing codewhale account login/logout flow, then inspect POST /api/account/device-link and sandbox/account-plugins.mjs for key-handling patterns. Run the existing npm test and npm run check:web checks where applicable. Done means login applies a reversible account transform, logout removes it, provider secrets stay server-side, and repeated logins leave one device key.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, rust
- Domain
- api, authentication, backend-api-design, cli
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 30/100