Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,17 @@ Experimental command families advertised through `entire labs`:

Top-level lifecycle and standalone commands: `enable`, `disable`, `status`,
`login`, `logout`, `clean`, `version`, `dispatch`, `activity`, `help`,
`configure`, `agent-help`.
`configure`, `agent-help`, `api`.

`api` is an authenticated passthrough to Entire's HTTP APIs (gh-style): it
attaches the right bearer and dials the right host so callers don't plumb auth
themselves. `--to core` (default) hits the control plane; `--to cell` hits an
entire-api cell. `--jurisdiction <slug>` (e.g. `us`, `eu`) targets a specific
jurisdiction's cell instead of the caller's home cell and implies `--to cell`
(cell routing + identity-token exchange live in `auth.NewEntireAPICellClient`
via `auth.CellTarget`). `{owner}`/`{repo}`/`{repo_id}` in the path are filled
from the current repo's origin remote. It is visible in `entire help` and
`entire agent-help`, so agents discover it as the supported way to call the API.

`agent-help` renders machine-readable, agent-facing usage live from the Cobra
command tree (so it always matches the installed binary): bare prints a
Expand Down
61 changes: 52 additions & 9 deletions cmd/entire/cli/api_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,16 @@ import (

const apiMaxResponseBytes = 32 << 20 // 32 MiB cap on a printed response body.

// The two --to backends: the control plane (core) and a per-jurisdiction
// entire-api cell.
const (
apiTargetCore = "core"
apiTargetCell = "cell"
)

type apiFlags struct {
to string
jurisdiction string
method string
rawFields []string
typedFields []string
Expand All @@ -45,21 +53,25 @@ func newAPICmd() *cobra.Command {
"chosen backend, so you don't have to plumb auth yourself:\n\n" +
" --to core the control plane (default): orgs, repos, mirrors, clusters, /me\n" +
" --to cell your home entire-api cell: /me/* activity, repo aggregates\n\n" +
"Use --jurisdiction <slug> (e.g. us, eu) to reach a specific jurisdiction's\n" +
"entire-api cell instead of your home one; it implies --to cell.\n\n" +
"<path> is the full path on that host, e.g. /api/v1/clusters. These\n" +
"placeholders are filled from the current repo's origin remote:\n" +
" {owner} {repo} the GitHub owner / repo\n" +
" {repo_id} the repo's Entire ULID (from its mirror) — cells key on this\n\n" +
"The method is GET unless a field/body is given (then POST); override with -X.",
Example: " entire api /api/v1/clusters\n" +
" entire api --to cell /api/v1/me/activity\n" +
" entire api --jurisdiction eu /api/v1/me/activity\n" +
" entire api --to cell \"/api/v1/me/recap?repo={repo_id}\"\n" +
" entire api -X POST /api/v1/projects -f name=demo",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return runAPI(cmd.Context(), cmd.OutOrStdout(), cmd.ErrOrStderr(), args[0], f)
return runAPI(cmd.Context(), cmd.OutOrStdout(), cmd.ErrOrStderr(), args[0], f, cmd.Flags().Changed("to"))
},
}
cmd.Flags().StringVar(&f.to, "to", "core", "which backend to call: core or cell")
cmd.Flags().StringVar(&f.to, "to", apiTargetCore, "which backend to call: core or cell")
cmd.Flags().StringVarP(&f.jurisdiction, "jurisdiction", "j", "", "target a specific jurisdiction's cell (e.g. us, eu) instead of your home cell; implies --to cell")
cmd.Flags().StringVarP(&f.method, "method", "X", "", "HTTP method (default GET, or POST when a field/body is given)")
cmd.Flags().StringArrayVarP(&f.rawFields, "raw-field", "f", nil, "add a string parameter in key=value format (repeatable)")
cmd.Flags().StringArrayVarP(&f.typedFields, "field", "F", nil, "add a typed parameter in key=value format; true/false/null/numbers are converted (repeatable)")
Expand All @@ -70,10 +82,15 @@ func newAPICmd() *cobra.Command {
return cmd
}

func runAPI(ctx context.Context, w, errW io.Writer, rawPath string, f *apiFlags) error {
func runAPI(ctx context.Context, w, errW io.Writer, rawPath string, f *apiFlags, toExplicit bool) error {
insecure := applyInsecureHTTPAuth(f.insecureHTTP)

client, err := resolveAPIClient(ctx, f.to, insecure)
to, jurisdiction, err := resolveAPITarget(f, toExplicit)
if err != nil {
return err
}

client, err := resolveAPIClient(ctx, to, jurisdiction, insecure)
if err != nil {
return err
}
Expand Down Expand Up @@ -110,12 +127,34 @@ func runAPI(ctx context.Context, w, errW io.Writer, rawPath string, f *apiFlags)
return writeAPIResponse(w, errW, resp, f.include)
}

// resolveAPITarget applies the --jurisdiction/--to interplay. --jurisdiction
// selects a specific jurisdiction's entire-api cell, so it implies --to cell;
// combining it with an explicit --to core is a contradiction and is rejected.
// The returned jurisdiction is normalized to a bare lowercase slug (e.g. "US"
// or " us " -> "us"); NewEntireAPICellClient validates it as a DNS label.
func resolveAPITarget(f *apiFlags, toExplicit bool) (to, jurisdiction string, err error) {
to = f.to
jurisdiction = strings.ToLower(strings.TrimSpace(f.jurisdiction))
if jurisdiction == "" {
return to, "", nil
}
switch {
case !toExplicit:
to = apiTargetCell // --jurisdiction targets a cell; imply it over the default core.
case strings.ToLower(strings.TrimSpace(f.to)) != apiTargetCell:
return "", "", fmt.Errorf("--jurisdiction targets a cell; use --to cell (not --to %s)", f.to)
}
return to, jurisdiction, nil
}

// resolveAPIClient builds an authenticated client for the chosen backend. Both
// return an *api.Client whose base URL is the backend origin, so <path> is the
// full path (e.g. /api/v1/…) against that host.
func resolveAPIClient(ctx context.Context, to string, insecure bool) (*api.Client, error) {
// full path (e.g. /api/v1/…) against that host. jurisdiction, when non-empty,
// pins the entire-api cell to that jurisdiction (cell path only; resolveAPITarget
// guarantees it is empty for core).
func resolveAPIClient(ctx context.Context, to, jurisdiction string, insecure bool) (*api.Client, error) {
switch strings.ToLower(strings.TrimSpace(to)) {
case "", "core":
case "", apiTargetCore:
target, err := resolveAuthStatusTarget(ctx, auth.Contexts, auth.RefreshedLoginToken)
if err != nil {
return nil, err
Expand All @@ -131,8 +170,12 @@ func resolveAPIClient(ctx context.Context, to string, insecure bool) (*api.Clien
}
}
return api.NewClientWithBaseURL(target.token, target.coreURL), nil
case "cell":
client, err := auth.NewEntireAPICellClient(ctx, insecure, nil)
case apiTargetCell:
var target *auth.CellTarget
if jurisdiction != "" {
target = &auth.CellTarget{Jurisdiction: jurisdiction}
}
client, err := auth.NewEntireAPICellClient(ctx, insecure, target)
Comment thread
Soph marked this conversation as resolved.
if err != nil {
return nil, err //nolint:wrapcheck // NewEntireAPICellClient already returns contextual auth errors
}
Expand Down
38 changes: 37 additions & 1 deletion cmd/entire/cli/api_cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,47 @@ func TestValidateAPIPath(t *testing.T) {

func TestResolveAPIClient_UnknownTarget(t *testing.T) {
t.Parallel()
if _, err := resolveAPIClient(context.Background(), "banana", false); err == nil {
if _, err := resolveAPIClient(context.Background(), "banana", "", false); err == nil {
t.Error("expected error for unknown --to")
}
}

func TestResolveAPITarget(t *testing.T) {
t.Parallel()

for _, tc := range []struct {
name string
flags apiFlags
toExplicit bool
wantTo string
wantJuris string
wantErr bool
}{
// No --jurisdiction: --to is passed through untouched.
{"default", apiFlags{to: apiTargetCore}, false, apiTargetCore, "", false},
// --jurisdiction with default --to: implies cell, slug normalized to lowercase.
{"implied cell", apiFlags{to: apiTargetCore, jurisdiction: " US "}, false, apiTargetCell, "us", false},
// --jurisdiction with explicit --to cell: allowed.
{"explicit cell", apiFlags{to: apiTargetCell, jurisdiction: "eu"}, true, apiTargetCell, "eu", false},
// --jurisdiction with explicit --to core: contradiction, rejected.
{"contradiction", apiFlags{to: apiTargetCore, jurisdiction: "eu"}, true, "", "", true},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
to, j, err := resolveAPITarget(&tc.flags, tc.toExplicit)
if tc.wantErr {
if err == nil {
t.Fatalf("resolveAPITarget(%+v) = (%q, %q, nil), want error", tc.flags, to, j)
}
return
}
if err != nil || to != tc.wantTo || j != tc.wantJuris {
t.Fatalf("resolveAPITarget(%+v) = (%q, %q, %v), want (%q, %q, nil)", tc.flags, to, j, err, tc.wantTo, tc.wantJuris)
}
})
}
}

func TestWriteAPIResponse(t *testing.T) {
t.Parallel()

Expand Down
24 changes: 22 additions & 2 deletions cmd/entire/cli/auth/cell_data_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,13 +208,33 @@ func resolveTargetCellBaseURL(ctx context.Context, target *CellTarget, dataOrigi
if target != nil && strings.TrimSpace(target.BaseURL) != "" {
return strings.TrimRight(target.BaseURL, "/"), nil
}
if !isBFFOrigin(dataOrigin) {
// Already a cell URL, or a loopback local-dev host: keep it verbatim.
// The configured origin is kept verbatim when it isn't a BFF/apex fronting
// multiple cells — i.e. it's already a direct cell or a loopback dev host —
// EXCEPT when a jurisdiction is explicitly pinned (target.Jurisdiction, e.g.
// `entire api --jurisdiction eu`) against a non-loopback origin. A pinned
// jurisdiction may name a DIFFERENT cell than the configured direct-cell
// origin, so dialing that origin verbatim would send an identity token minted
// for the pinned jurisdiction to the wrong cell; resolve the pinned
// jurisdiction's own cell from the catalog instead. A loopback dev host serves
// a single cell with no jurisdiction catalog, so it always stays verbatim.
explicitJurisdiction := target != nil && strings.TrimSpace(target.Jurisdiction) != ""
if !isBFFOrigin(dataOrigin) && (!explicitJurisdiction || isLoopbackOrigin(dataOrigin)) {
return strings.TrimRight(dataOrigin, "/"), nil
}
return resolveCellAPIBaseURL(ctx, coreURL, loginJWT, jurisdiction, httpClient)
}

// isLoopbackOrigin reports whether origin's host is a loopback address, at any
// scheme (isLoopbackHTTP only accepts http). Used to keep a local-dev cell
// verbatim even when a jurisdiction is explicitly pinned.
func isLoopbackOrigin(origin string) bool {
u, err := url.Parse(origin)
if err != nil {
return false
}
return isLoopbackHost(strings.ToLower(u.Hostname()))
}

// isBFFOrigin reports whether origin is a BFF / apex host that fronts multiple
// cells (so the actual cell must be resolved from the cluster catalog), as
// opposed to a direct entire-api cell (host contains ".api.") or a loopback
Expand Down
20 changes: 20 additions & 0 deletions cmd/entire/cli/auth/cell_data_api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,26 @@ func TestResolveTargetCellBaseURL(t *testing.T) {
if got, err := resolveTargetCellBaseURL(ctx, &CellTarget{BaseURL: "https://eu.api.entire.io/"}, "https://entire.io", "eu", "https://eu.auth.entire.io", "login", nil); err != nil || got != "https://eu.api.entire.io" {
t.Fatalf("target override: got %q, %v", got, err)
}
// A loopback origin with an explicitly pinned jurisdiction stays verbatim:
// local dev serves a single cell with no jurisdiction catalog to consult.
if got, err := resolveTargetCellBaseURL(ctx, &CellTarget{Jurisdiction: "us"}, "http://127.0.0.1:8099", "us", "http://127.0.0.1:9000", "login", nil); err != nil || got != "http://127.0.0.1:8099" {
t.Fatalf("loopback + explicit jurisdiction: got %q, %v", got, err)
}
// A non-loopback DIRECT cell origin with an explicitly pinned jurisdiction
// must NOT be dialed verbatim (it may name a different jurisdiction's cell):
// resolve the pinned jurisdiction's own cell from the catalog instead.
catalog := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != clustersAPIPath {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = fmt.Fprint(w, `{"clusters":[{"jurisdiction":"eu","isDefault":true,"apiUrl":"https://eu.api.entire.io"}]}`)
}))
defer catalog.Close()
if got, err := resolveTargetCellBaseURL(ctx, &CellTarget{Jurisdiction: "eu"}, "https://aws-us-east-2.api.entire.io", "eu", catalog.URL, "login", catalog.Client()); err != nil || got != "https://eu.api.entire.io" {
t.Fatalf("direct cell + explicit jurisdiction: got %q, %v (want catalog-resolved eu cell)", got, err)
}
}

// TestNewEntireAPICellClient_TargetRoutesToRepoCell proves the repo-scoped path:
Expand Down
Loading