devin: an account signed in only in magpie works without the Devin CLI signed in, and is listed once (蓝猫 on Discord: every request from Claude Code failed until devin auth login, then the same account showed twice) — the model list that turns a family and effort into the variant Devin takes (claude-sonnet-5-5 + high → claude-sonnet-5-5-high) was asked only of the CLI's own sign-in, so with that signed out or its key refused the bare family id went to Devin and was refused. It is now asked of the signed-in accounts in their own homes, the one in use first, the CLI's own last. And the CLI's own sign-in isn't listed beside a working magpie account of the same user (an old logins.json entry renamed to that user when the CLI signed in again). Tested with a new TestDevinSignedInOnlyInMagpie in a sandbox HOME with a fake devin CLI and exchange server, following 蓝猫's steps: it fails without either change (variant stays claude-sonnet-5-5; dev@example.com listed twice). go vet, go build (-tags nogui, GOOS=linux, GOOS=windows), go test -tags nogui ./... pass (TestUserPath and TestClaudeAccountNamedAsItsLogin flake under load, pass alone). Not tried against the real Devin API.

This commit is contained in:
yetone committed 2026-09-30 14:24:53 +08:00
1 parent 59aec42beb
commit 1721fac530
3 files changed
+139 -1

No files matched your search

+29 -1
View File
@@ -473,7 +473,35 @@ func devinFamiliesCached(families []DevinFamily) {
devinFamiliesCache.Unlock()
}
func askDevinFamilies(ctx context.Context) ([]DevinFamily, error) { return askDevinFamiliesAt(ctx, "") }
// askDevinFamilies is the model list as the accounts signed in give it, the
// one in use first: an account magpie signed in is asked in its own home,
// not the CLI's, which may be signed out (蓝猫 on Discord: with only a
// magpie sign-in every request failed, as the family's id went to Devin
// unturned into a variant, until `devin auth login`). The CLI's own is
// asked last when it isn't among them, as it was before there were homes.
func askDevinFamilies(ctx context.Context) ([]DevinFamily, error) {
var homes []string
for _, l := range devinLogins() {
homes = append(homes, l.Home)
}
if !slices.Contains(homes, "") {
homes = append(homes, "")
}
var first error
for _, home := range homes {
families, err := askDevinFamiliesAt(ctx, home)
if err == nil {
return families, nil
}
if first == nil {
first = err
}
if ctx.Err() != nil {
break
}
}
return nil, first
}
// askDevinFamiliesAt is the model list of the account signed in in home
// ("" for the CLI's own).
@@ -0,0 +1,100 @@
package provider
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
)
// TestDevinSignedInOnlyInMagpie is 蓝猫's Devin on Discord: the CLI kept a
// sign-in Devin no longer took, so magpie signed the account in in a home
// of its own. Every request failed until `devin auth login`: the model
// list, which turns a family (claude-sonnet-5-5) into the variant Devin's
// API takes, was asked of the CLI's own sign-in only. And once the CLI
// signed in to the same account, it was listed twice.
func TestDevinSignedInOnlyInMagpie(t *testing.T) {
home := claudeHome(t)
data := filepath.Join(home, "data")
t.Setenv("XDG_DATA_HOME", data)
os.MkdirAll(filepath.Join(data, "devin"), 0o700)
cliSignIn := func(key string) {
t.Helper()
if err := os.WriteFile(filepath.Join(data, "devin", "credentials.toml"), devinCredentials(key, "", "", ""), 0o600); err != nil {
t.Fatal(err)
}
forgetDevinStatus()
}
// a CLI that answers for the key in its data folder's credentials.toml:
// an old account's, one Devin refused, or the account signed in (by
// magpie, or by `devin auth login` after)
exe := filepath.Join(home, "devin")
os.WriteFile(exe, []byte(`#!/bin/sh
f="$XDG_DATA_HOME/devin/credentials.toml"
[ -f "$f" ] || { echo "Not logged in"; exit 1; }
if grep -q refused "$f"; then
if [ "$1" = models ]; then echo "Authentication required: Invalid token" >&2; exit 1; fi
`+devinKeyRefused+`
exit 0
fi
if [ "$1" = models ]; then
echo '{"families":[{"family_uid":"claude-sonnet-5-5","family_label":"Claude Sonnet 5.5","variants":[{"model_uid":"claude-sonnet-5-5-medium"},{"model_uid":"claude-sonnet-5-5-high"}]}]}'
exit 0
fi
if grep -q old "$f"; then who=old@example.com; else who=dev@example.com; fi
printf 'Logged in (via Devin).\n\nUser:\n Email: %s\n\nAccount:\n Tier: Devin Pro\n' "$who"
`), 0o755)
fakeDevin(t, exe)
devinFamiliesCached(nil)
t.Cleanup(func() { devinFamiliesCached(nil) })
// the CLI was signed in to another account once, remembered as its own
cliSignIn("devin-session-token$old")
if ls := devinLogins(); len(ls) != 1 || ls[0].User != "old@example.com" || ls[0].Home != "" {
t.Fatalf("CLI's own %+v", ls)
}
// then Devin refused its key: nobody is signed in there
cliSignIn("devin-session-token$refused")
if ls := devinLogins(); len(ls) != 0 {
t.Fatalf("refused %+v", ls)
}
fake := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]any{"sessionToken": "devin-session-token$magpie"})
}))
defer fake.Close()
oldTok := devinExchangeURL
devinExchangeURL = fake.URL
t.Cleanup(func() { devinExchangeURL = oldTok })
st, err := StartSignIn("devin")
if err != nil {
t.Fatal(err)
}
finishInBrowser(t, st, "dv-code")
if st = waitDone(t, st.ID); st.State != "done" || st.User != "dev@example.com" || !st.Using {
t.Fatalf("state %+v", st)
}
ls := devinLogins()
if len(ls) != 1 || ls[0].User != "dev@example.com" || ls[0].Home == "" {
t.Fatalf("signed in in magpie %+v", ls)
}
// a request for the family goes to Devin as its variant at the effort,
// read from the account magpie signed in
if got := DevinVariant(context.Background(), "claude-sonnet-5-5", "high"); got != "claude-sonnet-5-5-high" {
t.Fatalf("variant %q, want claude-sonnet-5-5-high", got)
}
// `devin auth login` to the same account: still one account
cliSignIn("devin-session-token$cli")
if ls := Logins("devin"); len(ls) != 1 || ls[0].User != "dev@example.com" || !ls[0].Active {
t.Fatalf("after the CLI signed in %+v", ls)
}
if p, ok := devinAccount(); !ok || p.Account.User != "dev@example.com" || p.AlsoOn() != nil {
t.Fatalf("account %+v also %+v", p.Account, p.AlsoOn())
}
}
+10
View File
@@ -75,6 +75,16 @@ func sideLogins(agent, ownUser string, usable func(savedLogin) bool) []sideLogin
if l.own() && (ownUser == "" || l.Hidden != "") || !l.own() && !usable(l) {
continue // signed out there, or removed in magpie
}
// the agent's own that is one of magpie's already, as when the
// agent was signed in to another account (or signed out) when
// magpie signed this one in, and the agent signed in to it after:
// that one stands for it, as above, not listed twice (蓝猫 on
// Discord, Devin)
if l.own() && slices.ContainsFunc(ls, func(m savedLogin) bool {
return m.Agent == agent && !m.own() && strings.EqualFold(m.User, l.User) && usable(m)
}) {
continue
}
if l.First || (first < 0 && l.own()) {
first = len(out)
}