cli: route `entire activity` to entire-api when configured · Entire
cli: route entire activity to entire-api when configured
efe23fd·
Soph·2w ago·4 files·+194 added/-1 removed
Assemble the entire-api client for the current repo and point activity
at it. The command already calls /me/activity and /me/commits — the exact
paths entire-api serves — so this is a routing change, not a response
rewrite:
- auth.ResolveEntireAPIToken mints a jurisdictional identity token for an explicit audience (no /.well-known; entire-api serves none), exchanged from the active login context.
- newEntireAPIClientForCurrentRepo ties it together: resolve the repo's mirror → cell/jurisdiction, fill the base-URL/audience templates, mint the token, return a client pointed at https://{cell}.api.entire.io.
- runAuthenticatedActivityAPI prefers that client and falls back to the data API when the ENTIRE_API_* templates are unset or the repo isn't routable, so existing users are unaffected.
Fallback is silent; genuine failures (control plane down, token rejected, bad template) surface. recap moves next.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
Sessions
690550a49bd7View transcript
Changes
4
cmd/entire/cli
Mactivity_cmd.go+1/-1
auth
Aentire_api.go+40
Aentireapi_client.go+111
Aentireapi_client_test.go+42
55 unmodified lines
56
57
58
59
59
60
61
62
55 unmodified lines
}
func runActivity(ctx context.Context, w, errW io.Writer) error {
return runAuthenticatedDataAPI(ctx, errW, false, func(ctx context.Context, client *api.Client) error {
return runAuthenticatedActivityAPI(ctx, errW, false, func(ctx context.Context, client *api.Client) error {
// Non-interactive fallback: piped output or accessibility mode
if !interactive.IsTerminalWriter(w) || IsAccessibleMode() {
return runActivityStatic(ctx, w, client)
Mcmd/entire/cli/activity_cmd.go+1/-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package auth
import (
"context"
"github.com/entireio/cli/cmd/entire/cli/api"
)
// ResolveEntireAPIToken mints a bearer for entire-api (the per-cell product
// API). entire-api verifies that the token's audience equals the jurisdiction
// it serves, so audienceOrigin must be the jurisdictional identity-token
// audience the caller derived from the repo's mirror and
// ENTIRE_API_AUDIENCE_TEMPLATE — NOT the entire-api host.
//
// Unlike ResolveDataAPIToken there is no /.well-known discovery (entire-api
// serves none): the audience is supplied explicitly and the token is exchanged
// from the active login context. That context's core must therefore be the one
// that signs tokens for this jurisdiction — true for single-core deployments;
// minting at a foreign jurisdiction's core (ENTIRE_CORE_BASE_URL_TEMPLATE) is a
// follow-up.
//
// Callers honouring --insecure-http-auth must call EnableInsecureHTTP first, as
// the data-API path does.
func ResolveEntireAPIToken(ctx context.Context, audienceOrigin string) (string, error) {
c, ok, err := activeContext()
if err != nil {
return "", err
}
if !ok {
return "", &reauthError{msg: "not logged in; run `entire login`", sentinel: ErrNotLoggedIn}
}
origin := api.OriginOnly(audienceOrigin)
allowInsecure := insecureHTTPEnabled() || isLoopbackHTTP(c.CoreURL)
provider, err := NewRefreshingResourceProvider(c, origin, nil, allowInsecure)
if err != nil {
return "", err
}
return provider(ctx)
}
Acmd/entire/cli/auth/entire_api.go+40
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
package cli
import (
"context"
"errors"
"fmt"
"io"
"strings"
"github.com/entireio/cli/cmd/entire/cli/api"
"github.com/entireio/cli/cmd/entire/cli/auth"
"github.com/entireio/cli/cmd/entire/cli/gitremote"
"github.com/entireio/cli/internal/coreapi"
)
// errRepoNotRoutable means the repo has no mirror carrying both a cell and a
// jurisdiction, so the CLI can't address its entire-api. It is not fatal:
// callers fall back to the data API.
var errRepoNotRoutable = errors.New("repo has no entire-api-routable mirror (missing cell/jurisdiction)")
// resolveCurrentRepoPlacement resolves the entire-api placement for the repo in
// the current working directory, via its "origin" remote. It lists the repo's
// mirrors through the control plane (c) and selects a routable one, returning
// errRepoNotRoutable when the repo is unmirrored or its mirrors predate
// cell/jurisdiction metadata.
func resolveCurrentRepoPlacement(ctx context.Context, c *coreapi.Client) (entireAPIPlacement, error) {
forge, owner, repo, err := gitremote.ResolveRemoteRepo(ctx, "origin")
if err != nil {
return entireAPIPlacement{}, fmt.Errorf("resolve current repo: %w", err)
}
provider, ok := forgeToMirrorProvider(forge)
if !ok {
return entireAPIPlacement{}, fmt.Errorf("unsupported forge %q for entire-api routing", forge)
}
mirrors, err := listMirrorsForRepo(ctx, c, provider, strings.ToLower(owner), repo)
if err != nil {
return entireAPIPlacement{}, fmt.Errorf("list mirrors for %s/%s: %w", owner, repo, err)
}
placement, ok := selectRoutablePlacement(mirrors, "")
if !ok {
return entireAPIPlacement{}, fmt.Errorf("%s/%s: %w", owner, repo, errRepoNotRoutable)
}
return placement, nil
}
// newEntireAPIClientForCurrentRepo builds an api.Client pointed at the entire-api
// cell hosting the current repo, carrying a jurisdictional identity token.
//
// ok=false with a nil error means "entire-api not in play": either the operator
// hasn't set the ENTIRE_API_BASE_URL_TEMPLATE / ENTIRE_API_AUDIENCE_TEMPLATE
// pair, or the repo has no routable mirror — so the caller falls back to the
data API. Genuine failures (control-plane unreachable, token exchange
// rejected, malformed template) return an error rather than silently masking a
// misconfigured opt-in.
func newEntireAPIClientForCurrentRepo(ctx context.Context, insecureHTTP bool) (*api.Client, bool, error) {
baseTemplate := api.EntireAPIBaseURLTemplate()
audTemplate := api.EntireAPIAudienceTemplate()
if baseTemplate == "" || audTemplate == "" {
return nil, false, nil
}
core, err := coreapi.New()
if err != nil {
return nil, false, fmt.Errorf("entire-api routing needs the control plane: %w", err)
}
placement, err := resolveCurrentRepoPlacement(ctx, core)
if err != nil {
if errors.Is(err, errRepoNotRoutable) {
return nil, false, nil
}
return nil, false, err
}
baseURL, err := api.BuildEntireAPIBaseURL(baseTemplate, placement.Cell)
if err != nil {
return nil, false, fmt.Errorf("build entire-api base URL: %w", err)
}
audience, err := api.BuildEntireAPIAudience(audTemplate, placement.Jurisdiction)
if err != nil {
return nil, false, fmt.Errorf("build entire-api audience: %w", err)
}
if insecureHTTP {
auth.EnableInsecureHTTP()
} else if err := api.RequireSecureURL(baseURL); err != nil {
return nil, false, fmt.Errorf("entire-api base URL check: %w", err)
}
token, err := auth.ResolveEntireAPIToken(ctx, audience)
if err != nil {
return nil, false, fmt.Errorf("resolve entire-api token: %w", err)
}
return api.NewClientWithBaseURL(token, baseURL), true, nil
}
// runAuthenticatedActivityAPI runs fn with an authenticated client for the
// activity/recap surface: the entire-api cell for the current repo when it is
// configured and the repo is routable, otherwise the data API. Both serve the
// same /me/* paths, so fn is agnostic to which client it receives.
func runAuthenticatedActivityAPI(ctx context.Context, errW io.Writer, insecureHTTP bool, fn func(context.Context, *api.Client) error) error {
client, ok, err := newEntireAPIClientForCurrentRepo(ctx, insecureHTTP)
if err != nil {
return renderDataAPIAuthError(errW, err)
}
if !ok {
return runAuthenticatedDataAPI(ctx, errW, insecureHTTP, fn)
}
return fn(ctx, client)
}
Acmd/entire/cli/entireapi_client.go+111
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package cli
import (
"context"
"testing"
"github.com/entireio/cli/cmd/entire/cli/api"
)
// When the entire-api templates are unset, the client builder must short-circuit
// to (nil, false, nil) — signalling "fall back to the data API" — without
// touching the control plane. This is the default path for every user who
// hasn't opted into entire-api routing.
func TestNewEntireAPIClientForCurrentRepo_UnconfiguredFallsBack(t *testing.T) {
// Cannot t.Parallel(): mutates process env via t.Setenv.
t.Setenv(api.EntireAPIBaseURLTemplateEnvVar, "")
t.Setenv(api.EntireAPIAudienceTemplateEnvVar, "")
client, ok, err := newEntireAPIClientForCurrentRepo(context.Background(), false)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if ok || client != nil {
t.Fatalf("got (client=%v, ok=%v), want (nil, false) when unconfigured", client, ok)
}
}
// Both templates are required: with only the base URL set, routing stays off
// (an audience-less token would be rejected by entire-api anyway).
func TestNewEntireAPIClientForCurrentRepo_RequiresBothTemplates(t *testing.T) {
// Cannot t.Parallel(): mutates process env via t.Setenv.
t.Setenv(api.EntireAPIBaseURLTemplateEnvVar, "https://{cell}.api.entire.io")
t.Setenv(api.EntireAPIAudienceTemplateEnvVar, "")
_, ok, err := newEntireAPIClientForCurrentRepo(context.Background(), false)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if ok {
t.Fatal("got ok=true with only the base template set, want false")
}
}