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:

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

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")
    }
}