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 app installation inside one organization to every repository that organization’s WAMP GitHub App installation covers. It is not an OAuth scope 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, and the whole GitHub App installation it names. |
| Permits | Everything that GitHub App installation can do on the repositories the grant reaches: clone them, push branches, open and merge pull requests. |
| Uniqueness | At most one active grant per GitHub App installation. |
| Mutability | None. A change of reach is a new grant. |
The grant stores no repository list at all. Which repositories it reaches is GitHub’s live answer — each git operation mints an installation token scoped to the one repository it is about, so a repository that has left the installation is refused by GitHub rather than by a stale snapshot.
A grant names no operations. There is one live-or-not answer, and holding a live grant is the whole of it — the same grant that clones a repository opens pull requests on it and pushes branches to it. What actually lands is GitHub’s decision, described in Repositories. The publish side is Publications.
What your app ever sees of a grant is its id. No tokens, no clone URLs, no
GitHub installation ids. The picker is a different
resource: GET /v1/repositories returns each live repository with its
grantId and repositoryId. 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 git exchange instead.
An administrator creates it, not your app
Section titled “An administrator creates it, not your app”The sequence is:
-
An administrator opens Repository access for your installation. It opens the GitHub section in Cloud Settings. The way in is either the installation’s generic configuration link in Account Center or Manage GitHub access in the Cloud repository picker. Both open the same Cloud settings pane; Account Center contains no GitHub-specific UI.
-
They add a GitHub App installation, if the organization has none yet. This installs the WAMP GitHub App on a GitHub organization or user account, and which repositories it reaches is chosen there, on GitHub — WAMP asks that question nowhere else and stores no answer to it.
-
They allow the account for your installation. One grant, naming the GitHub App installation and no repository: which repositories it covers stays GitHub’s answer, asked live on every request.
The separate Your GitHub authorization section is personal and self-service. It adds repositories only to that person’s Cloud picker; it neither connects an organization installation nor grants any WAMP application access for other members.
A grant is authority, not a catalogue. WAMP stores no repository list for it, so what the installation reaches can change on GitHub without anything here going stale.
Nobody hands you a grant id. There is no id on that screen to copy, and there does
not need to be: the administrator’s choice is which GitHub accounts may be
used, and your installation reads the usable repositories back from the API. So the first-run
experience of your product is a step where you tell the customer’s administrator
what to authorize, and then you read the result back yourself, as the next
section describes. Plan for the case where they
authorize nothing: a session with no source still works, it just has an empty
workspace.
Reading usable repositories
Section titled “Reading usable repositories”The picker is GET /v1/repositories. It requires
wamp.cloud.repositories:read and an installation principal — a
human-delegated bearer gets 403 installation_principal_required. This
machine-readable capability is intentionally separate from the human-only
wamp.cloud.repositories:manage: an integration may discover only the
repositories an administrator already granted to it, but it cannot connect,
widen, or revoke repository access.
The response is a page of live repositories. Each row is the exact pair you send
when creating a session, including the provider repositoryId:
{ "repositories": [ { "grantId": "b41d0e2a-1f7c-4d3e-9a10-8c5b2f0d7e64", "repositoryId": "1029384756", "fullName": "acme/payments-api", "defaultBranch": "main", "private": true } ], "nextCursor": null}Page with cursor and limit (1–100, default 50). nextCursor is null at
the end; there is no hasMore. A malformed cursor or limit is 400 invalid_input. Store grantId and repositoryId with the tenant record on
your side.
GET /v1/repository-grants lists the underlying authority records. Use it for
administration and for healing a stored grant id
([replacement](#when-a-grant-is-replaced)), not as a repository picker: a
grant carries no repository object and no provider id.
Discovery also provides a liveness signal for this source type:
curl -sS "$WAMP_API/v1/capabilities" \ -H "Authorization: Bearer $WAMP_TOKEN"import type { WampCloud } from '@wamp/app-sdk';
export async function repositoryCapabilities(cloud: WampCloud) { const capabilities = await cloud.capabilities(); const { sources, publications, merges } = capabilities.repositories; return { sources, publications, merges };}{ "capabilities": { "repositories": { "sources": [ { "kind": "public_https", "available": true }, { "kind": "github_repository_grant", "available": true, "discovery": "/v1/repositories" } ], "publications": [ { "kind": "github_draft_pull_request", "available": true }, { "kind": "github_direct_branch_push", "available": true } ], "merges": [ { "kind": "github_pull_request_merge", "available": true, "strategies": ["merge", "squash", "rebase"], "modes": ["now", "when_ready"] } ] } }}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 GET /v1/repositories. A human-delegated
credential always sees false here, because grants belong to installations.
The publications and merges entries answer the next question in the same
call, and all three turn on the same fact: one live grant reaches every delivery,
and an installation holding none reaches none. Read them here rather than
discovering at publish time that this customer authorized nothing.
Attach it to a session
Section titled “Attach it to a session”Pass the grantId and repositoryId from GET /v1/repositories as the
session’s source when you create the session. You never send a clone URL, a
token, or a branch ref you have not verified.
SESSION_ID=$(uuidgen | tr 'A-Z' 'a-z')TURN_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 '{ "initialTurn": { "id": "'$TURN_ID'", "message": "Add pagination to the invoices endpoint and open a draft PR." }, "title": "Invoice pagination", "model": "<model-id-from-/v1/capabilities>", "source": { "kind": "github", "grantId": "b41d0e2a-1f7c-4d3e-9a10-8c5b2f0d7e64", "repositoryId": "1029384756", "baseBranch": "main" } }'import { randomUUID } from 'node:crypto';import type { WampCloud } from '@wamp/app-sdk';
export async function startRepositoryWork(cloud: WampCloud, model: string) { const sessionId = randomUUID(); const turnId = randomUUID(); const { session } = await cloud.createSession(sessionId, { initialTurn: { id: turnId, message: 'Add pagination to the invoices endpoint and open a draft PR.', }, title: 'Invoice pagination', model, source: { kind: 'github', grantId: 'b41d0e2a-1f7c-4d3e-9a10-8c5b2f0d7e64', repositoryId: '1029384756', baseBranch: 'main', }, }); return session;}| Field | Rule |
|---|---|
kind |
github |
grantId |
UUID of a grant this installation holds |
repositoryId |
Required GitHub repository id as a decimal string, from GET /v1/repositories. Send it with the exact grantId returned on the same row. |
baseBranch |
Optional, 1–255 characters, no NUL / CR / LF. Defaults to the repository’s default branch. It has to exist already; a name that does not resolve is 400 cloud_repository_branch_not_found. |
Send the pair the picker returned. Do not infer an installation from a
repository name, and do not reuse a repositoryId with a different grant.
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 the create command, 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 change is a revoke plus a create. An administrator who removes a GitHub App installation from your WAMP application and then allows it again leaves the grant id you stored pointing at a revoked row. Your next session creation would fail.
The 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. Like the grant list, it requires
wamp.cloud.repositories:read and an installation principal, and it answers with
the same thin projection:
{ "repositoryGrant": { "id": "7c9a4f01-38be-4a15-b6d2-5e0a1c3f8d92" }}The semantics are exact:
- If the grant you named is still active, you get it back unchanged.
- If it was revoked and the same GitHub App installation now has an active grant, 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 — it is a 200 with
repositoryGrant: null, which is also the answer for a grant id that never
existed. The only error that path answers is 400 invalid_input, for an id that
is not a UUID.
The failure itself is the other half of the recovery path, because you will
often meet it before you think to look up a replacement. 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 was revoked
underneath it. Handle it as “this customer’s authorization changed”: call the
replacement endpoint first, and if it hands back null, surface a re-authorize
prompt naming the repository. Once the administrator has granted it again, page
GET /v1/repositories, match the repository by fullName, and write that
grantId + repositoryId pair over the one you stored — you never ask a human
to read an id to you. 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.