Skip to content

repo: a repo shared by direct grant now clones by its /et/ path - #2543

Merged
toothbrush merged 3 commits into
mainfrom
paul/clone-shared-repo-via-resolve
Sep 22, 2026
Merged

toothbrush merged 3 commits into
mainfrom
paul/clone-shared-repo-via-resolve

Conversation

@toothbrush

@toothbrush toothbrush commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

https://entire.io/gh/entireio/cli/trails/1388

Why

Share one private repo with one person and they cannot clone it:

$ entire grant repo add repo github:user2 --project proj --role reader   # user1
$ entire repo clone /et/proj/repo                                       # user2
permission denied

The grant is correct. The CLI resolves a /et/<project>/<repo> ref by looking the project up first (GET /projects?name=proj), and that route needs project#inspect. A direct repo grant confers repo#pull only, so core answers 403 before the repo is ever asked about. Logs today show user2 hitting exactly that 403 four times.

The only way through was to give the grantee read on the whole project, or hand them the raw entire:// URL.

What

Resolve the name pair through POST /repos/resolve instead. That route is gated on repo#pull, the permission cloning needs, and returns the repo ULID in one call. GetRepo (also pull-gated) still supplies clusterHost and path. No server change.

Every /et/ ref shares the helper, so repo view, delete, visibility, protection, grant, and the cell-routing commands (trail, search, …) are fixed with clone. Two round trips instead of three. A repo that does not resolve reports one thing, because the server answers unknown and unshared alike:

repo /et/proj/repo not found or not shared with you

Only --project <ULID> with a bare repo name still uses the project-scoped listing, since there is no ULID→name route.

Verification

  • go test ./cmd/entire/cli/ green. The fixtures now 403 any /projects call a native ref makes, which is the regression pin.
  • mise run lint:go, lint:gofmt, lint:gomod, dup clean. lint:shellcheck fails on untouched scripts with local shellcheck 0.11; unrelated.
  • Manual: POST /api/v1/repos/resolve {"repositories":[{"provider":"entire","fullName":"proj/repo"}]} against prod returns status: ready, the repo ULID and its placement.

🤖 Generated with Claude Code


Note

Medium Risk
Touches shared repo-ref resolution used by clone, grants, and cell routing; behavior change for permission boundaries but aligned with server pull-gated API and heavily test-pinned.

Overview
Fixes direct repo grantees who could not use /et/<project>/<repo> because the CLI resolved names via project-scoped APIs that require project#inspect, while grants only confer repo#pull.

Native <project>/<repo> pairs now resolve in one POST /repos/resolve call (resolveNativeRepoByPath), shared by clone, grant, cell routing, and other resolveRepoRef / resolveRepoPath callers. That drops a round trip versus the old project→repo chain and avoids hitting the forge-blind repos index for /et/ paths. Misses surface as repo /et/... not found or not shared with you when the server returns unavailable (unknown vs unshared are intentionally indistinguishable). --project <ULID> + bare repo name still uses ListProjectRepos because the path API expects names, not project ids.

Tests and grant wiring fixtures now stub /repos/resolve and 403 any accidental /projects lookup as the regression pin.

Reviewed by Cursor Bugbot for commit f694a33. Configure here.

A repo shared with one person by a direct repo grant could not be
cloned by that person: `entire repo clone /et/<project>/<repo>` looked
the project up first (GET /projects?name=), which needs project#inspect,
and a repo-only grant does not confer it. The server answered 403 and
the CLI printed "permission denied" before it ever asked about the repo.

Resolve the name pair through POST /repos/resolve instead. That route is
gated on repo#pull, the permission cloning needs, and returns the repo
ULID in one call. GetRepo (also pull-gated) still supplies clusterHost
and path. Every /et/ ref command goes through the same helper, so view,
delete, visibility, protection, grant, and the cell-routing commands are
fixed with clone. Only a --project ULID with a bare name still takes the
project-scoped listing, since there is no ULID-to-name route.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@toothbrush
toothbrush requested a review from a team as a code owner September 21, 2026 12:28
Copilot AI lite review requested due to automatic review settings September 21, 2026 12:28
@toothbrush

toothbrush commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

Full unit run on the branch head (go test ./... -count=1): 30 packages ok, exit 0, no failures. Lint: mise run lint:go (golangci-lint 2.13.2) 0 issues; lint:gofmt, lint:gomod, dup clean.

Server-side proof for the case in the description, from Datadog service:entire-core-regional-us today (account user2, reader grant on /et/proj/repo):

10:14:41Z  GET /api/v1/projects  403
10:15:04Z  GET /api/v1/projects  403
10:19:52Z  GET /api/v1/projects  403
10:22:03Z  GET /api/v1/projects  403

Every POST /api/v1/repos/resolve from the same account in the same window was 200.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

It changes a central repo-ref resolution path used by multiple commands and permission boundaries, so a final human verification of all affected flows is warranted.

Review effort: Lite
Findings: None

What changed in this PR

This PR fixes cloning (and other repo-ref commands) for repositories shared via direct repo grants by changing /et/<project>/<repo> resolution from project-scoped lookups (requiring project#inspect) to the pull-gated POST /repos/resolve, then using GetRepo only where additional repo metadata is needed.

Changes:

  • Replace native /et/<project>/<repo> name resolution with POST /repos/resolve via a shared helper (resolveNativeRepoByPath) to avoid project permission gating.
  • Update repo ref agreement-check behavior so --project <ULID> alongside a native path validates via GetRepo (owning project id), while --project <name> is checked before any request.
  • Adjust and extend fixtures/tests to pin the regression (native refs must not hit /projects lookups; “unavailable” resolves to a single “not found or not shared” message).
File Description
docs/​development/​cli-conventions.md Updates documentation to reflect the new pull-gated native ref resolution flow and agreement checks.
cmd/​entire/​cli/​resolveref.go Implements native repo path resolution via POST /repos/resolve and updates path/project agreement logic.
cmd/​entire/​cli/​resolveref_test.go Reworks resolver fixtures to serve /repos/resolve, forbids project lookups, and adds coverage for “unavailable” behavior.
cmd/​entire/​cli/​repo_clone.go Routes native clone resolution through the new path lookup helper to avoid ULID-shaped name pitfalls.
cmd/​entire/​cli/​repo_clone_test.go Updates native clone fixture to use /repos/resolve and removes now-invalid project-scoped endpoints.
cmd/​entire/​cli/​grant_wiring_test.go Updates grant wiring fixture to resolve /et/ refs via /repos/resolve.
cmd/​entire/​cli/​cell_target.go Aligns native cell placement resolution commentary and uses the shared native resolver client surface.
cmd/​entire/​cli/​cell_target_test.go Refactors fake core to support /repos/resolve and explicitly fail any project-scoped lookup attempts for native refs.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

toothbrush and others added 2 commits September 21, 2026 23:03
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A bare repo name under a non-ULID --project resolves through
repos/resolve, not the project-scoped listing. Nothing drove that
branch: the name subtests pass a project ULID and the path subtests
enter through resolveRepoPathRef. Add the hit and the miss so routing
it back onto /projects fails CI.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Entire-Checkpoint: 01M333AD4M204170HYY9JAYKBE
@toothbrush
toothbrush merged commit e6f32ce into main Sep 22, 2026
16 checks passed
@toothbrush
toothbrush deleted the paul/clone-shared-repo-via-resolve branch September 22, 2026 13:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants