Skip to content

Git smart-HTTP facade

The facade is the only surface end users ever touch. It is a JGit GitServlet registered at /git/*, with its own stateless security chain ordered ahead of the web chain.

Clients need no modification: it is an ordinary read-only git remote.

URLs

https://skills.corp.example/git/{marketplace}
https://skills.corp.example/git/{marketplace}.git

The standard smart-HTTP endpoints below that prefix:

  • GET /git/{marketplace}/info/refs?service=git-upload-pack
  • POST /git/{marketplace}/git-upload-pack

{marketplace} is validated against ^[a-z0-9][a-z0-9_-]*$ after stripping an optional .git suffix. Anything else is a 404 — which is also what blocks path traversal into arbitrary directories.

Authentication

HTTP Basic, backed solely by the PAT provider. An OIDC session can never authenticate a git fetch, because no OIDC provider is registered in this chain.

Only the password field is read. The username is ignored; token is the convention. This is what makes the standard git credential helper work unmodified.

$ git ls-remote https://token:sgw_...@skills.corp.example/git/acme
3f9c2ab...  refs/heads/main
sequenceDiagram
    participant G as git client
    participant F as gitChain (Order 1)
    participant PAT as PatAuthenticationProvider
    participant S as Git storage
    participant L as fetch_log

    G->>F: GET /git/acme/info/refs?service=git-upload-pack
    F-->>G: 401 + WWW-Authenticate Basic
    G->>F: retry with Basic token:sgw_...
    F->>PAT: authenticate (password only)
    PAT->>PAT: sha256Hex(token) → find active token
    alt no active token
        PAT-->>G: 401 bad credentials
    else valid
        PAT-->>F: principal + ROLE_GIT
        F->>S: publishedIfServing("acme")
        alt never approved
            S-->>G: 404 repository not found
        else serving
            S-->>F: published repo (read-only)
            F->>L: record info-refs
            F-->>G: advertise refs/heads/main
            G->>F: POST /git/acme/git-upload-pack
            F->>L: record upload-pack per wanted object
            F-->>G: packfile
        end
    end

An unauthenticated request gets 401 with WWW-Authenticate: Basic, which is exactly what the credential helper expects; a bad or revoked token gets 401 too.

The facade chain is unconditional

skills-gateway.dev-insecure-auth=true opens the web surface but does not touch /git/**. A valid PAT is still required.

What is served

Two namespaces, from {data-dir}/published/{marketplace}.git, and nothing else:

Advertised What it is
refs/heads/main The served tip. This is what a clone checks out.
refs/snapshots/<sha> Every approved snapshot, fetchable by name in its own right — including one a later approval has superseded, until it is revoked.

HEAD is advertised too, because a clone reads it to learn which branch to check out.

This is an allowlist, not a description of what the repositories happen to contain. Upload-pack advertises every ref it can see unless told otherwise, and every advertised tip is a legal want, so the facade states its surface explicitly: references the gateway keeps for its own purposes — the catalog's rebuild scaffolding, the staging namespace publication uses before it commits to serving anything — are never on the wire, whether or not the code that tidies them up succeeded.

Repository resolution opens only the published path and returns nothing unless refs/heads/main resolves. A marketplace that has been registered and ingested but never approved therefore returns 404: there is nothing to serve, and the quarantine repository is not reachable from here by any code path.

The served SHA changes only when a reviewer approves a snapshot.

Revocation removes both refs

Because a snapshot is fetchable by name as well as through main, taking one off the wire means removing both references. Revoking a snapshot that a later approval has already superseded removes only its own pinned reference and leaves the marketplace serving.

The facade accepts no writes

Receive-pack is disabled by construction — the servlet is configured with a null receive-pack factory, so no ReceivePack can be created at all. git push to /git/** receives the standard "service not enabled" rejection, whatever the credential.

There is no write-side endpoint, filter or hook on this servlet to misconfigure. This is a structural guarantee, not a policy one.

The gateway does accept a push, for marketplaces it hosts itself — on /publish/**, which is a different servlet resolving a different repository under a different token scope, and which cannot reach a published repository any more than this one can construct a ReceivePack. See ADR 0007 for why the two are separate objects rather than one with a mode flag.

Auditing

Two hooks append to the ledger:

Event When
info-refs Ref advertisement, recording the resolved main SHA.
upload-pack One entry per wanted object when the packfile is served.

Each entry carries the client address as source, the PAT's principal, the marketplace, the ref and the SHA. Negotiation rounds are not recorded.

Troubleshooting

Symptom Cause
401 on every request Token missing, mistyped, or revoked. Remember the value goes in the password field.
404 for a marketplace you registered No snapshot has ever been approved, so no published repository exists.
404 with an odd name The name failed ^[a-z0-9][a-z0-9_-]*$.
git push rejected Expected — the facade is read-only by construction. Publishing to a hosted marketplace goes to /publish/{name}.
Client keeps getting an old SHA Also expected. The published ref moves only on approval.