Skip to content

devin: add multi-account support - #928

Open
sonder-albert wants to merge 16 commits into
magpie-community:mainfrom
sonder-albert:devin-multi-account
Open

sonder-albert wants to merge 16 commits into
magpie-community:mainfrom
sonder-albert:devin-multi-account

Conversation

@sonder-albert

@sonder-albert sonder-albert commented Nov 10, 2026 •

Copy link
Copy Markdown

What changed

The Devin provider now keeps multiple sign-ins usable through magpie's existing account-slot handling, while preserving the provider id and request translation:

  • keep Devin's existing metadata.email / organization naming so separate people and organizations remain distinct;
  • add a validated Devin session token method for tokens copied from another machine;
  • leave unknown identity metadata empty so magpie can distinguish accounts by their stored key;
  • use Devin CLI-compatible browser OAuth (prompt=select_account) and keep the browser callback within magpie's 10-minute sign-in window;
  • document the separate account slots and retry behavior.

This keeps the browser and Devin CLI sign-in methods named consistently and avoids unsupported top-level account-id or max-account settings.

Validation

  • node --check packages/devin/index.mjs
  • npx bun test packages/devin/identity.test.mjs: 14 pass, 0 fail
  • regression coverage verifies token trimming/prefix validation, identity lookup, unknown identities, and prompt=select_account
  • package version updated to 0.1.39

The real-account checks used existing local credentials and did not include credentials in this pull request.

@yetone yetone left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this. Reviewed at c31079e. Requesting changes: the PR turns two existing tests red, and with magpie's current host the account-id change either does nothing or, if honoured, merges different people into one slot.

What was run

On Linux (WSL2, Bun), HOME=$(mktemp -d) bun test packages/devin:

  • origin/main b44b034: 208 pass, 0 fail

  • this PR: 207 pass, 2 fail, both in packages/devin/identity.test.mjs:

    • a second account is named as Devin names its key, not as the CLI's own account (line 91)
    • two organizations under the same email are named and keyed apart (line 118)

    Both fail because success() now returns an extra accountId field and those toEquals don't expect it.

  • bun scripts/check.mjs: passes.

  • Broken by hand: with accountId: accountIdOf(who) removed from success(), the new account-id.test.mjs still passes. It tests only the helper, not that a sign-in uses it.

No real Devin accounts were used in this review.

How magpie stores a Devin sign-in today

Devin's success() returns { type: "success", key, metadata } with no refresh. magpie's plugin host (internal/plugin/host.js, save()) stores such a result as { type: "api", key, metadata }. It does not keep a top-level accountId. It then de-duplicates (settle()) by whoOf(a) = a.accountId ?? a.metadata.email ?? …. For Devin that is metadata.email, which whoOfStatus already makes "email · org". So:

  • Several Devin accounts already work on main: every sign-in started as a new account gets its own slot (signInKey → freshKey). Two emails, or one email in two orgs, stay apart. The README's "Several accounts" gap line is stale, so dropping it is fine.
  • accountId in index.mjs:949 is dropped by the host, so it changes nothing in magpie (OpenCode doesn't read it either).
  • maxAccounts: 0 at index.mjs:3213 is on the provider config object, and nothing in magpie reads maxAccounts at all yet. zcode puts it on the auth hook and in package.json's magpie block. 0 would also mean "none" rather than "unlimited" to a reader that does read it. Please remove it: leaving it unset already means every account is used.

Problems if accountId were honoured

index.mjs:804: accountIdOf = who?.orgId || who?.teamId || who?.user || "Devin".

  1. Two members of one org collide. whoOfStatus sets orgId from devinInfo.orgId || teamId, which every seat in a Devin Teams org shares. Alice (a@x.dev, org org_1) signs in, then Bob (b@x.dev, same org) signs in as a new account. Both get accountId: "org_1", settle() sees the same whoOf, and Bob's token replaces Alice's slot. Alice's account is gone. account-id.test.mjs asserts that preference (orgId first) instead of catching it. An account id has to name the person and the org together, e.g. the "email · org" that metadata.email already holds, or email + orgId.
  2. An unknown identity becomes "the same account". When whoByKey and the CLI both can't tell, accountIdOf(undefined) is "Devin", so every such sign-in has one id and each one overwrites the last. A failed identity read means unknown. It should give no accountId, so the host falls back to telling accounts apart by their secret. (main has the same issue through metadata.email: "Devin". It is worth fixing here too: leave email unset when unknown, and name the row by the label fallback that already exists.)
  3. Existing users would get a duplicate. Accounts saved before this change have no accountId, so whoOf is "email · org". Signing in again to the same account would give "org_1", which matches nothing, and leave two slots for one account.

The new "Devin session token (another account)" method (index.mjs:3150)

  • It has no authorize, so the host stores the pasted text as is: no trimming, no check that it is a devin-session-token$…, no GetUserStatus call. A wrong or revoked token is kept as a signed-in account with no name until the first usage read marks it. Give it an authorize that trims, checks the prefix, and returns await success(key, SERVER). Then a refused key fails at sign-in, as the browser method's does.
  • "(another account)" is misleading: the browser and CLI methods add another account too when started from Add account. A paste method is still useful (a token from another machine), but name it "Devin session token".

Requested

  • Drop accountId from success() and accountIdOf, or make it per person (never org alone) and absent when identity is unknown.
  • Remove maxAccounts: 0.
  • Give the paste method an authorize that validates through success(), and rename it.
  • Update identity.test.mjs to match, and add tests that fail on the bugs above: two emails in one org give two different ids, and an unknown identity gives no id.
  • Fix the README paragraph at lines 41–45 accordingly (no "organization or team id" preference, no "unlimited account slots").

@yetone yetone left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the quick turnaround. Reviewed at a7f19ce. Every point from the last review is fixed, and the checks below pass. One small regression is left, plus two README wording fixes, so I'm requesting changes once more.

Fixed

  • accountId, accountIdOf and maxAccounts are gone, so magpie's host keys each account by metadata.email ("email · org") as before.
  • An unknown identity now gives empty metadata instead of email: "Devin", so the host tells such accounts apart by their key.
  • The paste method is named "Devin session token". Its authorize trims the token, checks the prefix and goes through success().
  • The identity.test.mjs expectations are updated, and the no-op test is removed.

What was run

On Linux (WSL2, Bun), with a fresh HOME:

  • bun test packages/devin: 209 pass, 0 fail.
  • bun scripts/check.mjs: passes.
  • Broken by hand, one at a time. Each made a test fail:
    • removing the prefix check in authorize;
    • returning { type: "success", key } without success();
    • dropping the .trim();
    • putting back email: who?.user || "Devin".

No real Devin accounts were used.

Still to fix

  1. index.mjs:984, the browser sign-in's success page. It says `${r.metadata.email} is signed in`. Now that email is left unset when neither Devin nor the CLI can name the account, that page reads "undefined is signed in". Please fall back the way the v2 labels do (r.metadata.email || "Devin"), or say "You're signed in" without a name. A test that checks the page with an unknown identity would guard it.
  2. README, the "Devin session token" bullet. "This adds a separate account slot instead of replacing the existing sign-in" isn't quite right. Every method adds a new account when it's started from Add account, and every method, this one included, replaces the account when it's started from that account's Sign in again. Something like "paste a session token, e.g. one from another machine" is enough.
  3. README, the new paragraph. The rest of the README writes "magpie" in lower case. Please do the same in "Magpie keeps separate account slots…" and "Magpie can try another…".

@yetone yetone left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the update. Reviewed at 8f8838b. The three points from the last review are fixed, but the two new behaviour changes need another look, so I'm requesting changes once more.

Fixed

  • The success page falls back to "Devin" when the account can't be named.
  • The README's paste bullet no longer says it adds a separate slot.
  • "magpie" is lower case in the README.

What was run

On Linux (WSL2, Bun), with a fresh HOME:

  • bun test packages/devin: 209 pass, 0 fail.
  • bun scripts/check.mjs: passes.
  • Broken by hand, one at a time: removing the || "Devin" fallback, putting back prompt: "select_account", and putting back the 10-minute timeout. All 209 tests still pass each time, so nothing in the suite covers these three changes.

Still to fix

  1. SIGN_IN_TIMEOUT raised to 30 minutes (8f8838b). magpie waits 10 minutes for a plugin's browser sign-in (signInTimeout in magpie's internal/provider/signin.go) and then tells the user it timed out. With 30 minutes here, the plugin keeps its callback server listening for 20 more minutes after magpie has given up, and a sign-in finished in that time says "You're signed in" in the browser while magpie already showed a failure. Please keep it at 10 minutes. If 10 minutes is too short in practice, that's a change on magpie's side; an issue on yetone/magpie with the case would be the place for it.
  2. prompt: "login" (ed40ae7). The commit says this forces a fresh sign-in for new accounts, but browserSignIn is the same for Add account and for an existing account's Sign in again, so every re-sign-in now has to enter credentials from scratch too, with no account picker. Also, the official devin CLI (3000.11.234) sends prompt=select_account to /auth/cli/continue; prompt=login appears in it only on the editor sign-in URL. The plugin follows the CLI's request, so please put back select_account. If you saw a case where select_account signed in the wrong account, please describe it (or open an issue) with what you tried, and we can look at it separately.
  3. The method label "Devin (browser, sign in another account)". Like the README point last time, this method is also what Sign in again uses for an existing account, so the label is misleading there. It now also differs from the v2 method's label, which is still "Devin (browser)". Please put back "Devin (browser)".
  4. index.mjs:984 indentation. The html(200, page(true, …)) line lost its indentation; please indent it like the lines around it.
  5. A test for the success page. Optional, but a test that runs the browser callback with an unknown identity and checks the page doesn't say "undefined" would keep point 1 of the last review fixed.

yetone added a commit to yetone/magpie that referenced this pull request Nov 10, 2026
An OpenCode plugin's "api" auth method with its own prompts and an
authorize (inkstone's API key ways, Devin's session token in
magpie-community/plugins#928) asks the key in its prompts and returns it
from authorize(inputs). The GUI asked those prompts and then always showed
a key field of its own, so the user typed the same token twice and the
second entry went nowhere. The host's save() also copied every answer into
the account's metadata, so the token sat there a second time in plain
text.

- host.js marks such a method keyAsked (type api, prompts, authorize).
  The GUI's startPluginSignIn signs in with no key field for it, as for
  keyless, and a rejected answer asks the prompts again on Try again.
  PluginAPIKey and `magpie plugin` login skip the empty-key check for it
  (Method.NoKey).
- save() keeps only select answers (choices, never secrets) beside the
  authorize result's own metadata. A method with no authorize still keeps
  every answer, as OpenCode does.
- An authorize that gives no key, with no key typed, is now an error
  instead of an "ok" that saved nothing.

OpenCode itself differs: its v1 CLI (providers.ts) asks the prompts, then
always asks "Enter your API key", saves result.key ?? the typed key, and
merges the answers into metadata; its TUI never calls authorize for api
methods. magpie-community plugins are written for magpie's flow and read
the key from inputs, so this follows them.

Plugins that read prompt answers back from metadata (grep of
magpie-community/plugins packages): zcode (site, a select), joycode
(tenant) and agnes (keyProfile, consoleToken). None has an authorize, so
none changes. Every api+authorize plugin (inkstone, cloudflare-workers-ai,
sensenova, agnes-code, astudio, sap-aicore, Devin #928) puts what it needs
in its result's metadata itself; sensenova's apiKey: undefined
workaround is now moot.

Run on mote (macOS 27, Go 1.26.8, Playwright 1.62.0) at this change on
origin/main 9c39edb: gofmt -l clean; go vet of internal/plugin/...
internal/gui/... internal/provider internal/cli, also GOOS=windows and
GOOS=linux -tags nogui: ok. go test ./internal/plugin/...
./internal/gui/... ./internal/provider: ok. ./internal/cli: ok but
TestEveryRouteHasACommandOrIsNamed, red the same alone on 9c39edb
(POST /api/gateway/pause from e8fbee6 has no command), filed as #5946.
node --test, chromium and webkit, of plugin-api-prompt-key (new),
plugin-keyless-signin, plugin-password-prompt, plugin-signin,
plugin-ask-ways, plugin-key-hint, plugin-other-ways, signin-paste-key,
plugin-signin-opened, plugin-signin-scrolls, plugin-pick-sub,
plugin-two-subs, zcode-site-layout and i18n-keys: 176 of 176 ok. The new
GUI test passed 3 of 3 alone. plugin-signin-no-url is red 22 of 22 at
the base too (987eac0); that is #5938, commented with the macOS run.

Broken by hand: GUI back to keyless-only and host without keyAsked ->
TestAPIPromptsKeyAskedAndMetadata and plugin-api-prompt-key 22 of 22 red;
save() copying every answer again -> the Go test red (token kept twice,
the note answer kept).

Not tried: a real plugin sign-in (inkstone, Devin), the CLI path (no
test), a run on the Windows or Linux boxes, the full ./... suite.
@yetone yetone mentioned this pull request Nov 10, 2026
@sonder-albert

Copy link
Copy Markdown
Author

Addressed the latest review feedback on 38cd8c5:

  • restored SIGN_IN_TIMEOUT to magpie's 10-minute window;
  • restored Devin CLI-compatible prompt=select_account;
  • restored the shared Devin (browser) label;
  • kept the unknown-identity success-page fallback and indentation fix;
  • added a regression test for the browser account chooser;
  • bumped @magpie-community/opencode-devin-auth to 0.1.39.

Validation: node --check packages/devin/index.mjs; npx bun test packages/devin/identity.test.mjs — 14 pass, 0 fail.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants