kiro: a Builder ID sign-in is asked with Builder ID's service profile, so its models are listed (#422, Q1CHENL: only Auto offered while kiro-cli lists Opus 5.5, Sonnet 5 …) — a Builder ID sign-in (kiro-cli's, the Kiro IDE's or magpie's own) names no profile, and Kiro won't list one for it: List-Available-Profiles answers 403 "AWS Builder ID is not supported for this operation" in both regions, so every call needing the account's auth failed — List-Available-Models (magpie fell back to Auto alone), Get-Usage-Limits (the account stayed "Kiro account") and chat. A sign-in kiro-cli keeps with Builder ID's start URL (or none, as kiro-cli takes it), or one saved with provider BuilderId, now takes the service profile the Kiro IDE and kiro-cli use for Builder ID (arn:aws:codewhisperer:us-east-1:638616132270:profile/AAAACCCCXXXX) without asking, and an AWS sign-in whose profile listing is that 403 takes it too. Identity Center sign-ins still have their profile listed. TestKiroBuilderIDListsItsModels and TestKiroBuilderIDFoundByKirosAnswer fail without the change (fake management API). go vet, go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui ./... pass (TestFXInState's temp-dir cleanup flaked once under load, passes alone and with its package).
This commit is contained in:
3 files changed
+154
-5
No files matched your search
@@ -97,6 +97,7 @@ type kiroCred struct {
|
||||
clientSecret string // idc
|
||||
tokenURL string // external-idp
|
||||
social bool // an IDE sign-in with Google or GitHub
|
||||
builderID bool // an AWS Builder ID sign-in, which has no profile of its own
|
||||
|
||||
dbKey string // the auth_kv row it came from
|
||||
idePath string // or the IDE's file
|
||||
@@ -145,6 +146,8 @@ func readKiroCLI() (kiroCred, bool) {
|
||||
c.method = "social"
|
||||
case "odic":
|
||||
c.method = "idc"
|
||||
// as kiro-cli tells them apart: Builder ID's start URL, or none
|
||||
c.builderID = str(m, "start_url") == "" || str(m, "start_url") == kiroBuilderIDStart
|
||||
if reg := get("kirocli:odic:device-registration"); reg != nil {
|
||||
c.clientID, c.clientSecret = str(reg, "client_id"), str(reg, "client_secret")
|
||||
}
|
||||
@@ -177,12 +180,14 @@ func readKiroIDE() (kiroCred, bool) {
|
||||
Region string `json:"region"`
|
||||
ClientIDHash string `json:"clientIdHash"`
|
||||
AuthMethod string `json:"authMethod"`
|
||||
Provider string `json:"provider"`
|
||||
ProfileArn string `json:"profileArn"`
|
||||
}
|
||||
if json.Unmarshal(b, &t) != nil || t.AccessToken == "" {
|
||||
return kiroCred{}, false
|
||||
}
|
||||
c := kiroCred{access: t.AccessToken, refresh: t.RefreshToken, region: t.Region, profile: t.ProfileArn, method: "idc", idePath: path}
|
||||
c := kiroCred{access: t.AccessToken, refresh: t.RefreshToken, region: t.Region, profile: t.ProfileArn, method: "idc", idePath: path,
|
||||
builderID: strings.EqualFold(t.Provider, "BuilderId")}
|
||||
c.expires, _ = time.Parse(time.RFC3339Nano, t.ExpiresAt)
|
||||
if c.region == "" {
|
||||
c.region = "us-east-1"
|
||||
@@ -494,10 +499,14 @@ func kiroPost(ctx context.Context, u, contentType string, body []byte, headers m
|
||||
return json.Unmarshal(b, dst)
|
||||
}
|
||||
|
||||
// kiroManagementURL is where Kiro's management API is in a region; a var so
|
||||
// tests can answer it.
|
||||
var kiroManagementURL = func(region string) string { return "https://management." + region + ".kiro.dev/" }
|
||||
|
||||
// kiroManagement calls Kiro's management API: GET with a query, or POST
|
||||
// with a JSON body when body is set.
|
||||
func kiroManagement(ctx context.Context, region, token, tokenType, method string, q url.Values, body any, dst any) error {
|
||||
u := "https://management." + region + ".kiro.dev/" + method
|
||||
u := kiroManagementURL(region) + method
|
||||
verb := http.MethodGet
|
||||
var rd io.Reader
|
||||
if body != nil {
|
||||
@@ -543,11 +552,21 @@ func (e *kiroStatusError) Error() string {
|
||||
return "Kiro: " + http.StatusText(e.status)
|
||||
}
|
||||
|
||||
// A Builder ID sign-in has no profile of its own, and Kiro won't list one
|
||||
// for it (List-Available-Profiles is a 403, "AWS Builder ID is not supported
|
||||
// for this operation"); the Kiro IDE and kiro-cli name this service profile
|
||||
// for it instead, so its models, usage and chat are asked with it too.
|
||||
const (
|
||||
kiroBuilderIDStart = "https://view.awsapps.com/start"
|
||||
kiroBuilderIDProfile = "arn:aws:codewhisperer:us-east-1:638616132270:profile/AAAACCCCXXXX"
|
||||
)
|
||||
|
||||
// kiroProfile finds the profile a sign-in that doesn't name one uses: an
|
||||
// API key's own, or the first Kiro lists in either of its regions.
|
||||
// API key's own, Builder ID's, or the first Kiro lists in either of its
|
||||
// regions.
|
||||
func kiroProfile(ctx context.Context, c kiroCred) (string, error) {
|
||||
if c.method == "apikey" {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://management.us-east-1.kiro.dev/", strings.NewReader("{}"))
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, kiroManagementURL("us-east-1"), strings.NewReader("{}"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -573,6 +592,9 @@ func kiroProfile(ctx context.Context, c kiroCred) (string, error) {
|
||||
}
|
||||
return out.Profile.Arn, nil
|
||||
}
|
||||
if c.builderID {
|
||||
return kiroBuilderIDProfile, nil
|
||||
}
|
||||
tt := ""
|
||||
if c.method == "external-idp" {
|
||||
tt = "EXTERNAL_IDP"
|
||||
@@ -585,6 +607,11 @@ func kiroProfile(ctx context.Context, c kiroCred) (string, error) {
|
||||
} `json:"profiles"`
|
||||
}
|
||||
if err := kiroManagement(ctx, region, c.access, tt, "List-Available-Profiles", nil, map[string]any{}, &out); err != nil {
|
||||
// a Builder ID sign-in the token didn't say was one
|
||||
var se *kiroStatusError
|
||||
if c.method == "idc" && errors.As(err, &se) && se.status == http.StatusForbidden && strings.Contains(se.body, "Builder ID") {
|
||||
return kiroBuilderIDProfile, nil
|
||||
}
|
||||
last = err
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// fakeKiroManagement answers Kiro's management API as it answers a Builder
|
||||
// ID sign-in: no profiles to list (a 403), and the account's models and
|
||||
// usage for the Builder ID service profile.
|
||||
func fakeKiroManagement(t *testing.T) *[]string {
|
||||
t.Helper()
|
||||
var mu sync.Mutex
|
||||
asked := []string{}
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
mu.Lock()
|
||||
asked = append(asked, r.URL.Path)
|
||||
mu.Unlock()
|
||||
switch r.URL.Path {
|
||||
case "/List-Available-Profiles":
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
w.Write([]byte(`{"message":"AWS Builder ID is not supported for this operation."}`))
|
||||
case "/List-Available-Models":
|
||||
if r.URL.Query().Get("profileArn") != kiroBuilderIDProfile {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte(`{"message":"profileArn is required"}`))
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(map[string]any{"defaultModel": map[string]any{"modelId": "auto"}, "models": []any{
|
||||
map[string]any{"modelId": "auto", "modelName": "auto", "tokenLimits": map[string]any{"maxInputTokens": 1000000}},
|
||||
map[string]any{"modelId": "claude-opus-5.5", "modelName": "Claude Opus 5.5", "supportedInputTypes": []string{"TEXT", "IMAGE"},
|
||||
"tokenLimits": map[string]any{"maxInputTokens": 1000000, "maxOutputTokens": 128000}},
|
||||
map[string]any{"modelId": "claude-sonnet-5", "modelName": "Claude Sonnet 5", "tokenLimits": map[string]any{"maxInputTokens": 1000000}},
|
||||
map[string]any{"modelId": "claude-haiku-4.5", "modelName": "Claude Haiku 4.5", "tokenLimits": map[string]any{"maxInputTokens": 200000}},
|
||||
}})
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
was := kiroManagementURL
|
||||
kiroManagementURL = func(string) string { return srv.URL + "/" }
|
||||
t.Cleanup(func() { kiroManagementURL = was })
|
||||
return &asked
|
||||
}
|
||||
|
||||
func kiroFetchedIDs(t *testing.T) []string {
|
||||
t.Helper()
|
||||
p, ok := kiroAccount()
|
||||
if !ok {
|
||||
t.Fatal("no Kiro account")
|
||||
}
|
||||
ms, err := p.Fetch(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("fetch: %v", err)
|
||||
}
|
||||
var ids []string
|
||||
for _, m := range ms {
|
||||
ids = append(ids, m.ID)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// A Builder ID sign-in (kiro-cli's here) names no profile and Kiro lists
|
||||
// none for it, so magpie couldn't ask for its models at all and offered
|
||||
// Auto alone (#422); it is asked with Builder ID's service profile, as
|
||||
// kiro-cli and the IDE do, and Kiro's whole list is offered.
|
||||
func TestKiroBuilderIDListsItsModels(t *testing.T) {
|
||||
kiroSandbox(t)
|
||||
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
|
||||
asked := fakeKiroManagement(t)
|
||||
writeKiroCLI(t, map[string]any{
|
||||
"kirocli:odic:token": map[string]any{"access_token": "at", "refresh_token": "rt", "expires_at": "2099-01-01T00:00:00Z",
|
||||
"region": "us-east-1", "start_url": kiroBuilderIDStart, "oauth_flow": "DeviceCode"},
|
||||
"kirocli:odic:device-registration": map[string]any{"client_id": "cid", "client_secret": "sec"},
|
||||
})
|
||||
got := strings.Join(kiroFetchedIDs(t), ",")
|
||||
if got != "auto,claude-opus-5.5,claude-sonnet-5,claude-haiku-4.5" {
|
||||
t.Fatalf("models = %s (asked %v)", got, *asked)
|
||||
}
|
||||
for _, a := range *asked {
|
||||
if a == "/List-Available-Profiles" {
|
||||
t.Fatalf("asked for a Builder ID sign-in's profiles: %v", *asked)
|
||||
}
|
||||
}
|
||||
a, err := KiroAuthOf(context.Background(), "", "", false)
|
||||
if err != nil || a.Profile != kiroBuilderIDProfile || a.Region != "us-east-1" {
|
||||
t.Fatalf("auth = %+v %v", a, err)
|
||||
}
|
||||
// once listed, the account offers them, not Auto alone
|
||||
p, _ := kiroAccount()
|
||||
if ms := p.Available(); len(ms) != 4 {
|
||||
t.Fatalf("available = %+v", ms)
|
||||
}
|
||||
}
|
||||
|
||||
// An Identity Center sign-in still has its profile listed; one that turns
|
||||
// out to be Builder ID's (the IDE's file not saying) takes Builder ID's
|
||||
// profile when Kiro says it has none to list.
|
||||
func TestKiroBuilderIDFoundByKirosAnswer(t *testing.T) {
|
||||
kiroSandbox(t)
|
||||
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
|
||||
asked := fakeKiroManagement(t)
|
||||
dir := kiroIDEDir()
|
||||
os.MkdirAll(dir, 0o755)
|
||||
os.WriteFile(filepath.Join(dir, "kiro-auth-token.json"), []byte(`{"accessToken":"at","refreshToken":"rt","expiresAt":"2099-01-01T00:00:00Z",
|
||||
"authMethod":"IdC","region":"us-east-1","clientIdHash":"h"}`), 0o600)
|
||||
os.WriteFile(filepath.Join(dir, "h.json"), []byte(`{"clientId":"cid","clientSecret":"sec"}`), 0o600)
|
||||
if got := strings.Join(kiroFetchedIDs(t), ","); got != "auto,claude-opus-5.5,claude-sonnet-5,claude-haiku-4.5" {
|
||||
t.Fatalf("models = %s (asked %v)", got, *asked)
|
||||
}
|
||||
if (*asked)[0] != "/List-Available-Profiles" {
|
||||
t.Fatalf("asked %v", *asked)
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@ func readKiroFile(path string) (kiroCred, bool) {
|
||||
return kiroCred{}, false
|
||||
}
|
||||
c := kiroCred{access: t.AccessToken, refresh: t.RefreshToken, region: t.Region, profile: t.ProfileArn,
|
||||
clientID: t.ClientID, clientSecret: t.ClientSecret, idePath: path}
|
||||
clientID: t.ClientID, clientSecret: t.ClientSecret, idePath: path, builderID: strings.EqualFold(t.Provider, "BuilderId")}
|
||||
c.expires, _ = time.Parse(time.RFC3339Nano, t.ExpiresAt)
|
||||
if c.region == "" {
|
||||
c.region = "us-east-1"
|
||||
|
||||
Reference in new issue
Block a user