# Gmail

> Connect Gmail to LightHouse: the ways to connect, the read-only scope, where the credential is held, and what is indexed.

**Built in the engine.** The engine reads one labelled mailbox: the messages carrying a label you name, in a mailbox you name. A whole mailbox is never the default and cannot be asked for.

## Connect in LightHouse

You need the **Client administrator** (`client_admin`) role, signed in as a person. Google has no approval screen that LightHouse can send you to (the access is granted in your own Google Cloud and Workspace consoles), so Gmail connects with a service-account key only.

### Before you start

1. In your own Google Cloud project, create a service account, and create a JSON key for it. The card’s **Open Gmail’s token page** link opens [console.cloud.google.com/apis/credentials](https://console.cloud.google.com/apis/credentials).
2. In your Google Workspace admin console, grant the service account’s client ID domain-wide delegation for `https://www.googleapis.com/auth/gmail.readonly` and no other scope.

### Connect

1. Go to **Setup and admin → Connectors**. Under **Documents and knowledge**, on the **Gmail** card, choose **Connect**. The page is headed **Connect Gmail**.

2. Under **Three ways to connect it**, fill in:

   - **Mailbox label**: the one label whose messages are read, for example `agents/inbox`. A whole mailbox is never read.
   - **Read as** (required): the mailbox owner’s address, for example `ops@example.com`. A service account reads a mailbox by acting as its owner, and there is no default.

3. Under **3. Paste an access token** (the only route offered), paste the whole JSON key file into **Service-account key file**. Paste it as it is, with its line breaks.

4. Choose **Connect**.

What happens next depends on [where the credential is held](/connect/#where-every-credential-is-held):

- **Where your deployment keeps credentials in your own store**, the page seals the key file in your browser, with the **Read as** person, and LightHouse never sees it. The page says *“Waiting for your deployment … to keep it in your own store. Nothing is connected until it says so.”* Once your deployment has kept it, the page says the connection is connected and where the key is, and your deployment checks it from your account: a one-hour token for `https://www.googleapis.com/auth/gmail.readonly`, acting as the **Read as** person, then `GET /gmail/v1/users/me/profile`. LightHouse cannot run the checks below on a key it never sees.
- **Otherwise**, the button reads **Checking it…** while LightHouse asks Google for a token. On success the page says *“Gmail: … is connected, read only, healthy.”*, naming the service account, followed by a note that the key is held sealed under your key until your deployment supports your own store. The card’s health detail reads *“Gmail answered, and the key mints a read-only token.”*

What LightHouse checks, in order, for a key it sees:

1. The file is a service-account key (`"type": "service_account"`) with a `client_email` and a `private_key`, and its `token_uri`, if present, is Google’s own (`https://oauth2.googleapis.com/token`). A key is sent to Google and nowhere else.
2. LightHouse signs a request for a one-hour token for `https://www.googleapis.com/auth/gmail.readonly` alone acting as the **Read as** person, and Google issues it.
3. **Every** scope Google says the token carries must be `https://www.googleapis.com/auth/gmail.readonly`. A key whose token carries anything more is refused, not narrowed.

A service-account key does not expire, so the connection never shows as expiring.

Gmail is offered read only. Unlike most connectors, it has no **Read and write** level: a request for `access: read_write` is refused with `422 invalid`. See [Access levels](/connect/#access-levels-read-or-read-and-write).

### If it is refused

The reason appears in red under the form.

| The page says                                                                       | Why                                                                                                                                                                                                                             | What to do                                                                                  |
| ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| *Gmail: the credential is the service-account key file, as JSON*                    | What was pasted is not JSON                                                                                                                                                                                                     | Paste the whole downloaded file                                                             |
| *Gmail: that JSON is not a service-account key (its type is not service_account) …* | An OAuth client file, or another JSON file, was pasted                                                                                                                                                                          | Download a key from the service account itself                                              |
| *Gmail: the service-account key has no client_email* (or *private_key*)             | The file is incomplete                                                                                                                                                                                                          | Download a new key                                                                          |
| *Gmail: that key’s token_uri is not Google’s own …*                                 | The file names another token address                                                                                                                                                                                            | Use an unmodified key from Google                                                           |
| *Gmail: that service-account key’s private_key could not be read*                   | The private key is damaged, often by pasting it on one line                                                                                                                                                                     | Paste the file again with its line breaks                                                   |
| *Gmail: name the mailbox to read as `user` …*                                       | **Read as** is empty                                                                                                                                                                                                            | Enter the mailbox owner’s address                                                           |
| *Gmail: `user` is an email address in your domain*                                  | **Read as** is not an email address                                                                                                                                                                                             | Enter an address such as `ops@example.com`                                                  |
| *Gmail: the approval was refused*                                                   | Google refused to issue the token: for example, the domain-wide delegation does not grant `https://www.googleapis.com/auth/gmail.readonly` to this service account’s client ID, or the **Read as** person is not in your domain | Check the delegation in your Google Workspace admin console                                 |
| *Gmail: Google says that key’s token carries …, which is more than reading …*       | The token has a scope beyond `https://www.googleapis.com/auth/gmail.readonly`                                                                                                                                                   | Grant the service account `https://www.googleapis.com/auth/gmail.readonly` and nothing else |
| *Gmail could not be reached*, or *Gmail answered an error; try again shortly*       | Google did not answer, or answered with a server error (`503 provider_unavailable`)                                                                                                                                             | Try again shortly                                                                           |

Where the key would be held under your tenant’s key and that key is unavailable, LightHouse stores nothing and answers `503 secret_store_unavailable`; try again, and tell Atlantic if it persists. Where your deployment does not keep it, the page shows **Not kept** with the reason in words; see [A connection exists once its credential is kept](/connect/#a-connection-exists-once-its-credential-is-kept).

The API equivalent is `POST /api/v1/connectors/gmail/token`, from a person’s portal session only. See [Post Token](/api/reference/read/#post-token) in the reference.

> **Reading this connection in your deployment:**
>
> A connection kept in your own store is read by a source card that names it with `credential_secret: <connection id>`, set up in code (below): your deployment mints a one-hour token from the stored key file, and the source never sees the key. A connection held under your tenant’s key is not passed to your deployment: the source reads Gmail with an access token your deployment mints itself.

## From a terminal

`lighthouse-ai connect gmail --credential-stdin` connects a key file you pipe in, for example `lighthouse-ai connect gmail --credential-stdin --target mailbox_label=agents/inbox --target user=ops@example.com < key.json`. It runs as you, exactly as if you had pasted the key on the Connect page, and the CLI seals the key file on your machine where your deployment keeps credentials in your own store. See [The CLI](/cli/#connecting-a-provider).

## Connect in code

The engine takes an OAuth **access token** with the `gmail.readonly` scope, from the environment variable `GMAIL_TOKEN`. It holds no refresh token: minting a fresh access token is your platform’s job, for example a service account with domain-wide delegation, or workload identity federation. Because access tokens expire within an hour, a leaked run directory holds no long-lived credential.

```yaml
- id: ops-correspondence
  purpose: Read the labelled operations mailbox into the knowledge base.
  executor: python
  target: aal_engine.ingest.connector:run_card
  overseer: ops@example.com
  budget_usd: 0
  effect: internal
  source:
    kind: gmail
    mailbox_label: ops-inbox     # required; a whole mailbox is never read
    label: confidential
    requires:
      reads: [messages carrying the ops-inbox label, with their headers and bodies]
      reason: the operations agents answer correspondence from what was said before
```

## Scopes

| Scope                                            | Why                                            |
| ------------------------------------------------ | ---------------------------------------------- |
| `https://www.googleapis.com/auth/gmail.readonly` | List and read the labelled messages. Read only |

Whichever way you connect, the scope is the same one and no other is accepted. A service-account key whose delegation grants anything broader is refused when you connect it: Google is asked what the minted token carries, and **every** scope it answers with has to be this one.

## Where the credential is held

- **A LightHouse connection, in your own secrets store**, where your deployment can keep it: the key file is sealed to your deployment and written by it under `lighthouse/connectors/gmail/<connection id>`, as the fields `service_account` and `subject`. LightHouse never holds it in a form it can read.
- **A LightHouse connection, under your tenant’s key**, until your deployment can keep it: the service-account key is held encrypted in LightHouse under a KMS key used for your tenant alone, and is never shown again.
- **A source set up in code**: in your own account. Whatever mints the access token (a service-account key in your secret store, or federation) stays there; the engine sees only a short-lived token in `GMAIL_TOKEN`.

See [Where every credential is held](/connect/#where-every-credential-is-held).

## What is indexed

- **Messages** carrying the label you named, as plain text (the plain-text part, or the HTML part with its tags removed), with their subject, correspondents and date.
- **Never read:** anything outside that label, and attachments. A document that matters belongs in a folder or drive the engine reads.
- **How often:** on the card’s schedule. Each pull reads the mailbox’s history feed since the last one.
- **Removals:** a message that loses the label, or is deleted, is removed from the corpus, so agents stop citing mail your team refiled. Gmail keeps history for about a week; if the stored position has expired, the next pull lists the label again in full and removes what is no longer there.

## Agents that draft replies

The Gmail connector cannot send mail: it only issues reads, and nothing in it calls a send endpoint. The one tool it offers that writes, `aal__gmail__create_draft`, creates a Gmail **draft** and nothing more. A draft reaches outside the run, so it is an external effect:

- the agent’s card must list the tool among its `effect_tools`;
- the tool policy must allow that agent the tool *with approval*;
- the call is refused unless it carries a named person’s approval from the orchestrator;
- at autonomy 0 the policy forbids it outright.

A person then reviews and sends the draft from Gmail. See [Approvals](/concepts/approvals/).

## Check it worked

- The source’s run note: `written N, unchanged N, quarantined N, removed N`.
- **Agents → Knowledge**: documents from `gmail`.

## Removing it

- **In LightHouse**: on the Gmail card, choose **Disconnect**, then **Yes, disconnect** (`DELETE /api/v1/connections/{cid}`). LightHouse keeps the connection’s record and fingerprint for the audit trail. In your own store, Disconnect sends your deployment `connector.revoke`: it deletes the key from your store and answers `provider_manual`, and the card names where to revoke it, *Google Cloud: IAM, Service accounts, Keys; and the domain-wide delegation in the Admin console*. Under your tenant’s key, LightHouse deletes the encrypted key.
- **At Google**: LightHouse cannot revoke a service-account key. Delete the key, or withdraw its domain-wide delegation, in your own Google Cloud and Workspace consoles.
- **In your deployment**: remove the source card and its secret. Documents already written stay in the corpus until you delete them.

Source: https://docs.atlanticagenticlabs.com/connect/gmail/
