Classify a cell's 401 as a credential rejection, not a raw error - #2124
Open
Soph wants to merge 1 commit into
Open
Classify a cell's 401 as a credential rejection, not a raw error#2124Soph wants to merge 1 commit into
Soph wants to merge 1 commit into
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
Improves semantic-search UX and diagnosability when an entire-api cell responds to query-serve but rejects the caller’s bearer (401/403), ensuring the CLI reports this as a credential rejection (with actionable guidance) rather than a raw upstream error or mistaken regional unavailability.
Changes:
- Add
search.ErrCellUnauthorizedand classify 401/403 responses from CellV4 as credential rejection while preserving status/body details for debug logs. - Track unauthorized cells separately during v4 semantic-search fan-out and prioritize “credentials rejected” when no cells return results; add partial-failure warnings naming rejecting regions.
- Update docs/comments to reflect that cell clients carry the login JWT (not per-jurisdiction minted identity tokens).
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| cmd/entire/cli/search/search.go | Adds ErrCellUnauthorized, classifies 401/403 in CellV4, and introduces serviceErrorMessage helper. |
| cmd/entire/cli/search/search_test.go | Adds coverage for 401/403 classification and repoints generic error JSON test to a non-auth status. |
| cmd/entire/cli/search_v4.go | Extends fan-out classification to track unauthorized cells and improves user-facing prioritization + warnings. |
| cmd/entire/cli/search_v4_test.go | Adds tests for all-cells-unauthorized, prioritization, and partial-unauthorized warnings. |
| cmd/entire/cli/cell_fanout.go | Updates comment/doc to match the current auth behavior (login JWT per cell client). |
| CLAUDE.md | Updates documentation to reflect login-JWT cell auth and clarifies the remaining jurisdiction-token path. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
An EU-home-jurisdiction account gets 401 from every entire-api cell's semantic-search route, and the CLI printed the service's body verbatim: search failed: semantic search: search service error (401): Unauthorized That message names no region, offers no next step, and invites the one action that cannot work — logging in again. The rejection is server-side and account-scoped (entirehq/entire-search#196): the same bearer is accepted by code search at the same host. No CLI change can make this search succeed — the index is server-side and there is no local substitute — so the error buys the user the only two things left: that retrying and re-authenticating are both dead ends, and the command that turns the dead end into a report someone can act on. It deliberately does not offer 'entire search --code' as a consolation: code search answers a different question, and pointing someone looking for session history at file matches reads as a fix while being none. CellV4 now classifies 401/403 as search.ErrCellUnauthorized, wrapping the status and the service's message so debug logs keep them. The fan-out tracks those cells separately from cells that lack query-serve, and when nothing answered, the credential rejection outranks every other explanation — it is the one outcome certain to repeat, and neither the repo nor the region message is true of it. Partial failures gain a warning naming the rejecting regions, since a rejection is not the transient regional failure the generic count implies. Deliberately NOT reported as regional unavailability, which cli#2121 suggests: that would mask a server bug as geography and promise an availability rollout that will never fix it. Also corrects comments (and the CLAUDE.md token rule) that still described cell clients as carrying minted per-jurisdiction identity tokens. 3df7ea4 removed that exchange; they carry the login JWT. The stale docs are what sent both the reporter and this investigation to audit token audiences. Refs: #2121, #1923, entirehq/entire-search#196 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 01M0WPHPWAW7GZG8J3NSQT4EAQ
Soph
force-pushed
the
soph/search-401-classification
branch
from
August 25, 2026 14:52
43ab3b3 to
09cfaa0
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
An account whose
home_jurisdictioniseugets a 401 from every cell's semantic-search route, and we printed the service's body verbatim (#2121):That names no region, offers no next step, and invites the one action that cannot possibly work — logging in again. The rejection is server-side and account-scoped (entirehq/entire-search#196); the same bearer is accepted by code search at the same host.
Now:
Setting expectations on how much this helps. It doesn't make the search work — the index is server-side and the CLI has no local substitute — so all it can buy the user is the two things left: that retrying and re-authenticating are both dead ends (the #2121 reporter spent days on exactly that reflex), and the one action that changes anything. An earlier draft offered
entire search --codeas a consolation; that is now asserted against in the tests. Code search answers a different question, and pointing someone looking for session history at file matches reads as a fix while being none.The thing that would actually help is a local fallback — #1923's second ask — and it is a feature, not a message. Worth knowing before anyone scopes it: local checkpoint trees reference blobs this clone does not have.
git grepoverentire/checkpoints/v1in this repo does find matches, but takes 30s+ and triggersupload-pack: not our reffetches, so a local search would silently answer from partial data. "No history" from a partial index is the #1923 failure mode all over again, which is why this PR does not fake one.What changed
search.ErrCellUnauthorized—CellV4classifies 401/403 the way it already classifies the two 404s, wrapping the status and the service's own message (%w (HTTP 401): Unauthorized) so debug logs keep the only clue to which auth stage refused. 403 joins 401: entire-search verifies a token and then declines it with 403 when the subject carries no account id.failed(coverage flags and accounting unchanged) but are tracked apart from cells that simply lack query-serve.This is the machine-readable distinction #1923 asks for, in the correct category. It is deliberately not reported as regional unavailability, which #2121 suggests as its fallback: that would mask a server bug as geography and promise an availability rollout that will never arrive. The existing
errNoRegionAvailablestill covers the real thing (a gateway 404 — query-serve genuinely not deployed).Why the reporter's primary suggestion isn't here
#2121 concludes the token's
aud=https://eu.entire.iomismatches theaudience: https://entire.ioinentire.io/.well-known/entire-api.json, and asks us to mint the advertised audience. Probing prod says otherwise:aud=https://eu.entire.ioaud=https://entire.io(the advertised one)Minting the advertised audience would break semantic search for everyone, and code search outright.
audis also never dialed, soeu.entire.iobeing NXDOMAIN is irrelevant —au/inare too, by design.Drive-by: stale token comments
Chasing that audience theory cost real time because our own comments — and the CLAUDE.md "Token rule" — still describe cell clients as carrying minted per-jurisdiction identity tokens.
3df7ea461("Use login JWTs directly for cell auth") removed that exchange;CellClientFactory.ClientForpassessubject.loginJWTstraight through, andCellTarget's jurisdiction now selects only the host. Four sites corrected (cell_fanout.go,search_v4.go,CellV4's doc, CLAUDE.md), noting that the minting path survives only behindentire auth token --jurisdiction.Happy to split this into its own PR if you'd rather keep the diff to the classification.
Testing
mise run check— fmt, lint (0 issues), unit + integration + canary all green.TestCellV4_Unauthorized(httptest, 401/403/raw-body, and that a 500 does not match);TestMergeSemanticV4Responses_AllCellsUnauthorized,_UnauthorizedOutranksOtherExplanations,_PartialUnauthorized.TestCellV4_ErrorJSONrepointed at a 500, so the generic wording stays pinned for every unclassified status.entire searchstill works normally (my account isus-home).Not in scope
Code search's 401 path is untouched. It already returns a typed
*api.HTTPError, and peregrine only 401s on a genuine audience mismatch — which would be our routing bug, a different class needing a different message.Three follow-ups I left alone: the older sentinels double the prefix (
semantic search: semantic search is not yet available …) — my message omits it, but fixing theirs changes strings #1923 quotes;entire doctorstill has no search-reachability check (#2121's third ask), which is where this error now points its users and so the natural next PR; and the local fallback discussed above.