Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions devlog/_plan/260911_devin_two_providers/001_plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# 001 — Devin/Cognition as two providers

Objective: opencodex gains two Devin-family providers.

- `devin` — cloud-direct. Connect-RPC to Cognition's `exa.api_server_pb.ApiServerService`,
carried from PR #4078 (author @wtfsayo) onto current `dev` and hardened.
- `devin-cli` — local. Spawns the Devin CLI and speaks Agent Client Protocol
(newline-delimited JSON-RPC on stdio), modeled on the user-supplied working
`server.mjs` proxy and the reference executor in `.tmp/openproxy-ref`.

`.tmp/openproxy-ref` (quangdang46/openproxy) is read-only reference. No code or
license-bearing text from it enters this repository.

## Work phases

| id | outcome |
|---|---|
| wp1 | Carry + harden the cloud-direct `devin` adapter on current `dev` |
| wp2 | Live Cognition evidence (free signup + client download via aside), fold verified constants in |
| wp3 | Second provider `devin-cli` over ACP stdio |
| wp4 | Docs/locale parity, full gates, PR, merge into `dev` |

## wp1 — what changes and why

The carry itself is done: `git merge --squash pr4078` applied cleanly onto
`9ea5759226`, the root-level test moved to its layout domain
(`tests/providers/devin-adapter.test.ts`) with `scripts/test-layout/layout.json`
and `tests/fixtures/test-layout-expected.json` updated, and the focused suites pass
(36/36). Four independent reviewers audited the result. Their findings define wp1's
diff:

### 1. Tenant api-server routing (major, real runtime failure)

`src/oauth/devin.ts` stores RegisterUser's `api_server_url` on the credential, but
`src/adapters/devin.ts` always posts GetUserJwt / GetCascadeModelConfigs /
GetChatMessage to `provider.baseUrl`, which `src/providers/registry.ts` hardcodes to
`https://server.codeium.com`. EU and FedStart tenants return a different host
(`eu.windsurf.com/_route/api_server`, `windsurf.fedstart.com/_route/api_server`), so
those accounts log in and then send every call to the wrong server. GitHub Copilot
already threads `credential.apiBaseUrl` through; Devin must do the same, falling back
to the default host only when RegisterUser returned nothing.

### 2. Portal/register override (major, real runtime failure)

Login always signs in against `DEFAULT_REGION`. `src/oauth/devin/types.ts` documents
a `--portal-url` override that nothing wires, so a non-US tenant never reaches its
matching RegisterUser host. Honor the override and persist it next to the api-server
URL on the credential.

### 3. Model-id normalization (minor, degraded path)

`src/adapters/devin.ts` has no dotted-to-hyphen map. With the live catalog missing we
append `-medium` to the raw id, turning `swe-1.6` into `swe-1.6-medium`, which
Cognition answers with an opaque `permission_denied`. Normalize `.` to `-` before
lookup and suffix only ids that actually carry an effort segment.

### 4. Docs/locale parity (major, deferred to wp4)

English `providers.md` and `reference/adapters.md` gained `devin`; the seven locales
(`ko ja zh-cn zh-tw fr ru tr`) still jump from `cursor` to `github-copilot` and from
`cursor` to `azure-openai`. No test compares them, but AGENTS.md forbids a locale
contradicting the English source. Both providers land in every locale in wp4, once
the final surface is known.

### 5. Auth and streaming findings

Two reviewers (credential handling; streaming terminal/abort semantics) are still
running. Their blockers and majors fold into this same wp1 diff before A closes.

## Boundaries

- No change to `src/router.ts`, `src/server/lifecycle.ts`, or
`src/server/responses/core.ts` reaching `src/lab/`.
- No new CLI command, so `skills/ocx/` and `src/cli/capabilities.ts` stay as they are.
- `devin` keeps `dashboardPreset: false` and stays out of the featured lists.
- Security notes stay in `.tmp/`, never in `devlog/`.
69 changes: 69 additions & 0 deletions devlog/_plan/260911_devin_two_providers/002_audit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# 002 — wp1 audit: folded reviewer findings

Four independent reviewers (xai/grok-4.6, high effort) audited the carried commit
`142c095673`. Three returned; the streaming reviewer is still running and its
findings fold into this same cycle if they arrive before C. Verdicts below are mine
after reading the cited code.

## Accepted — blocker

**Raw upstream bodies in auth error messages.** `register-user.ts:96,113` and
`cloud-direct/auth.ts:99,126` copy the response body into `Error.message`. That
message reaches CLI output, the adapter's `emit({ type: "error" })`, and
`/api/logs`. A Connect error that echoes `firebase_id_token`, or a 200 whose
`user_jwt` fails the shape regex, publishes a live credential; `redactSecretString`
does not match a bare `eyJ…` JWT. Confirmed by reading both files. Fix: status plus
allowlisted Connect code plus trace id, never the body.

## Accepted — major

1. **Tenant api-server routing.** `credential.apiBaseUrl` is written at login but
no call site reads it, and `store.ts:461` only persists Copilot origins, so an
EU/FedStart host is dropped on the next load anyway. Thread it through
`mintUserJwt`, the catalog fetch, and `streamChatEvents`, and teach the store to
persist a validated Devin origin.
2. **Redirect following on credential POSTs.** Both credential POSTs use the default
`redirect: "follow"`, so a 307/308 forwards the Firebase token or the protobuf
`api_key` to an attacker-chosen `Location`. Set `redirect: "error"` and validate
the host the same way `validateCopilotApiBaseUrl` does.
3. **Credential shape.** `refresh: ""` makes `detectOAuthWarning` report
`stale_credentials` for every Devin account from the moment of login, and
`refreshDevinToken` extends the expiry without contacting Cognition, so a revoked
key keeps looking valid. Use the durable-key house pattern: `refresh` carries the
key, expiry is effectively unbounded, and refresh throws so a 401 marks
`needsReauth`.
4. **Paste parsing.** `loginDevin` posts the entire pasted string as
`firebase_id_token`. The on-screen value is a token, but a user who pastes the
callback URL instead sends a URL. Parse a fragment/query token out of a URL paste
and reject a paste that contains no token.
5. **`clearCachedUserJwt` is never called.** The cached `user_jwt` (its payload
contains `api_key`) survives logout in process memory. Wire it into the Devin
logout path.

## Accepted — minor

6. `result.name` overwrites the JWT `email` with a display name, so reauth identity
comparison collides. Keep the email; the name is not an identity.
7. `registerUser` does not receive `ctrl.signal`, so cancelling login does not abort
the exchange.
8. No dotted-to-hyphen model-id map, so a degraded-path `swe-1.6` becomes
`swe-1.6-medium` and Cognition answers `permission_denied`.

## Rejected / deferred

- **Copying the reference's gRPC-web framing.** `.tmp/openproxy-ref` talks to
`LanguageServerService` over gRPC-web with a Bearer header; we talk to
`ApiServerService` over Connect-RPC with the key inside `Metadata`. They are two
different products. Adopting the reference's headers or field numbers would break
auth and proto decode. Reference value is the CLI/ACP executor, which is wp3.
- **`defaultRefreshPolicy: "disabled"`.** Correct for a durable key; keep it.
- **Docs/locale parity.** Real and required, but the final surface is not known until
`devin-cli` lands, so it is wp4.
- **Dead plugin types** (`PersistedCredentials`, `syncedViaOpencodeAuth`). Removed
where they are genuinely unreferenced; not a leak either way.

## Verification for this cycle

`bun x tsc --noEmit`, the focused Devin/adapter/layout suites, `bun run privacy:scan`,
plus new regression tests for: error messages that must not contain a token, redirect
refusal, host allowlist rejection, tenant host threading, and the dotted model id.
170 changes: 170 additions & 0 deletions devlog/_plan/260911_devin_two_providers/003_live_evidence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# 003 — wp2: live Cognition evidence

A free Cognition account was created through the browser on 2026-09-12 and the
shipped desktop client was downloaded. Everything below is measured, not inferred.

## What the account looks like

Devin Desktop 3.9.19 (`Devin-darwin-arm64-3.9.19.dmg`, 337 MB). Windsurf has been
rebranded: `windsurf.com` now redirects to `devin.ai/desktop`, and the bundled
extension still identifies itself as `publisher: codeium`, `name: windsurf`,
`displayName: Devin`. `product.json` reports `windsurfVersion: 3.9.19` and
`codeiumVersion: 1.48.2`.

## Constants confirmed against the shipped client

Read from `Devin.app/Contents/Resources/app/extensions/windsurf/dist/extension.js`:

- Auth0 client id `3GUryQ7ldAeKEuD2obYnppsnmj58eP5u` — present verbatim. The
carried adapter's value is correct.
- Hosts: `server.codeium.com`, `server-staging.codeium.com`,
`server-beta.codeium.com`, `register.windsurf.com`, `eu.windsurf.com/_route/api_server`,
`windsurf.fedstart.com/_route/api_server`, and the tenant template
`your-company.windsurf.com`. The allowlist in `src/oauth/devin/api-base.ts` was
widened to the two staging/beta hosts on this evidence.
- Method names `RegisterUser`, `GetChatMessage` and `GetCascadeModelConfigs` all
appear as string literals.

## What the live calls proved

1. **The sign-in token is not a JWT.** A real sign-in returned a 47-character
`ott$<base64url>` one-time token, and RegisterUser exchanged it successfully.
The JWT-shape gate added during wp1 would have rejected every real login, so
`parseDevinAuthPaste` now checks for one opaque credential-shaped word instead
of a token format. The token is single-use: the second exchange of the same
value fails, which is why the probe needed a fresh sign-in.

2. **The tenant-routing fix is load-bearing, not theoretical.** RegisterUser
returned `api_server_url: https://server.self-serve.windsurf.com` for an
ordinary free account — not `server.codeium.com`, which the registry hardcodes
and the carried adapter always used. Without wp1's change every free-tier
account would have sent its RPCs to a host it is not provisioned on.

3. **The api_key and the catalog work.** `GetCascadeModelConfigs` against that
host returned 227 model uids. Exactly one is enabled on the free tier:
`swe-1-6-slow`. The site advertises "unlimited SWE-2"; the API does not agree,
which is worth knowing before anyone documents a model list.

4. **`GetChatMessage` fails with `invalid_argument`.** Message is the opaque
"an internal error occurred (trace ID: …)". Client version strings `3.9.19`,
`2.0.0` and `1.48.2` in Metadata fields 2 and 7 all fail identically, so the
version pin is not the cause — the comment in `metadata.ts` claiming a version
mismatch produces exactly this error is no longer a sufficient explanation.
The version default was still moved to the shipped `3.9.19` with an
`OPENCODEX_DEVIN_CLIENT_VERSION` override, because `2.0.0` predates the rebrand
and nothing argues for keeping it.

This is the open item. The request encoding is being compared field by field
against the shipped bundle and against the two actively maintained references.

## Ecosystem survey

Twelve independent Windsurf/Cognition proxies were catalogued. The two that
matter here:

- `dwgx/WindsurfAPI` (~2975 stars, updated this week) uses the same
`server.codeium.com` `GetChatMessage` Connect-RPC path we do.
- `rsvedant/opencode-windsurf-auth` (~70 stars) is a direct-cloud Connect-RPC
streaming client for an opencode plugin. Our carried files reference
`opencode auth login`, `syncedViaOpencodeAuth` and an
`opencode-windsurf-auth` CLI in `src/oauth/devin/types.ts`, so #4078 very
likely derives from it. Its license and the derivation are being checked; if
it is derived, attribution is required before this merges.

`quangdang46/openproxy` talks to a different product (gRPC-web
`LanguageServerService`), so it is a secondary reference only.

## wp2 outcome: the cloud chat path stays unverified

Every request-shape hypothesis was tried against the live account and none of
them changed the trailer. In probe order: client version `3.9.19`, `2.0.0`,
`1.48.2`; the Connect request frame sent uncompressed with
`Connect-Content-Encoding` dropped; `Metadata` #31 filled with 732 hex
characters; `GetChatMessageRequest` #2, #15 and #20 added and #22 dropped on the
first turn; `ChatMessagePrompt` #1 `message_id` added; `Authorization: Basic`
in both base64 and raw doubled-key forms; and both hosts. Same
`invalid_argument: an internal error occurred` every time, with a fresh trace id.

The model gate is provably fine. `swe-2-high` and `claude-sonnet-5-medium` are
refused locally as disabled, and a bogus uid is refused as unlisted, so the
failure is specific to `swe-1-6-slow` — the one model a free account has, and a
"slow" lane at that.

**Entitlement now outranks request shape as the explanation.** The site
advertises "Slow Devin Cloud access with limited quotas" for free accounts, and a
slow lane plausibly is not served by this RPC at all. #4078's author reported a
live PONG on 2026-09-09 with the *original* field set, which is the deciding
fact: shipping unverified wire changes would risk regressing an account that
works today in exchange for no measured gain here. The whole experimental delta
was reverted; only the wp1 hardening and the MIT notice remain.

Confirming this needs a paid account or a captured working request. Neither is
available in this session, so the cloud provider is not merge-ready and the
adapter's own model gate is what stops a user hitting this blindly.

## The chat path works. What was actually wrong.

A paid account was obtained on 2026-09-12 and the entitlement hypothesis died
immediately: all 229 catalogue models came back enabled, and `GetChatMessage`
failed exactly as it had on the free account. The failure was never about the
plan.

Isolating it took one decisive move. The most actively maintained reference
(`dwgx/WindsurfAPI`) is zero-dependency ESM, so its request builder can simply be
imported. Building a turn with the reference builder and sending it through our
own transport returned **HTTP 200** and a real Connect stream — which proved the
transport, the headers and the credential were all fine, and put the fault in our
request encoder. Diffing the two encoded messages field by field left exactly one
difference: `CompletionConfiguration` (#8).

reference #1=1 #2=8192 #3=128000 #5=double #7=40 #8=double
ours #1=1 #2=64000 #3=32 #5=double #6=double #7=50 #8=double #11=double

**#2 is the output cap and #3 is the context window; we had them swapped.** A
caller asking for 32 output tokens wrote 32 into the context-window field, and
Cognition answered with an opaque `invalid_argument: an internal error occurred`.
That is why every account failed identically and why no amount of probing the
transport helped. The reference's own comments record the same mis-tagging and
the same re-calibration.

A second, independent trap sat behind it: **a temperature of exactly 0 is
refused** with the same opaque error. Deterministic output is the common case for
coding clients, so it is clamped to the smallest accepted value rather than
silently replaced with the service default.

Three transport facts also had to be right together, and testing them one at a
time is why they looked useless earlier:

- the credential is the session token doubled and dash-joined in
`Authorization: Basic`, while the protobuf body keeps a single copy;
- the request envelope is uncompressed;
- `Metadata` #31 carries 732 hex characters, whose length the service checks and
whose value it does not.

The metadata identity is also its own shape — seven fields, the optional
`user_jwt`, and the fingerprint — not the desktop client's fuller telemetry set.

### Verified

Six combinations, two hosts by three models, all returning `PONG` with a finish
reason and usage:

| host | model | result |
|---|---|---|
| `server.codeium.com` | `swe-2-high` | PONG, stop, 476/36 |
| `server.codeium.com` | `claude-sonnet-5-medium` | PONG, stop, 576/5 |
| `server.codeium.com` | `gpt-5-6-sol-medium` | PONG, 394/6 |
| `server.self-serve.windsurf.com` | `swe-2-high` | PONG, stop, 1/36 |
| `server.self-serve.windsurf.com` | `claude-sonnet-5-medium` | PONG, stop, 576/5 |
| `server.self-serve.windsurf.com` | `gpt-5-6-sol-medium` | PONG, 394/6 |

The tag map is now pinned by a regression test that builds a request and asserts
the field layout, so the swap cannot come back silently.

### What this retracts

The earlier conclusion in this document — that entitlement was the leading
explanation and that the request shape had been ruled out — was wrong. The
request shape was the whole problem; the probing that "ruled it out" changed one
variable at a time against a broken `CompletionConfiguration` that no single
variable could rescue.
33 changes: 33 additions & 0 deletions devlog/_plan/260911_devin_two_providers/004_devin_cli_split.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# 004 — wp5: splitting devin-cli out

The wp4 audit recommended splitting, citing MAINTAINERS.md: a new canonical
registry destination is a maintained promise, and when the evidence is incomplete
the repository wants an inert directory row rather than a registry entry. The
cloud `devin` provider cannot complete a turn on the account we can measure.
`devin-cli` does not share that RPC.

## What moved

Branch `codex/260912-devin-cli-provider` from a freshly fetched `origin/dev`
(`29d632ff25`). It carries `src/adapters/devin-cli/` and
`tests/providers/devin-cli-adapter.test.ts` byte-identical, plus only the
`devin-cli` hunks of the adapter registry, the provider registry, the routing
behaviour table, the layout map and the membership fixture. Docs get the English
provider row and adapters section and the provider row in all seven locales.

The tool-conformance skip lists needed care: on the other branch they name both
wires, and here only `devin-cli` exists, so naming a wire that is absent would
have been a silent no-op rather than a skip.

## What stayed

Everything cloud-direct: `src/adapters/devin/`, `src/oauth/devin*`, the `devin`
registry entry and its documentation, the MIT notice for the derived files, and
this plan unit. PR #4285 keeps them.

## Verification

`bun x tsc --noEmit` clean; 76 focused tests pass; `privacy:scan` green. An
independent audit of the split diff (21 files, +925/-5) found no cloud-provider
leakage, agreeing registries, resolving imports, and a PR description that
matches the code. Remote CI on the exact head is the suite gate.
Loading
Loading