Repository navigation
devin: add multi-account support - #928
sonder-albert wants to merge 16 commits into
Conversation
yetone
left a comment
There was a problem hiding this comment.
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 extraaccountIdfield and thosetoEquals don't expect it. -
bun scripts/check.mjs: passes. -
Broken by hand: with
accountId: accountIdOf(who)removed fromsuccess(), the newaccount-id.test.mjsstill 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. accountIdinindex.mjs:949is dropped by the host, so it changes nothing in magpie (OpenCode doesn't read it either).maxAccounts: 0atindex.mjs:3213is on the provider config object, and nothing in magpie readsmaxAccountsat all yet. zcode puts it on theauthhook and inpackage.json'smagpieblock.0would 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".
- Two members of one org collide.
whoOfStatussetsorgIdfromdevinInfo.orgId || teamId, which every seat in a Devin Teams org shares. Alice (a@x.dev, orgorg_1) signs in, then Bob (b@x.dev, same org) signs in as a new account. Both getaccountId: "org_1",settle()sees the samewhoOf, and Bob's token replaces Alice's slot. Alice's account is gone.account-id.test.mjsasserts that preference (orgIdfirst) instead of catching it. An account id has to name the person and the org together, e.g. the"email · org"thatmetadata.emailalready holds, oremail + orgId. - An unknown identity becomes "the same account". When
whoByKeyand 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 noaccountId, so the host falls back to telling accounts apart by their secret. (main has the same issue throughmetadata.email: "Devin". It is worth fixing here too: leaveemailunset when unknown, and name the row by the label fallback that already exists.) - Existing users would get a duplicate. Accounts saved before this change have no
accountId, sowhoOfis"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 adevin-session-token$…, noGetUserStatuscall. A wrong or revoked token is kept as a signed-in account with no name until the first usage read marks it. Give it anauthorizethat trims, checks the prefix, and returnsawait 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
accountIdfromsuccess()andaccountIdOf, or make it per person (never org alone) and absent when identity is unknown. - Remove
maxAccounts: 0. - Give the paste method an
authorizethat validates throughsuccess(), and rename it. - Update
identity.test.mjsto 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
left a comment
There was a problem hiding this comment.
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,accountIdOfandmaxAccountsare gone, so magpie's host keys each account bymetadata.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
authorizetrims the token, checks the prefix and goes throughsuccess(). - The
identity.test.mjsexpectations 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 }withoutsuccess(); - dropping the
.trim(); - putting back
email: who?.user || "Devin".
- removing the prefix check in
No real Devin accounts were used.
Still to fix
index.mjs:984, the browser sign-in's success page. It says`${r.metadata.email} is signed in`. Now thatemailis 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.- 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.
- 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
left a comment
There was a problem hiding this comment.
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 backprompt: "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
SIGN_IN_TIMEOUTraised to 30 minutes (8f8838b). magpie waits 10 minutes for a plugin's browser sign-in (signInTimeoutin magpie'sinternal/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.prompt: "login"(ed40ae7). The commit says this forces a fresh sign-in for new accounts, butbrowserSignInis 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 officialdevinCLI (3000.11.234) sendsprompt=select_accountto/auth/cli/continue;prompt=loginappears in it only on the editor sign-in URL. The plugin follows the CLI's request, so please put backselect_account. If you saw a case whereselect_accountsigned in the wrong account, please describe it (or open an issue) with what you tried, and we can look at it separately.- 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)".
index.mjs:984indentation. Thehtml(200, page(true, …))line lost its indentation; please indent it like the lines around it.- 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.
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.
|
Addressed the latest review feedback on
Validation: |
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:
metadata.email/ organization naming so separate people and organizations remain distinct;Devin session tokenmethod for tokens copied from another machine;prompt=select_account) and keep the browser callback within magpie's 10-minute sign-in window;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.mjsnpx bun test packages/devin/identity.test.mjs: 14 pass, 0 failprompt=select_account0.1.39The real-account checks used existing local credentials and did not include credentials in this pull request.