@colony2/gitvend 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,181 @@
1
+ # Docker Compose deployment
2
+
3
+ The stack runs one persistent `gateway`, a one-shot `init`, and an on-demand
4
+ `issuer`. All use the same image and UID/GID `10001:10001`. Only signing keys
5
+ persist; the gateway remains stateless.
6
+
7
+ ## Configure and start
8
+
9
+ Download a [release archive](https://github.com/colony-2/gitvend/releases) and
10
+ extract it into your deployment directory. Use a release containing `compose.yaml`,
11
+ `compose.config.yaml`, and `deploy/gitvend.default.yaml`; keep this layout for the
12
+ commands below. Docker Compose v2 or newer is required; no local build is needed.
13
+
14
+ Set `GITVEND_IMAGE` to the matching published image tag (replace `vX.Y.Z` with your
15
+ release version), then pull the image and start the stack:
16
+
17
+ ```sh
18
+ export GITVEND_IMAGE='ghcr.io/colony-2/gitvend:vX.Y.Z'
19
+ export GITHUB_TOKEN='your-github-access-token'
20
+ docker compose --profile tools pull
21
+ docker compose up -d --no-build
22
+ docker compose ps -a
23
+ curl --fail http://127.0.0.1:8443/healthz
24
+ ```
25
+
26
+ Keep both variables exported for later Compose commands, or save them in a
27
+ private `.env` file in the deployment directory. Pin a release tag or digest
28
+ when upgrading. A private GHCR package requires registry login.
29
+
30
+ `GITHUB_TOKEN` is a GitHub HTTPS access token,
31
+ not an SSH key. Keep it exported for subsequent Compose commands, or put
32
+ `GITHUB_TOKEN=...` in a private `.env` file. Compose passes it
33
+ only to the gateway, never to the build, initializer, or issuer.
34
+
35
+ The image includes [default configuration](../deploy/gitvend.default.yaml);
36
+ no local config or token file is needed. It accepts any owner on `github.com`,
37
+ using explicit `["*"]` provider and signing-key owner lists. Each request must
38
+ still match the agent JWT's permissions and the GitHub token's actual access.
39
+ Writes/deletions to `main` and `master` are denied for all owners, and repository
40
+ creation is disabled. No GitHub owner discovery or extra API scope is needed.
41
+ Newly accessible owners work without regenerating config.
42
+
43
+ `init` exits once keys are ready, then `gateway` starts. `issuer` is in the
44
+ `tools` profile, so normal `up` does not start it. Explicit `compose run issuer`
45
+ activates it automatically. See Docker's [profile behavior](https://docs.docker.com/compose/how-tos/profiles/).
46
+
47
+ Optional settings in the environment or root `.env` file:
48
+
49
+ | Variable | Default |
50
+ | --- | --- |
51
+ | `GITVEND_PORT` | `8443` (host loopback only) |
52
+ | `GITVEND_IMAGE` | Set explicitly to your published image tag; `gitvend:local` is the source-build default. |
53
+
54
+ ## HTTPS endpoint
55
+
56
+ The default listener is HTTP on container port 8080, published **only** on host
57
+ loopback port 8443. Put an existing HTTPS reverse proxy on the host in front of
58
+ `http://127.0.0.1:8443`; agents use its public HTTPS address.
59
+
60
+ For a proxy container, attach it to the Compose network and target
61
+ `http://gateway:8080`. Treat that network as trusted. Preserve `Authorization`
62
+ and `Git-Protocol`, stream bodies, and align request sizes/timeouts with Gitvend.
63
+ Upstream GitHub connections remain HTTPS.
64
+ See [TLS terminator configuration](gateway.md#running-behind-a-tls-terminator).
65
+ `/healthz` checks liveness only; use `git ls-remote` through the public HTTPS
66
+ endpoint to validate actual GitHub access.
67
+
68
+ ## Issue a JWT
69
+
70
+ After startup:
71
+
72
+ ```sh
73
+ (umask 077
74
+ docker compose run --rm -T --no-deps issuer \
75
+ --subject agent-47 --ttl 15m \
76
+ --permission 'github.com/myorg/project#main:r' \
77
+ --permission 'github.com/myorg/project#agents/47/*:rw' \
78
+ > agent-47.jwt)
79
+ ```
80
+
81
+ The issuer reads the private key, writes the JWT to stdout, and exits. `--rm`
82
+ removes the container; `-T` avoids terminal formatting. No issuer process or
83
+ signing API runs continuously. Deliver the JWT through your own secret mechanism.
84
+
85
+ Defaults are key `/var/lib/gitvend/private/signing.key`, key ID `compose-1`,
86
+ issuer `compose`, and audience `gitvend`, matching the bundled config. Additional
87
+ flags override them. Supported properties also include `--policy-revision` and
88
+ `--max-token-bytes`; arbitrary custom claims are unsupported. Increase the gateway's
89
+ `max_token_lifetime_seconds` in a custom config before issuing tokens longer than
90
+ its 15-minute default.
91
+
92
+ To use a grant file instead of permissions:
93
+
94
+ ```sh
95
+ (umask 077
96
+ docker compose run --rm -T --no-deps \
97
+ -v "$PWD/examples/grant.json:/run/grant.json:ro" issuer \
98
+ --subject agent-47 --grant /run/grant.json > agent-47.jwt)
99
+ ```
100
+
101
+ Edit the grant's organization/repository names and make it readable by UID 10001.
102
+ Host redirection avoids `--output` paths referring to the container filesystem.
103
+
104
+ ## Optional custom configuration
105
+
106
+ For an explicit owner allowlist, repository creation, signing-key rotation,
107
+ or other advanced settings, copy the bundled config and use the optional overlay:
108
+
109
+ ```sh
110
+ cp deploy/gitvend.default.yaml deploy/gitvend.yaml
111
+ # Edit deploy/gitvend.yaml, then:
112
+ docker compose -f compose.yaml -f compose.config.yaml up -d --no-build
113
+ ```
114
+
115
+ The overlay mounts `deploy/gitvend.yaml` read-only (override its path with
116
+ `GITVEND_CONFIG`). Keep it readable by UID 10001 and use both `-f` flags for
117
+ subsequent Compose operations. To restrict organizations, replace `["*"]` in
118
+ both `keys[].owners["github.com"]` and `providers[].allowed_owners` with lowercase
119
+ owner names. Preserve the `env:GITHUB_TOKEN` credential reference.
120
+
121
+ ## Keys and operations
122
+
123
+ `signing-private` is mounted only by `init` and `issuer`; `signing-public` is
124
+ mounted read-only by the gateway. The issuer's private mount is also read-only.
125
+ Only the gateway receives `GITHUB_TOKEN`. Init and issuer have no networking.
126
+
127
+ `keygen --if-missing` locks initialization, validates existing keys, and never
128
+ overwrites them. It atomically commits the private key first and recovers a
129
+ missing public key from it after interruption. Corrupt keys, mismatched pairs,
130
+ or a public key without its private key fail startup; restore the matching
131
+ backup. Parent directories must exist. Use local Docker volumes; distributed
132
+ filesystem locking is outside this deployment's scope.
133
+
134
+ Named volumes survive recreation, upgrades, and `docker compose down`.
135
+ **`docker compose down -v` deletes the signing keys.** Keep the Compose project
136
+ name stable across upgrades/moves, since changing it selects different volumes.
137
+
138
+ Back up both volumes after initialization:
139
+
140
+ ```sh
141
+ (umask 077
142
+ docker compose run --rm -T --no-deps --entrypoint tar init \
143
+ -C /var/lib/gitvend -cf - private public > signing-backup.tar)
144
+ ```
145
+
146
+ Protect this archive as a private key. Restore both directories into their
147
+ corresponding volumes with the gateway stopped, preserving UID/GID 10001 and
148
+ private-file mode 0600. Do not remove the lock file during initialization.
149
+
150
+ For signing-key rotation, enable custom configuration, then run
151
+ `docker compose run --rm --no-deps init keygen` with new
152
+ `--private` and `--public` filenames in the existing volume directories. Add the
153
+ new public key under a new ID in config, reload, and pass the new `--key` and
154
+ `--kid` to `issuer`. Keep the original pair for the default initializer; retain
155
+ old verification keys until their tokens expire, including clock tolerance.
156
+
157
+ ```sh
158
+ docker compose kill -s SIGHUP gateway
159
+ docker compose logs --tail 50 gateway
160
+ ```
161
+
162
+ Reload after changing denies or verification keys in custom config. If a bind-mounted file
163
+ was replaced by rename, recreate the gateway to refresh the mount:
164
+ `docker compose up -d --no-build --force-recreate gateway`. Listener/TLS changes also require
165
+ recreation. Compose allows 15 seconds to stop, covering Gitvend's 10-second drain.
166
+ To rotate the GitHub token, update the exported `GITHUB_TOKEN` (or `.env`) and
167
+ run `docker compose up -d --no-build --force-recreate gateway`; SIGHUP cannot change a
168
+ running container's environment.
169
+
170
+ ## Verify the deployment
171
+
172
+ Check `docker compose ps -a`, the gateway logs, and `/healthz`, then use a real
173
+ agent credential with `git ls-remote` against the public HTTPS endpoint. The
174
+ health endpoint alone does not confirm GitHub access.
175
+
176
+ For creation controls, including visibility and GitHub features, see the
177
+ [gateway guide](gateway.md#repository-creation-defaults). The bundled container
178
+ configuration disables creation until you enable it in a custom config.
179
+
180
+ Source builds and automated container tests are covered in the
181
+ [developer guide](development.md#container-development).
@@ -0,0 +1,440 @@
1
+ # Gitvend gateway guide
2
+
3
+ Gitvend is a Git/HTTPS gateway with permissions carried in signed JWTs. Agents use ordinary Git and one gateway credential across their assigned repositories. The gateway holds a shared upstream token and enforces repository, ref-discovery, branch-write, tag and repository-creation permissions before forwarding requests.
4
+
5
+ Implemented for GitHub and GitHub Enterprise-compatible APIs. Client and upstream Git connections use HTTPS; fetch/ref lookup requires protocol v2, while pushes use Git's normal receive-pack format. The service does not require a GitHub App. The gateway is stateless: it needs configuration, public verification keys and upstream credentials, with no database or writable state volume. Multiple instances can serve the same agents.
6
+
7
+ This guide takes an operator from configuring a gateway to giving an agent a credential, cloning a repository, and pushing an allowed branch.
8
+
9
+ [Install](#install) · [Docker Compose](docker-compose.md) · [Set up the gateway](#set-up-the-gateway) · [Issue a credential](#issue-an-agent-credential) · [Agent usage](#use-gitvend-as-an-agent) · [Permissions](#permission-strings) · [Operations](#provisioning-and-audit) · [Troubleshooting](#troubleshooting) · [CLI reference](#cli-reference)
10
+
11
+ ## How credentials and permissions fit together
12
+
13
+ ```text
14
+ Agent running Git -- HTTPS + agent JWT --> gitvend -- HTTPS + upstream token --> GitHub
15
+ ^
16
+ trusts the issuer's public key
17
+ ```
18
+
19
+ | Credential | Who holds it | Purpose |
20
+ | --- | --- | --- |
21
+ | Upstream GitHub token | Gateway operator; available to the gateway process | Lets gitvend access repositories and, when enabled, create them. Shared across agents. |
22
+ | Ed25519 private signing key | Trusted orchestrator or operator | Signs each agent's JWT with its identity, expiry and permissions. |
23
+ | Ed25519 public signing key | Gateway | Verifies JWTs without contacting the issuer. |
24
+ | Agent JWT | The assigned agent | Used as its Git HTTPS password. Contains the signed permission strings. |
25
+
26
+ An operation must pass all configured checks: the provider's allowed owners, the signing key's allowed owners, the JWT's grants, gateway-wide deny rules, and the upstream account's permissions. A JWT cannot grant access outside these limits. Give agents their JWT; retain the upstream token on the gateway and the private signing key with the trusted issuer.
27
+
28
+ For JobDB workers, use the [JobDB plugin](jobdb-secret-provider.md) to issue
29
+ credentials automatically for each job lease. Follow that guide for issuer setup;
30
+ the gateway configuration, creation controls and agent Git usage here still apply.
31
+
32
+ Read permissions hide ungranted ref names; they do **not** isolate Git objects by branch. An agent with any read grant for a repository may fetch objects by known hash, subject to what the upstream serves. Use separate repositories when you need object confidentiality.
33
+
34
+ ## Install
35
+
36
+ Install the published package on the gateway, the machine that issues credentials,
37
+ and agents that use the Git credential helper:
38
+
39
+ ```sh
40
+ npm install -g @colony2/gitvend
41
+ gitvend version
42
+ ```
43
+
44
+ The npm package downloads a prebuilt executable and verifies its SHA-256 checksum;
45
+ it requires Node.js 22+ and `tar`, with install scripts enabled. No Go toolchain
46
+ is needed. For installation without Node.js, download the Linux or macOS archive
47
+ for your amd64/arm64 system from [GitHub Releases](https://github.com/colony-2/gitvend/releases),
48
+ verify it against the release's `checksums.txt`, and put `gitvend` on `PATH`.
49
+ Archives include the example configurations and user guides.
50
+
51
+ For a container deployment using published images, follow the
52
+ [Docker Compose guide](docker-compose.md). That stack manages signing keys,
53
+ starts the gateway and provides an on-demand credential issuer.
54
+
55
+ ## Set up the gateway
56
+
57
+ The walkthrough uses organization `fooorg`, an existing repository `project` with a `main` branch, and gateway address `git-gateway.example:8443`. Replace these with your own values. Commands assume `gitvend` is on `PATH`. Agents need Git with protocol v2 support; the gateway serves HTTPS directly in this example.
58
+
59
+ ### 1. Provide the upstream token
60
+
61
+ Have your process supervisor or secret manager set `GITHUB_TOKEN` in the **gateway process** environment. The token's account must have access to the repositories the agents will use. For automatic creation, it must also be able to create repositories with the configured visibility in the organization and access them afterward. Gitvend does not generate or renew this upstream token.
62
+
63
+ GitHub's [token guide](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) describes account and organization restrictions. For private organization repository creation, GitHub documents `repo` scope for classic tokens or repository Administration write permission for fine-grained tokens; creation permissions alone do not establish Git read/write access. Check repository selection, organization approval and workflow-file access for your account. See [GitHub's create-repository requirements](https://docs.github.com/en/rest/repos/repos#create-an-organization-repository).
64
+
65
+ If you prefer a mounted token file, use this credential object inside the provider entry:
66
+
67
+ ```yaml
68
+ credential:
69
+ kind: token
70
+ secret_ref: file:/run/secrets/github-token
71
+ ```
72
+
73
+ The same credential is used for Git and REST API calls. An optional provider-level `api_credential` object with the same shape selects a separate REST token. This release supports token credentials; it does not use SSH keys or discover credentials from `gh auth` or Git credential helpers.
74
+
75
+ ### 2. Generate and install the issuer key
76
+
77
+ On the trusted orchestrator or operator machine:
78
+
79
+ ```sh
80
+ mkdir -p var
81
+ gitvend keygen --private var/signing.key --public var/signing.pub
82
+ ```
83
+
84
+ Keep `var/signing.key` on that machine. Copy **only** `signing.pub` to `var/signing.pub` in the gateway's working directory. If the operator and gateway run on one machine, restrict the private key to the issuer's account. Each configured key has an ID and an issuer name; the token-signing command must use those exact values.
85
+
86
+ ### 3. Configure and start the server
87
+
88
+ On the gateway, prepare the public-key directory and save the
89
+ [configuration template](../examples/gitvend.yaml) as `gitvend.yaml`. If you
90
+ downloaded a release archive, copy its included example:
91
+
92
+ ```sh
93
+ mkdir -p var
94
+ cp examples/gitvend.yaml gitvend.yaml
95
+ ```
96
+
97
+ Edit `gitvend.yaml` using this checklist:
98
+
99
+ | Field | Set it to |
100
+ | --- | --- |
101
+ | `listen` | `0.0.0.0:8443` to accept connections on the gateway's network interfaces. The template binds to `127.0.0.1:8443` for local access. |
102
+ | `tls_cert`, `tls_key` | Readable certificate/key files for `git-gateway.example`, trusted by the agents. |
103
+ | `keys[].public_key_file` | The public key installed in step 2. |
104
+ | `keys[].owners` and `providers[].allowed_owners` | Your lowercase organization names in both places. |
105
+ | `providers[].credential.secret_ref` | `env:GITHUB_TOKEN` or the mounted token file reference. |
106
+ | `providers[].allow_creation` | `true` for the scratch-repository example below; `false` to disable creation. |
107
+ | `providers[].creation_defaults` | Optional visibility and GitHub feature settings for new repositories; see [creation defaults](#repository-creation-defaults). |
108
+ | `denies` | Replace `fooorg` in the example deny rule too. It blocks updates/deletion of `main` and `master`. |
109
+
110
+ Configuration uses YAML. Unknown fields, duplicate keys, and multiple YAML documents are rejected. JSON-shaped mappings are also valid YAML and load through the same parser. Relative file paths are resolved from the server's **working directory**, not the configuration file's directory; use absolute paths when running under a supervisor.
111
+
112
+ An owner list may be `["*"]` to accept every owner on that exact host. JWT grants,
113
+ gateway denies, and upstream permissions still apply. A wildcard signing-key
114
+ ceiling requires a wildcard provider for that host; a wildcard provider can also
115
+ use narrower signing-key owner lists. Wildcard providers cannot overlap another
116
+ provider on the same host. The Compose image uses this opt-in wildcard mode so
117
+ no organization configuration is required.
118
+
119
+ ```sh
120
+ gitvend validate --config gitvend.yaml
121
+ gitvend serve --config gitvend.yaml
122
+ ```
123
+
124
+ The process runs in the foreground and writes operational logs to stderr. From another terminal:
125
+
126
+ ```sh
127
+ curl --fail https://git-gateway.example:8443/healthz
128
+ ```
129
+
130
+ Expected response: `ok`. This is a liveness check; it does not verify a JWT or contact GitHub. The agent's `ls-remote` command below verifies the Git path.
131
+
132
+ The remote URL has this structure:
133
+
134
+ ```text
135
+ https://GATEWAY/PROVIDER_ALIAS/OWNER/REPOSITORY.git
136
+ https://git-gateway.example:8443/github/fooorg/project.git
137
+ ```
138
+
139
+ The `github` segment is the configured provider alias. Permission strings use the configured upstream authority (`github.com`), not that alias or the gateway hostname. Repository permission patterns omit the `.git` suffix. Add more provider entries for distinct GitHub-compatible endpoints or owner/credential mappings; overlapping host/owner routes are rejected. Other forge APIs require an adapter.
140
+
141
+ ### Running behind a TLS terminator
142
+
143
+ Set `allow_http: true` only for the protected HTTP listener behind the terminator, and bind it to the intended internal interface. This option makes the listener serve HTTP even if TLS files are present. Agents still use the public HTTPS address and its exact host/port in their credential helper.
144
+
145
+ Preserve the URL, `Authorization` and `Git-Protocol` headers, allow streaming request/response bodies, and align ingress body, header and timeout limits with gitvend. `allow_http_upstream` is a separate local-test option; upstream GitHub connections remain HTTPS. Neither option disables certificate verification for HTTPS.
146
+
147
+ ## Issue an agent credential
148
+
149
+ On the trusted orchestrator, issue a short-lived JWT carrying the task's permissions:
150
+
151
+ ```sh
152
+ gitvend sign \
153
+ --key var/signing.key \
154
+ --kid orchestrator-1 \
155
+ --issuer orchestrator \
156
+ --subject agent-47 \
157
+ --audience gitvend \
158
+ --ttl 15m \
159
+ --permission 'github.com/fooorg/project#main:r' \
160
+ --permission 'github.com/fooorg/project#agents/47/*:rw' \
161
+ --permission 'github.com/fooorg/agent-47-*:c' \
162
+ --permission 'github.com/fooorg/agent-47-*#agents/47/*:rw' \
163
+ --output var/agent-47.jwt
164
+ ```
165
+
166
+ Alternatively, save the [grant example](../examples/grant.json) locally and pass
167
+ `--grant grant.json`. The grant file is JSON and contains `v` and the `permissions` array. Permissions follow repository names, including future repositories with matching names; there are no repository-ID bindings. `--output` replaces the token file atomically with permissions `0600`; without it, `sign` writes the token to stdout. `keygen` refuses to overwrite existing key files.
168
+
169
+ JWTs use Ed25519 signatures (`alg=EdDSA`, `typ=gitvend+jwt`). Verification requires the configured issuer, key ID and audience plus subject, token ID, issued-at, not-before, expiry and grant version. Unknown fields, duplicate JSON keys, unsupported algorithms and malformed rules are rejected. The gateway needs no issued-token database and never contacts the issuer on a Git request.
170
+
171
+ The sample grant allows reading `main`, reading/writing `agents/47/*` branches, and creating private scratch repositories named `agent-47-*` with writable branches under the same prefix. It does not allow pushing `main`, deleting branches or creating tags. The sample server deny rule protects `main` and `master` even if a later JWT grants them writes.
172
+
173
+ Deliver `var/agent-47.jwt` to the agent through your orchestrator's secret mechanism, for example as `/run/secrets/gitvend.jwt`. The file must be readable by that agent. Gitvend does not deliver tokens to agents itself.
174
+
175
+ Refresh the injected file before expiry. Re-running the signing command with `--output` atomically replaces its output file; arrange the same replacement at the agent's token path. The credential helper reads the current file for each credential request, so Git remotes need no change. The default maximum lifetime is 15 minutes; a longer `--ttl` also requires a matching server-side `max_token_lifetime_seconds` setting.
176
+
177
+ A task's existing token remains usable until expiry plus configured clock tolerance even if the issuer stops renewing it. There is no immediate per-agent revocation list. Removing a signing key or adding a gateway-wide deny rule affects new requests after reload; an admitted transfer may finish within its deadline.
178
+
179
+ ## Use gitvend as an agent
180
+
181
+ ### Configure Git authentication
182
+
183
+ Run these commands inside the agent's own OS account or sandbox, with `gitvend` on `PATH`:
184
+
185
+ ```sh
186
+ git config --global protocol.version 2
187
+ git config --global credential.https://git-gateway.example:8443.helper ''
188
+ git config --global --add credential.https://git-gateway.example:8443.helper \
189
+ '!gitvend credential --host git-gateway.example:8443 --token-file /run/secrets/gitvend.jwt'
190
+ ```
191
+
192
+ The empty entry resets inherited helpers for this host. The helper supplies username `git` and the JWT as the HTTPS Basic password only for the exact configured host and port. Use `git-gateway.example` in both settings if serving on standard HTTPS port 443 with no explicit port in the URL. The shell-style `!` helper invokes `gitvend` from `PATH`; Git appends its `get`, `store` or `erase` operation automatically. Store/erase are ignored because the orchestrator owns token replacement.
193
+
194
+ `--token-env VARIABLE_NAME` can replace `--token-file` if you inject the JWT into the agent's environment. Do not put the JWT in a clone URL. A custom Git client can use HTTP Basic directly, with the JWT as its password, and must request protocol v2 for fetch/discovery.
195
+
196
+ ### Clone and push an assigned branch
197
+
198
+ ```sh
199
+ # Confirm that the credential, route and upstream repository work.
200
+ git ls-remote --heads https://git-gateway.example:8443/github/fooorg/project.git
201
+
202
+ # Select main explicitly, even if the repository's default branch is hidden.
203
+ git clone --branch main https://git-gateway.example:8443/github/fooorg/project.git
204
+ git -C project switch -c agents/47/my-task
205
+
206
+ # After making and committing your changes, push an explicit destination.
207
+ git -C project push -u origin HEAD:refs/heads/agents/47/my-task
208
+ ```
209
+
210
+ With the sample grant, discovery shows `main` and any existing `agents/47/*` branches. The push can create the assigned branch or update it. A push to `refs/heads/main` is denied. If one push contains any unauthorized ref update, gitvend rejects the whole request before forwarding its push commands or pack to GitHub. GitHub may independently reject permitted updates through its branch rules; use `git push --atomic` when you require all permitted updates to succeed together and the upstream supports it.
211
+
212
+ For an existing checkout, route its remote through the gateway:
213
+
214
+ ```sh
215
+ git -C project remote set-url origin https://git-gateway.example:8443/github/fooorg/project.git
216
+ ```
217
+
218
+ Changing a remote does not remove previously fetched objects or refs from that checkout. Use a fresh clone when you want its initial local refs to reflect the current discovery grant.
219
+
220
+ ### Create a missing scratch repository on first push
221
+
222
+ The sample token also grants creation of `fooorg/agent-47-*`. Create a local repository with a branch the agent can write, then push it through the gateway:
223
+
224
+ ```sh
225
+ git init -b agents/47/start agent-47-scratch
226
+ git -C agent-47-scratch config user.name 'Agent 47'
227
+ git -C agent-47-scratch config user.email 'agent-47@example.invalid'
228
+ git -C agent-47-scratch commit --allow-empty -m 'Initialize agent scratch repository'
229
+ git -C agent-47-scratch remote add origin \
230
+ https://git-gateway.example:8443/github/fooorg/agent-47-scratch.git
231
+ git -C agent-47-scratch push -u origin HEAD:refs/heads/agents/47/start
232
+ ```
233
+
234
+ Gitvend creates the missing private repository during push discovery, then forwards the authorized push. It creates no initial commit or `main` branch of its own. For a later clone, select `--branch agents/47/start` explicitly. A clone of a missing repository does not create it. Even `git push --dry-run` can create an empty repository; see [provisioning behavior](#provisioning-and-audit).
235
+
236
+ ## Permission strings
237
+
238
+ ```text
239
+ [!]host/namespace/repository[#ref-selector]:actions
240
+ ```
241
+
242
+ | Action | Meaning |
243
+ | --- | --- |
244
+ | `r` | Read objects by hash and discover matching refs. |
245
+ | `w` | Create/update matching refs, including force updates. |
246
+ | `d` | Delete matching refs. |
247
+ | `c` | Create matching repositories using the provider's creation defaults; use without a ref selector. |
248
+
249
+ ```text
250
+ github.com/fooorg/*:r
251
+ github.com/fooorg/blue*:rw
252
+ github.com/*:r
253
+ github.com/fooorg/red#(main|master|link):rw
254
+ !github.com/fooorg/*#(main|master):wd
255
+ ```
256
+
257
+ Each rule is a separate `--permission` argument or an element in a grant's `permissions` array; do not combine rules into a comma-separated string. Quote rules in shell commands so glob and `!` characters reach gitvend unchanged.
258
+
259
+ | Rule | Use |
260
+ | --- | --- |
261
+ | `github.com/fooorg/project#main:r` | Discover main and allow repository hash reads. |
262
+ | `github.com/fooorg/project#agents/47/*:rw` | Read/create/update this agent's branches. Add `d` (`:rwd`) to allow deletion. |
263
+ | `github.com/fooorg/project#refs/tags/agent-47-*:rw` | Read/create/update matching tags explicitly. |
264
+ | `github.com/fooorg/agent-47-*:c` | Permit matching repository creation; also grant `rw` on at least one branch. |
265
+ | `!github.com/fooorg/*#(main\|master):wd` | Deny main/master writes and deletion across the organization. |
266
+
267
+ Repository `*` stays within one path segment; branch `*` can span `/`. `(a|b)` selects alternatives, not arbitrary regular expressions. `github.com/*` is the whole-host shorthand, still constrained by configured owners.
268
+
269
+ Matching allows combine; any matching deny wins. No `#` covers branches and tags; `#*` covers branches only. A `w` or `d` operation also requires read/discovery permission. `r` scoped to a branch hides other refs but deliberately does not block known-hash reads from the same repository. A broad read grant is not narrowed by adding a more specific grant.
270
+
271
+ See [the complete syntax](permission-syntax.md) for wildcard, alternative, escaping and deny semantics. Inspect a decision without making network calls:
272
+
273
+ ```sh
274
+ gitvend explain \
275
+ --permission 'github.com/fooorg/*:r' \
276
+ --permission 'github.com/fooorg/blue*:w' \
277
+ --permission '!github.com/fooorg/*#main:wd' \
278
+ --repo github.com/fooorg/bluebird \
279
+ --ref refs/heads/main \
280
+ --action ref.update
281
+ ```
282
+
283
+ The example returns `"allowed": false`. `explain` evaluates only the rules you pass; include applicable deny rules yourself. It does not load server configuration, verify a JWT, or check GitHub permissions. A valid explanation command exits successfully even for a denied decision; inspect `allowed`. A `repo.create` explanation evaluates the `c` permission only, not the additional branch-overlap or upstream checks.
284
+
285
+ ## Provisioning and audit
286
+
287
+ Repository creation is opt-in twice: the provider must set `allow_creation: true` and the JWT must grant `c` plus an overlapping readable/writable branch. Matching deny rules still apply to both creation and branch writes.
288
+
289
+ Creation occurs during receive-pack discovery. Consequently, `git push --dry-run`, abandoned pushes and later branch-policy rejections may leave empty repositories. Clone/fetch never creates repositories. Direct receive-pack POSTs cannot create missing repositories. Created repositories are private by default and always uninitialized; the gateway never changes an existing repository's visibility, deletes it, or silently seeds `main`.
290
+
291
+ Concurrent creation is coordinated by the forge's repository-name uniqueness. Different gateway instances may both attempt creation; after a conflict or ambiguous response, gitvend looks up the name and checks that the resulting repository matches the configured creation visibility and requested owner/name. It never changes an existing repository's visibility. If the outcome cannot be confirmed, the request fails with 503; a later normal push discovery performs a fresh lookup. There are no background reconciliation jobs, creation quotas or stored attempts.
292
+
293
+ ### Repository creation defaults
294
+
295
+ Each GitHub provider can specify its own `creation_defaults`, alongside its
296
+ upstream credentials and `allow_creation`:
297
+
298
+ ```yaml
299
+ allow_creation: true
300
+ creation_defaults:
301
+ visibility: private
302
+ has_issues: false
303
+ has_projects: false
304
+ has_wiki: false
305
+ has_pull_requests: false
306
+ ```
307
+
308
+ Supported settings use GitHub's field names:
309
+
310
+ | Settings | Values and behavior |
311
+ | --- | --- |
312
+ | `visibility` | `private` (default) or `public`. |
313
+ | `has_issues`, `has_projects`, `has_wiki`, `has_pull_requests` | Optional booleans. An explicit `false` disables the feature; omission leaves GitHub's default. |
314
+ | `description`, `homepage` | Optional strings applied to newly created repositories. |
315
+ | `pull_request_creation_policy` | Optional `all` or `collaborators_only`. |
316
+ | `allow_squash_merge`, `allow_merge_commit`, `allow_rebase_merge` | Optional booleans. At least one merge method must remain available. |
317
+ | `allow_auto_merge`, `delete_branch_on_merge` | Optional booleans; GitHub account/organization restrictions still apply. |
318
+
319
+ Gitvend supplies `name` from the authorized repository and always sets
320
+ `auto_init: false`. Initialization templates and client-selected creation options
321
+ are not accepted. GitHub organization policies and upstream permissions can
322
+ reject settings; these errors fail provisioning rather than silently changing
323
+ requested options. See the [GitHub creation API](https://docs.github.com/en/rest/repos/repos#create-an-organization-repository).
324
+
325
+ Pull request availability and its creation policy use GitHub's
326
+ [repository update API](https://docs.github.com/en/rest/repos/repos#update-a-repository)
327
+ after creation or successful name reconciliation, before forwarding discovery
328
+ and the first push. The API credential must authorize these repository updates
329
+ as well as creation. A failed/unconfirmed update returns HTTP 502 and records
330
+ `configuration_failed`. The repository may already exist: inspect and complete
331
+ its settings before retrying. These are creation defaults, not ongoing policy
332
+ enforcement. An existing repository found by the initial lookup is left unchanged,
333
+ including on a later retry or after a gateway restart. Gitvend does not store
334
+ pending provisioning state or roll back repository creation.
335
+
336
+ Defaults belong to the upstream provider, not to a JWT signing key or JobDB
337
+ tenant. Different providers can use different tokens and defaults for distinct
338
+ organizations/users. Provider selection uses the URL alias; overlapping owners
339
+ on the same host are rejected. Repository-pattern credential routing within one
340
+ organization is not supported.
341
+
342
+ Permissions follow repository names. Deleting and recreating a repository under the same name leaves a matching JWT grant applicable to that name. Gitvend does not enroll repositories or pin their IDs.
343
+
344
+ ### Audit logs
345
+
346
+ `gitvend serve` emits newline-delimited JSON audit records on **stdout** and operational logs on stderr. Collect stdout with your process supervisor or logging service. Gitvend keeps no audit database or outbox and has no `audit` command.
347
+
348
+ Each JSON record has an `audit` object containing request ID, issuer/subject, token ID, repository, operation and outcome when available. A request can emit an `admitted` event before contacting the upstream and a final outcome with the same `audit.id`. Creation events have a separate ID and an `audit.parent_id` linking them to the Git request's `X-Request-ID` response header. JWT strings, upstream tokens and pack contents are excluded.
349
+
350
+ For example, inspect an existing log export with `jq`:
351
+
352
+ ```sh
353
+ jq -c 'select(.audit.operation == "git-receive-pack") | .audit' audit.jsonl
354
+ ```
355
+
356
+ Push audit distinguishes `accepted`, `rejected`, `partial` and `unknown`; HTTP 200 alone is not success. Creation outcomes are `created`, `reconciled`, `configuration_failed`, `rejected` or `unknown` and do not claim that a subsequent push succeeded. A crash can leave an admission with no final record. Log delivery is best-effort, with retention and durability handled by your logging infrastructure. Inspect the remote refs before retrying an interrupted push; gitvend never retries a push automatically.
357
+
358
+ ### Multiple instances
359
+
360
+ Run any number of instances behind the same HTTPS endpoint with matching configuration, trusted keys and credentials. No shared disk, database, distributed lock or sticky session is required. Configuration/key rotations must reach each instance; a reload on one instance does not update others. `max_concurrent` and request limits apply per instance. If you need fleet-wide rate limits or creation budgets, enforce them outside gitvend. A metrics export endpoint is not implemented.
361
+
362
+ ## Configuration and limits
363
+
364
+ Run the service under a supervisor with a consistent working directory and readable configuration/key/credential files. The running gateway can use a read-only filesystem; operator commands such as `keygen` and `sign --output` still write their explicitly requested files. `SIGTERM` or Ctrl-C starts graceful shutdown with a 10-second drain period; transfers still running afterward are closed.
365
+
366
+ Send `SIGHUP` to atomically reload public keys, issuer ceilings, upstream token files, deny rules and limits. A failed reload preserves the last valid configuration. Changes to the listener or TLS paths require restart. Replacing an environment variable requires restarting the process. Config reload does not rewrite credentials already in use by an admitted operation.
367
+
368
+ For signing-key rotation, add the new public key under a new `id`, reload, and issue tokens with that `--kid`. Keep the old key configured until its tokens have expired, including clock tolerance, if they should remain valid; then remove it and reload. To rotate an upstream file token, replace that file and reload; the gateway reads upstream credentials at startup/reload, unlike the agent helper, which reads its JWT for each credential request.
369
+
370
+ Important defaults:
371
+
372
+ | Setting | Default |
373
+ | --- | --- |
374
+ | `max_token_bytes` | 4,096 serialized JWT bytes; configurable up to 32,768 |
375
+ | `max_token_lifetime_seconds` | 900 |
376
+ | `clock_skew_seconds` | 30 |
377
+ | `max_control_bytes` | 4 MiB |
378
+ | `max_pack_bytes` | 1 GiB incoming encoded and decoded request body limits |
379
+ | `max_concurrent` | 64 active Git requests |
380
+ | `request_timeout_seconds` | 1,800 |
381
+
382
+ The HTTP server accepts up to 64 KiB of headers. Basic authentication expands a JWT by roughly one-third, so ingress limits must accommodate the configured token budget too. Use `sign --max-token-bytes` with the same budget as the gateway. Oversized grants are rejected, never truncated or broadened.
383
+
384
+ Incoming packs are streamed after their command prefix passes authorization. Pack contents are not inspected for file/path rules or commit ancestry.
385
+
386
+ Ordinary clone/fetch/push requests need only the upstream Git endpoint. REST lookups are used during push discovery when both the configuration and JWT permit automatic creation. Those API calls share the upstream credential's rate limit; gitvend does not coordinate rate limits across instances.
387
+
388
+ ## Troubleshooting
389
+
390
+ Start with `gitvend version`, `/healthz`, and `git ls-remote --heads` as shown above. Use the HTTP `X-Request-ID` for authenticated Git requests to correlate a failure with the collected audit logs. Avoid sharing raw credentials or full authorization headers when collecting diagnostics.
391
+
392
+ | Symptom | What to check |
393
+ | --- | --- |
394
+ | Connection refused or TLS certificate error | Confirm the listener interface/port, DNS, certificate hostname and trust chain. The template binds loopback; external agents need a reachable listener or TLS terminator. |
395
+ | Git prompts for a password; helper returns no credential | Confirm `gitvend` is on the agent's `PATH`, the JWT file is readable, and the helper's host **and port** match the remote. Configure the helper in that agent's account. |
396
+ | HTTP 401 / invalid or expired JWT | Refresh the JWT; check issuer, `kid`, audience, clock, lifetime and byte limits against gateway configuration. Agents use their JWT, not the upstream GitHub token. |
397
+ | `Git protocol v2 required` | Set `protocol.version=2`; ensure an ingress proxy preserves the `Git-Protocol` header. Pushes still use standard receive-pack. |
398
+ | HTTP 403 / `push denied for ...` | Inspect the exact destination ref, matching `r` plus `w`/`d` grants, issuer/owner limits and gateway denies. The sample denies writes to main/master. |
399
+ | No refs shown; clone cannot find main or remote HEAD | The requested branch may be absent or hidden. Use an explicitly permitted branch with `--branch`; new scratch repos may have only `agents/47/start`. |
400
+ | HTTP 404 / unsupported route or missing repository | Use `/alias/owner/repo.git`, check allowed owners, and verify the upstream identity can see the repository. Only authorized push discovery can create a missing repo. |
401
+ | Missing repo is not created | Check `allow_creation`, `c`, overlapping branch `rw`, owner limits and upstream organization permissions. Check the audit logs for an unconfirmed create attempt. |
402
+ | HTTP 429 / gateway busy | The concurrent-request limit is reached. Wait for active requests to finish and retry. |
403
+ | HTTP 409 / creation visibility violation | A creation attempt resolved to unexpected visibility. Investigate the upstream name/visibility; gitvend will not alter it automatically. |
404
+ | HTTP 502 / configuration incomplete | The repository exists but its requested pull request settings could not be confirmed. Inspect and complete the settings before retrying. |
405
+ | HTTP 502 / upstream request refused or lookup failed | Check the upstream token, Git/API base URLs and forge availability/rate limits. A gateway JWT cannot overcome upstream restrictions. |
406
+ | HTTP 503 / repository creation unconfirmed | The create attempt and follow-up lookup could not confirm success. Check upstream access/availability, then retry normal push discovery. |
407
+ | HTTP 431 or large token rejected at ingress | Align ingress header limits, server `max_token_bytes` and signing `--max-token-bytes`; Basic authentication expands the token. |
408
+ | Push disconnects or audit outcome is `unknown` | Fetch or inspect the permitted remote ref before deciding whether to retry; some or all updates may already have reached GitHub. |
409
+ | npm-installed command reports a missing binary | Check whether install scripts were disabled and whether release downloads were reachable; after fixing that, run `npm rebuild @colony2/gitvend`. |
410
+
411
+ When upgrading an earlier checkout, remove `state_file`, `creates_per_subject_per_day` and `creates_per_owner_per_day` from configuration. Remove `bindings` from grant files and reissue any JWTs containing that field. Unknown fields are rejected. Old database files are no longer read by the gateway.
412
+
413
+ ## CLI reference
414
+
415
+ Run `gitvend --help` for a setup walkthrough, or use `--help` on any command.
416
+ The CLI supports shell completion and accepts both standard `--flag` options and
417
+ legacy single-dash long flags. For credential helpers, Git appends the final operation.
418
+
419
+ | Command | Purpose / useful flags |
420
+ | --- | --- |
421
+ | `gitvend version` | Show the release version or development build identity. |
422
+ | `gitvend serve --config gitvend.yaml` | Start the gateway. |
423
+ | `gitvend validate --config gitvend.yaml` | Validate local config, public keys and upstream credential references without starting the server. |
424
+ | `gitvend keygen --private signing.key --public signing.pub` | Generate a new issuer key pair without overwriting existing files. Add `--if-missing` to validate/reuse persistent keys or recover a missing public key. Parent directories must exist. |
425
+ | `gitvend sign` | Issue a JWT; requires `--key`, `--kid`, `--issuer`, `--subject` and a grant. Use repeated `--permission` **or** `--grant`, not both; `--output` writes an atomic token file. |
426
+ | `gitvend credential` | Git helper; specify `--host` and exactly one of `--token-file` or `--token-env`, followed by `get`, `store` or `erase`. |
427
+ | `gitvend explain` | Explain explicit `--permission` rules for a `--repo`, optional `--ref`, and `--action`. Ref values are fully qualified, such as `refs/heads/main`. |
428
+
429
+ `explain` actions are `repo.read`, `repo.create`, `ref.discover`, `ref.create`, `ref.update` and `ref.delete`. The default is `ref.update`.
430
+
431
+ ## Compatibility
432
+
433
+ Supported object format is SHA-1. SSH, Git LFS, Git protocol v0/v1 fetch, signed push certificates, push options, named shallow exclusions, ref-in-want, offloaded pack/bundle URLs and provider-managed ref namespaces are rejected or not advertised. Branch writes can include workflow files; CI execution and secrets remain governed by the forge's configuration. GitHub branch rules continue to apply to the shared upstream identity.
434
+
435
+ Use a disposable repository to confirm your upstream token, organization creation
436
+ policy, workflow permissions and GitHub Enterprise version before rolling out to
437
+ workers. Gitvend's configuration cannot grant access that GitHub rejects.
438
+
439
+ See the [developer guide](development.md) for source builds, testing and release
440
+ maintenance, or return to the [overview](../README.md).