Files
hanzo-dev 811ff08010 git: a project-scoped repo names its project in the path
The project sub-scope rode X-Project-Id alone, and a git client sends no
headers, so a repo outside the org's default scope had no remote a client could
reach: cloneURL emitted /v1/git/<org>/<name>.git for every repo, and
resolvePackRepo dropped the scope entirely for anonymous reads.

Smart-HTTP and SSH both take the scope as an optional middle segment —
/v1/git/:org/:project/:repo and git@host:org/project/repo.git — beside the
existing two-segment routes, which keep their exact meaning. cloneURL and sshURL
advertise whichever form matches the repo, so a caller is never told a URL that
does not work.

The path wins over the header when both are present, because the path is what a
client can express. An anonymous caller may use it: naming a project addresses a
repo rather than asserting a scope, and the repo's Public flag still decides the
read, whereas an unauthenticated X-Project-Id stays unvalidated input and is
ignored as before. The segment is checked against projectRE, since it becomes a
storage path segment.

This is what lets one Hanzo org hold repos from several GitHub owners:
hanzo/hanzo-apps/ai and hanzo/hanzo-docs/ai are distinct repos rather than two
upstreams fighting over hanzo/_/ai.git.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-30 20:25:10 -07:00

48 lines
2.2 KiB
Go

package git
import "context"
// export.go is git's in-process seam for the coding-agent orchestrator
// (clients/coding). git already imports clients/integrations (notify.go), and
// integrations imports clients/agents — so coding, which integrations calls, can
// NOT import git without a cycle (integrations -> coding -> git -> integrations).
// The composition root wires these two read-only functions into coding's CloneURL
// / VerifyRef seams instead, so coding COMPOSES the git host + a post-push ref
// check without importing the package. Both read the mounted service and are
// fail-safe before Mount (empty / not-found), never panicking on a nil singleton.
// CloneURL returns the HTTPS smart-HTTP clone URL for an org's repo
// (https://<domain>/v1/git/<org>/<repo>.git) — the exact URL the git handlers
// serve. Empty when git is not mounted. The credential is NEVER embedded here;
// the sandbox presents it out of band (env-fed http.extraHeader), so this URL is
// safe to log and to hand to a subprocess on argv.
func CloneURL(org, name string) string {
s := mounted.Load()
if s == nil {
return ""
}
return cloneURL(s, org, "", name)
}
// VerifyRef reports the tip commit of branch in an org's repo, reading the on-disk
// bare repo directly (the shared git storage every cloud replica mounts). It is the
// independent, in-process confirmation that a branch a sandbox claims to have pushed
// actually LANDED in native git — cloud trusts the branch tips it can read, not the
// remote runner's self-report. ok is false when git is unmounted, the repo/branch is
// absent, or the read fails (fail-closed: an unverifiable ref is treated as absent).
func VerifyRef(ctx context.Context, org, repo, branch string) (sha string, ok bool) {
s := mounted.Load()
if s == nil || org == "" || repo == "" || branch == "" {
return "", false
}
// git is org-scoped at project "" (storeFor uses ""), so the bare repo lives at
// the org/"" /repo path — the same absRepoPath the pack handlers operate on.
bareDir := s.State.storage.absRepoPath(org, "", repo)
tips := branchTips(ctx, bareDir)
tip, present := tips[branch]
if !present || tip == "" {
return "", false
}
return tip, true
}