cli: cell-routing foundation — client factory, generic repo→cell resolver, multi-cell fan-out#1641
cli: cell-routing foundation — client factory, generic repo→cell resolver, multi-cell fan-out#1641Soph wants to merge 4 commits into
Conversation
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 73b1b2e. Configure here.
There was a problem hiding this comment.
Pull request overview
This PR extracts and generalizes the CLI’s entire-api cell-routing plumbing into shared helpers, enabling future multi-cell commands (e.g., code search) to fan out across cells without duplicating routing/auth logic. It introduces a reusable cell client factory to avoid repeated subject resolution and redundant identity-token exchanges during multi-cell operations, and adds a generic repo→cell resolver plus fan-out grouping/join logic aligned with the BFF routing model.
Changes:
- Introduces
auth.CellClientFactoryto build cell clients from one resolved subject and cache identity tokens per jurisdiction. - Generalizes the repo-scoped routing resolver into
resolveRepoCellTargetand adds multi-cell fan-out helpers (groupReposByCell,resolveCellBaseURLs,fanOutCells). - Adds unit tests for token reuse, deterministic grouping/join behavior, and per-cell failure isolation; documents the three routing shapes in
CLAUDE.md.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| cmd/entire/cli/entireapi_client.go | Updates timeout comment reference to the generalized cell resolver timeout. |
| cmd/entire/cli/corecmd_test.go | Adds shared writeJSON helper for corecmd test stub handlers. |
| cmd/entire/cli/cell_target.go | Renames/generalizes experts-specific repo→cell routing into shared resolver + interfaces. |
| cmd/entire/cli/cell_target_test.go | Updates resolver tests to the generalized names; shares test constants/stubs. |
| cmd/entire/cli/cell_fanout.go | Adds new multi-cell fan-out foundation: grouping, catalog join, parallel fan-out. |
| cmd/entire/cli/cell_fanout_test.go | Adds tests for grouping determinism, correct catalog join key, fan-out semantics. |
| cmd/entire/cli/auth/cell_data_api.go | Refactors NewEntireAPICellClient to use CellClientFactory; adds per-jurisdiction token caching. |
| cmd/entire/cli/auth/cell_data_api_test.go | Adds coverage pinning “one token per jurisdiction” reuse behavior via the factory. |
| cmd/entire/cli/api_client.go | Switches repo-scoped client construction to resolveRepoCellTarget. |
| CLAUDE.md | Documents the three cell-routing shapes and the per-jurisdiction token rule. |
73b1b2e to
c20a5dc
Compare
32e8a17 to
86a21a8
Compare
c20a5dc to
48d5ac5
Compare
evisdren
left a comment
There was a problem hiding this comment.
minor comment from claude, otherwise good:
In tokenFor (cell_data_api.go), slot.mu.Lock() is held while exchangeJurisdictionToken makes an HTTP call. The comment on CellClientFactory.mu says "never
held across I/O", but mintSlot.mu is held across I/O — that's the design (single-flight per jurisdiction). This is fine for correctness and the intent is
correct (block concurrent callers for the same jurisdiction while one mints), but the comment on CellClientFactory.mu could mislead readers into thinking
no mutex in the struct is held across I/O. Consider clarifying that the factory-level mu is never held across I/O, while slot-level mu intentionally is.
File: cmd/entire/cli/auth/cell_data_api.go (around tokenFor)```
…tion NewEntireAPICellClient resolved the stored login subject (discovery + login refresh) and ran the RFC 8693 exchange on every call. For a single-cell command that's fine, but a multi-cell fan-out (one request per cell hosting the caller's repos, the BFF's code-search pattern) would pay all of it once per cell — even though identity tokens are per-jurisdiction, not per-cell: every cell in a jurisdiction accepts the same token. CellClientFactory resolves the subject once at construction and caches minted identity tokens by jurisdiction; ClientFor(target) reuses them across cells. NewEntireAPICellClient stays as the single-cell wrapper (factory of one), so existing callers are unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Entire-Checkpoint: 935f91398e25
resolveExpertsCellTarget was experts-named but nothing in it is experts-specific: it maps any repo (ULID or owner/repo) to the entire-api cell hosting it, exactly what every future repo-scoped or fan-out command needs (code search is next, PR #1616). Rename the file and identifiers — resolveRepoCellTarget, cellCoreClient, newCellCoreClient, cellResolveTimeout — and keep experts as the documented example consumer. Mechanical rename; no behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Entire-Checkpoint: 89b4d13bd2b0
The data plane has no server-side cross-cell aggregator: a query over all of the caller's repos must be fanned out to each cell hosting them and merged client-side (the entire.io BFF's code-search pattern). PR #1616 inlines that orchestration into search_cmd.go; this extracts the generic layer so code search — and any later repo-set command — shares one implementation: - groupReposByCell: repo index → per-cell groups, deterministic order. - resolveCellBaseURLs: cluster-catalog join on ClusterSlug↔Cluster.Slug. The catalog exposes no cell field, so joining the cell name against Slug (as #1616 currently does) only works when the two coincide. - fanOutCells: parallel per-cell calls under a per-cell timeout, one shared auth.CellClientFactory (subject resolved once, one identity token per jurisdiction), partial failures isolated per result slot. Merge semantics stay with the command. Also documents the three cell-routing shapes in CLAUDE.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Entire-Checkpoint: 842e421e74a8
- Bound resolveCellBaseURLs's catalog lookup with cellResolveTimeout, like resolveRepoCellTarget: a hung core must not stall the command before the fan-out even starts. - Refuse a concrete baseURL without a jurisdiction: minting a home-jurisdiction token and dialing a foreign cell with it is exactly the mismatch resolveRepoCellTarget refuses; such a group stays on home routing. - Key groupReposByCell on (cell, jurisdiction): blank-cell index rows in different jurisdictions no longer collapse into one group routed by whichever repo came first. - Single-flight token mints per jurisdiction (mintSlot) instead of one factory-wide mutex held across the exchange: a slow region's mint no longer burns other cells' per-cell deadlines. Failed mints cache nothing, so the next caller retries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Entire-Checkpoint: ab23b05bf985
9ef8a5c to
299b273
Compare

https://entire.io/gh/entireio/cli/trails/762
Stacked on #1592. Extracts the CLI's cell-routing plumbing into a shared foundation so multi-cell commands (code search, #1616, and anything after it) stop growing parallel copies of it. The backend model gives the CLI exactly three routing shapes, mirroring the entire.io BFF: repo-scoped → one cell, user-scoped /me → home cell, repo-set queries → fan out and merge client-side (no server-side aggregator exists). #1592 covers the second; this PR generalizes the first and builds the third.
Commits
auth.CellClientFactory—NewEntireAPICellClientresolved the login subject (discovery + refresh) and ran the RFC 8693 exchange on every call. Identity tokens are per-jurisdiction, not per-cell, so a fan-out over N cells was about to pay N× all of it. The factory resolves the subject once and mints at most one token per jurisdiction;NewEntireAPICellClientstays as the single-cell wrapper, existing callers unchanged.cell_target.go— mechanical generalization ofexperts_cell_target.go(resolveRepoCellTarget,cellCoreClient,cellResolveTimeout): nothing in it was experts-specific, and it's the repo-scoped shape every cell-routed command needs. No behavior change.cell_fanout.go— the repo-set shape, extracted from what cli: add code search with multi-region fan-out (ENT-993) #1616 currently inlines intosearch_cmd.go:groupReposByCell: repo index → per-cell groups, deterministic order.resolveCellBaseURLs: cluster-catalog join onClusterSlug↔Cluster.Slug. The catalog exposes no cell field — cli: add code search with multi-region fan-out (ENT-993) #1616's current join of the cell name againstSlugonly works when the two happen to coincide; a test pins the correct key.fanOutCells: parallel per-cell calls under a per-cell timeout through one sharedCellClientFactory; partial failures are isolated per result slot, merge semantics stay with the command.Intended consumer
#1616 (code search) keeps its
codesearchclient, merge/sort/dedup semantics, and output rendering, and replaces its inlinegroupReposByCell/searchAllCells/searchCellorchestration with these helpers — fixing the slug join and the per-cell token minting along the way.Testing
mise run test:cigreen (unit + integration + both canary suites),-raceon the fan-out tests. New tests: factory token reuse per jurisdiction, group determinism, slug-join pinning, per-cell failure isolation.🤖 Generated with Claude Code
Note
Medium Risk
Touches authentication (RFC 8693 exchange caching) and cross-region API routing; behavior is largely refactored with tests, but fan-out and factory concurrency affect how multi-cell commands authenticate and fail partially.
Overview
Introduces shared entire-api cell routing so multi-cell commands can reuse one plumbing layer instead of duplicating BFF-style routing.
Auth:
NewEntireAPICellClientnow delegates toCellClientFactory, which resolves the login subject once and caches one jurisdictional identity token per jurisdiction (not per cell).NewEntireAPICellClientremains the single-cell entry point; existing callers are unchanged.Repo-scoped routing: Experts-style resolution is renamed and generalized to
resolveRepoCellTarget(fromresolveExpertsCellTarget), with sharedcellCoreClient/cellResolveTimeoutnaming. Behavior is unchanged;NewAuthenticatedEntireAPICellClientcalls the new resolver.Repo-set routing: New
cell_fanout.goprovidesgroupReposByCell,resolveCellBaseURLs(catalog join onClusterSlug↔Cluster.Slug, not cell name), and genericfanOutCells(parallel per-cell calls, per-cell timeout, isolated partial failures, one factory per operation).Docs:
CLAUDE.mddocuments the three routing shapes (repo-scoped,/mehome cell, repo-set fan-out) and the per-jurisdiction token rule.Minor: shared
writeJSONtest helper incorecmd_test.go; comment tweak inentireapi_client.gofor timeout naming.Reviewed by Cursor Bugbot for commit 73b1b2e. Configure here.