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:
3 files changed
+139
-1
No files matched your search
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user