Skip to content

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.

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.

The sequence is:

  1. 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.

  2. 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.

  3. 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.

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:

Terminal window
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/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.

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.

Terminal window
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"
}
}'
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.

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.

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 repositoryGrant is null — 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.