Connect a repository
After this page you can attach a private GitHub repository to a Cloud session, explain to a customer’s administrator what they need to authorize, know exactly what the sandbox holds while the agent works, and recover when that authorization changes underneath you.
The unit of authorization is a grant
Section titled “The unit of authorization is a grant”A repository grant binds one exact repository to one exact app installation inside one organization. It is not an OAuth scope, not organization-wide access, and not something your app can widen at runtime.
| Property | Value |
|---|---|
| Identifier | A UUID, opaque to you. This is what you store. |
| Bound to | One organization, one app installation, one GitHub repository. |
| Operations | Either ["CHECKOUT"] or ["CHECKOUT","PUBLISH"]. |
| Uniqueness | At most one active grant per installation per repository. |
| Mutability | None. A change of scope is a new grant. |
Only two operation shapes exist, and a database constraint enforces it:
read-only (CHECKOUT) or read-and-publish (CHECKOUT, PUBLISH). There is no
publish-only grant, because publishing an agent’s work without being able to
read the base branch is not a coherent permission.
What your app ever sees of a grant is its id, the repository’s full name, default branch and private flag, and the operation list. No tokens, no clone URLs, no GitHub installation ids. The grant id is a capability handle; everything that could be used to reach GitHub directly stays on the server, and the sandbox is handed a short-lived ticket scoped to one operation instead.
An administrator creates it, not your app
Section titled “An administrator creates it, not your app”The sequence is:
-
An administrator in the organization connects their GitHub identity to WAMP Cloud, which requires them to authorize WAMP against the GitHub organizations they can act for.
-
They choose the exact repositories your installation may use, and for each one whether it is read-only or read-and-publish. Each choice produces one grant.
-
They hand your integration the grant ids, which you store against your own tenant record.
Practically, that means the first-run experience of your product includes a
step where you tell the customer’s administrator which repositories to
authorize, and then you read the result back. Plan for the case where they
authorize nothing: a session with no source still works, it just has an empty
workspace.
Reading grants back
Section titled “Reading grants back”There are two read paths under /v1/repository-grants — a list and a
replacement lookup — and both are installation-only: a human-delegated
bearer gets 403 installation_principal_required, because a grant belongs to an
installation and there is nothing coherent to return for a person.
Both calls require wamp.cloud.repositories:read. This machine-readable
capability is intentionally separate from the human-only
wamp.cloud.repositories:manage: an integration may discover only the grants
an administrator already assigned to it, but it cannot connect, widen, or
revoke repository access.
The response shape, when a caller can reach it, is the same thin projection described above:
{ "repositoryGrants": [ { "id": "b41d0e2a-1f7c-4d3e-9a10-8c5b2f0d7e64", "repository": { "fullName": "acme/payments-api", "defaultBranch": "main", "private": true }, "operations": ["CHECKOUT", "PUBLISH"] } ]}Take the selected grant id from the list response and store it with the tenant record on your side. Discovery also provides a liveness signal for this source type:
curl -sS "$WAMP_API/v1/capabilities" \ -H "Authorization: Bearer $WAMP_TOKEN"{ "capabilities": { "repositories": { "sources": [ { "kind": "public_https", "available": true }, { "kind": "github_repository_grant", "available": true, "discovery": "/v1/repository-grants" } ] } }}github_repository_grant.available is true only when the calling installation
actually holds at least one active grant, so it answers “may I offer this
customer a repository at all?” before you render a picker. It does not tell you
which repository — that is what your stored grant id is for. A human-delegated
credential always sees false here, because grants belong to installations.
Attach it to a session
Section titled “Attach it to a session”Pass the grant id as the session’s source when you create the session. The
grant id is the only repository input the API accepts — you never send a URL, a
branch ref you have not verified, or a token.
SESSION_ID=$(uuidgen | tr 'A-Z' 'a-z')
curl -sS -X PUT "$WAMP_API/v1/sessions/$SESSION_ID" \ -H "Authorization: Bearer $WAMP_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "task": "Add pagination to the invoices endpoint and open a draft PR.", "title": "Invoice pagination", "model": "claude-sonnet-5", "source": { "kind": "github", "grantId": "b41d0e2a-1f7c-4d3e-9a10-8c5b2f0d7e64", "baseBranch": "main" } }'| Field | Rule |
|---|---|
kind |
github |
grantId |
UUID of a grant this installation holds |
baseBranch |
Optional, 1–255 characters, no NUL / CR / LF. Defaults to the repository’s default branch. |
The session response echoes a richer source than you sent, because the server
resolves and pins the base at creation time:
{ "source": { "kind": "github", "grantId": "b41d0e2a-1f7c-4d3e-9a10-8c5b2f0d7e64", "label": "acme/payments-api", "private": true, "baseBranch": "main", "baseOid": "9f2c1b7e4a6d5c3b2a190f8e7d6c5b4a39281706" }}baseOid is the exact commit the work started from. It does not move when
someone pushes to main during the run, which is what makes a later
publication reviewable against a fixed base. source, like task, is
immutable — PATCH /v1/sessions/{sessionId} accepts only title, model and
runtime.
The other two source shapes:
{ "kind": "public", "url": "https://github.com/acme/example.git", "label": "acme/example" }A public clone needs no grant. The URL must be HTTPS (http:// is rejected),
at most 2048 characters, and carry no embedded credentials. Omitting source
entirely gives the session an empty workspace, which is the right choice for
tasks that are not about an existing codebase.
What the sandbox receives
Section titled “What the sandbox receives”The agent works in a real git checkout, and it never holds GitHub authority.
| In the sandbox | Detail |
|---|---|
| Working tree | /workspace/project when the session has a source; /workspace when it does not. |
| Checkout | Depth-1, fetched by exact object id, so the tree is baseOid and nothing else. |
| Branch | A server-derived working branch under wamp/publications-v1/. You do not name it, and the agent does not commit onto your base branch. |
| Base | Kept as refs/remotes/origin/<baseBranch> at baseOid, which is what a review diffs against. |
origin |
The canonical repository URL, and credential-free. |
| Fetch and push | Brokered through WAMP with a short-lived ticket bound to one operation, revoked as soon as that operation finishes. |
| GitHub token | Never present. Not in the environment, not in git config, not on disk. |
The consequence for your integration is worth stating plainly: an agent that goes wrong cannot push to an unrelated branch, cannot reach a repository the administrator did not grant, and cannot exfiltrate a credential that would let someone else do either. Publishing is a separate, server-mediated command — see Open a pull request.
When a grant is replaced
Section titled “When a grant is replaced”Grants are immutable, so every scope change is a revoke plus a create. An
administrator who upgrades acme/payments-api from read-only to
read-and-publish leaves the grant id you stored pointing at a revoked row. Your
next session creation would fail.
The intended repair path is GET /v1/repository-grants/{grantId}/replacement,
which is designed exactly for this: hand it the id you stored and it tells you
what that authorization is now. It sits behind the same unsettled capability as
the grant list, so today it answers 403 rather than a grant — but the semantics
are worth knowing, because they are what your recovery flow should assume once it
is reachable:
{ "repositoryGrant": { "id": "7c9a4f01-38be-4a15-b6d2-5e0a1c3f8d92", "repository": { "fullName": "acme/payments-api", "defaultBranch": "main", "private": true }, "operations": ["CHECKOUT", "PUBLISH"] }}The semantics are exact:
- If the grant you named is still active, you get it back unchanged.
- If it was revoked and the same installation now holds an active grant for the
same repository that includes
CHECKOUT, you get that grant. Store its id in place of the old one. - Otherwise
repositoryGrantisnull— access was withdrawn rather than changed, and the honest response to your customer is to ask the administrator to grant it again.
A null there is not an error, so do not treat it as one. Real errors from that
path are 400 invalid_input for a malformed id and
403 cloud_repository_grant_required when the grant service refuses the lookup.
Until it is reachable, the recovery path is the failure itself. Creating a
session against a revoked grant answers 403 cloud_repository_grant_required, and
so does submitting a turn or publishing on a session whose grant lost the
operation it needs. Handle that code as “this customer’s authorization changed”:
surface it as a re-authorize prompt naming the repository, take the fresh grant
id from the administrator, and write it over the one you stored. Do not retry the
same grant id — nothing about it will become valid again.
Related
Section titled “Related”- Open a pull request — turning the resulting work into a reviewable, mergeable PR.
- Repository grants operations — the generated endpoint reference.
- Sessions operations — the full
sourceandoriginfield constraints. - Errors — every status this flow can return.