# ZergHub API for agents

Base URL: `https://hub.zergstack.com`. This reference lists what an agent can
call with the credentials that exist today, and states plainly what it cannot.
Setup, secure configuration and limits are in https://hub.zergstack.com/agents.md;
push rules are at https://hub.zergstack.com/docs/push-policy.

## Authentication

An agent's credential is a personal access token (`zhp_…`) that a signed-in
person creates at https://hub.zergstack.com/settings/tokens, with the scopes
`repo:read` (always) and `repo:write` (optional). It works on git smart HTTP
only, sent as either:

- `Authorization: Bearer zhp_…`, or
- `Authorization: Basic base64(<any user name>:zhp_…)`, the form git's
  credential helpers send. The token may also stand in the user name.

Keep the token in the agent host's secret manager and hand it to git through
its environment (see agents.md). Never put it in a URL, a command line or a log.

The REST API under `/api/v1` accepts browser sessions only. A request that
carries an `Authorization` header is never treated as signed in, so a token on
any `/api/v1` route gets `401` with a JSON body whose `data.code` is
`authentication_required`.

## Git over HTTPS

Git's smart HTTP protocol (protocol v2 for fetches when the client asks with
`Git-Protocol: version=2`; pushes use v0). `{owner}` is a namespace: 2 to 39
lower-case letters, digits and single hyphens, starting and ending with a
letter or digit. `{repo}` is a repository name: 1 to 100 lower-case letters,
digits, `.`, `-` and `_`, starting and ending with a letter or digit. The
`.git` suffix is optional. Any other name answers 404. Every answer below 400 comes from git itself.

| Method | Path | Auth | Request | Response |
| --- | --- | --- | --- | --- |
| GET | `/{owner}/{repo}.git/info/refs?service=git-upload-pack` | Token with `repo:read` | Optional `Git-Protocol: version=2` | 200 `application/x-git-upload-pack-advertisement` |
| GET | `/{owner}/{repo}.git/info/refs?service=git-receive-pack` | Token with `repo:write` and write permission; standard repositories only | none | 200 `application/x-git-receive-pack-advertisement` |
| POST | `/{owner}/{repo}.git/git-upload-pack` | Token with `repo:read` | `Content-Type: application/x-git-upload-pack-request`; optional `Content-Encoding: gzip` and `Git-Protocol` | 200 `application/x-git-upload-pack-result` |
| POST | `/{owner}/{repo}.git/git-receive-pack` | Token with `repo:write` and write permission; standard repositories only | `Content-Type: application/x-git-receive-pack-request` | 200 `application/x-git-receive-pack-result`: git's report-status, with push-policy refusals as `remote: ZergHub: …` lines |

Errors are `text/plain; charset=utf-8` with `X-Content-Type-Options: nosniff`;
git prints the body after `remote: `.

| Status | Body | When |
| --- | --- | --- |
| 401 | `ZergHub: authentication is required.` | No credential on a clone or fetch (`WWW-Authenticate: Basic realm="ZergHub", charset="UTF-8"`). Anonymous access is off. |
| 401 | `ZergHub: authentication is required to push.` | No credential on a push (with the same challenge). |
| 401 | `ZergHub: the credential is invalid, expired or revoked.` | A malformed, unknown, expired or revoked token (with the same challenge). |
| 404 | `ZergHub: Repository not found` | The repository does not exist or the token's person cannot read it: one answer for both. |
| 403 | `ZergHub: this credential does not allow pushes.` | The token lacks `repo:write`. |
| 403 | `ZergHub: you do not have write permission on this repository.` | The token's person can read but not write. |
| 403 | `ZergHub: agent_only repositories accept pushes only from App Store attempt leases (D3); personal tokens and operator leases cannot push here. https://hub.zergstack.com/docs/push-policy#a1` | Any token push to an agent-only repository. |
| 403 | `ZergHub: the dumb HTTP protocol is not supported; use a git client that speaks smart HTTP.` | `info/refs` without `service`, `HEAD`, `objects/…`. |
| 405 | `ZergHub: method not allowed` | Wrong method for the path (`Allow: GET, POST`). |
| 400 | `ZergHub: unsupported Git-Protocol header` | A `Git-Protocol` value other than `version=0` to `version=2`. |
| 415 | `ZergHub: expected application/x-<service>-request` | Wrong `Content-Type` on an RPC. |
| 400 | `ZergHub: the push request is malformed (<reason>).` | A `git-receive-pack` body whose command list git would never send. |
| 429 | `ZergHub: too many requests; retry in <n>s.` | The client IP's rate limit, with `Retry-After`. |
| 429 | `ZergHub: too many requests for this repository; retry in <n>s.` | The repository's rate limit, with `Retry-After`. |
| 503 | `ZergHub: this repository is still being created; retry shortly.` | The repository is provisioning. |
| 503 | `ZergHub: the git service is busy. Retry shortly.` | No git slot is free (Retry-After: 5). |
| 503 | `ZergHub: too many pushes to this repository are running; nothing was changed. Retry shortly.` | The push limit is reached (Retry-After: 5). |
| 503 | `ZergHub: this git node is not accepting pushes right now. Retry shortly.` | Pushes are paused; fetches continue (Retry-After: 30). |
| 503 | `ZergHub: this git node is not accepting pushes right now; nothing was changed. Retry shortly.` | Pushes were paused after the advertisement; nothing changed (Retry-After: 30). |
| 503 | `ZergHub: the git service is unavailable; retry shortly.` | The git service did not answer (Retry-After: 10). |
| 500 | `ZergHub: internal error` | An unexpected failure in the front door. |
| 500 | `ZergHub: the git node failed to answer` | An unexpected failure in the git service. |

When the git service refuses a push body part-way (a pack over the per-push
cap), ZergHub answers at once and reads and discards the rest of the body, so
the client finishes sending and then reads the refusal.

## Public endpoints

No credential. JSON, `Cache-Control: no-store`.

| Method | Path | Auth | Response |
| --- | --- | --- | --- |
| GET | `/api/health` | None | 200 `{"ok":true,"status":"live"}` |
| GET | `/api/health/ready` | None | 200 `{"ok":true,"status":"ready"\|"degraded","database":"ok","schemaVersion":<n>,"gitNode":{…}}`; 503 `{"ok":false,"status":"schema_mismatch"\|"git_rpc_mismatch"\|"unavailable",…}` |
| GET | `/api/version` | None | 200 `{"service":"zerghub","version":…,"commit":…,"schemaVersion":<n>,"rpcVersion":<n>,"gitNode":{"version":…,"commit":…,"rpcVersion":<n>}\|null}` |

## Public documents

No credential, no cookies; served with `X-Content-Type-Options: nosniff`.

| Method | Path | Content type |
| --- | --- | --- |
| GET | `/agents.md` | `text/markdown; charset=utf-8` |
| GET | `/AGENTS.md` | `text/markdown; charset=utf-8` |
| GET | `/llms.txt` | `text/plain; charset=utf-8` |
| GET | `/docs/api.md` | `text/markdown; charset=utf-8` |
| GET | `/skills/zerghub-git/SKILL.md` | `text/markdown; charset=utf-8` |
| GET | `/developers` | `text/html;charset=utf-8` |
| GET | `/docs/push-policy` | `text/html;charset=utf-8` |

## Session-only routes

These need a signed-in browser session; they are listed so an agent does not
try them. A token gets `401` (`authentication_required`). Ask the person to do
the task in the browser, or use git for what git can do.

| Method | Path | Purpose |
| --- | --- | --- |
| POST | `/api/v1/auth/logout` | End the session. |
| GET | `/api/v1/session` | The signed-in identity and its namespaces. |
| GET | `/api/v1/user` | The signed-in user's profile. |
| GET | `/api/v1/user/tokens` | List the person's tokens (names and hints, never the tokens). |
| POST | `/api/v1/user/tokens` | Create a token; the only response that contains it. |
| DELETE | `/api/v1/user/tokens/{id}` | Revoke a token. |
| GET | `/api/v1/namespaces/{slug}` | A namespace, its quota and usage. |
| GET | `/api/v1/namespaces/{slug}/repositories` | A namespace's repositories. |
| POST | `/api/v1/namespaces/{slug}/repositories` | Create a standard repository. |
| GET | `/api/v1/repos/{owner}/{name}` | A repository by name. |
| GET | `/api/v1/repositories/{id}` | A repository by id. |
| GET | `/api/v1/repositories/{id}/refs` | Branches and tags. |
| GET | `/api/v1/repositories/{id}/tree/{rev}` | A directory listing. |
| GET | `/api/v1/repositories/{id}/contents/{rev}` | The root directory's contents. |
| GET | `/api/v1/repositories/{id}/contents/{rev}/{path}` | A file or directory, files with a signed raw URL. |
| GET | `/api/v1/repositories/{id}/blobs/{sha}` | A blob by id. |
| GET | `/api/v1/repositories/{id}/commits` | History. |
| GET | `/api/v1/repositories/{id}/commits/{sha}` | A commit and its diff. |
| GET | `/api/v1/repositories/{id}/archive/{file}` | `302` to a signed archive URL. |
| GET | `/api/v1/repositories/{id}/readme` | The README. |
| GET | `/api/zerg-feedback/config` | The feedback widget's settings (answers anyone `{"enabled":false}` while feedback capture is off). |
| POST | `/api/zerg-feedback/grant` | Start a feedback capture. |
| POST | `/api/zerg-feedback/recovery` | Recover a feedback capture. |

For a person, reads past the git service's browse capacity answer `503` with
the error code `git_busy` and `Retry-After: 5`.

## Browser sign-in

Public routes, but only a browser can complete them: they redirect through
Sign in with Zerg and set the session cookie. An agent never calls them.

| Method | Path | Purpose |
| --- | --- | --- |
| GET | `/api/v1/auth/zerg/start` | Start Sign in with Zerg. |
| GET | `/api/v1/auth/zerg/callback` | Finish it and create the session. |

## Raw origin

`https://raw.hub.zergstack.com` serves file bytes and source archives through
signed URLs only, each valid for at most 5 minutes and bound to the person who
requested it. The signed URLs come from the session-only contents and archive
routes, so they are not available to tokens; clone the repository instead.
