# Connect a repository Attach an administrator-authorized private GitHub repository to a Cloud session, understand what the sandbox actually receives, and recover when the authorization behind a stored grant id changes. Source: https://docs.cloud.vampikez.fun/guides/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 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](/concepts/repositories/#the-grant-is-the-authority-model). The publish side is [Publications](/concepts/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 No `/v1` endpoint creates, widens, or revokes a grant. Granting access to a private repository is a decision a human with repository-management rights makes in the WAMP Cloud web app, holding a capability (`wamp.cloud.repositories:manage`) that is only ever granted to people. An installation token cannot perform it, by design — a stolen backend credential must not be able to reach new source code. 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. ## 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`: ```json { "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: ```bash curl -sS "$WAMP_API/v1/capabilities" \ -H "Authorization: Bearer $WAMP_TOKEN" ``` ```ts check export async function repositoryCapabilities(cloud: WampCloud) { const capabilities = await cloud.capabilities(); const { sources, publications, merges } = capabilities.repositories; return { sources, publications, merges }; } ``` ```json { "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. `GET /v1/capabilities` is gated on `wamp.cloud.sessions:create`, not on `sessions:read`. An installation consented only to read cannot call discovery at all. ## 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. ```bash 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": "", "source": { "kind": "github", "grantId": "b41d0e2a-1f7c-4d3e-9a10-8c5b2f0d7e64", "repositoryId": "1029384756", "baseBranch": "main" } }' ``` ```ts check 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: ```json { "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: ```json { "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 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/` 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](/guides/open-a-pull-request/). ## 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: ```json { "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. A grant that backs an existing session cannot be deleted from underneath it — the database restricts it. Revocation stops new sessions and new publications; it does not corrupt sessions already running. ## Related - [Open a pull request](/guides/open-a-pull-request/) — turning the resulting work into a reviewable, mergeable PR. - [Repository grants operations](/api/operations/tags/repository-grants/) — the generated endpoint reference. - [Sessions operations](/api/operations/tags/sessions/) — the full `source` and `origin` field constraints. - [Errors](/reference/errors/) — every status this flow can return.