Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Contributing

## Development

```bash
bun install
bun run typecheck
bun run typecheck:live
bun run test
bun run build
```

The tests inject `fetch` implementations and do not call Gmail.

## Live Gmail checks

Use a dedicated Gmail fixture account and a Desktop OAuth client granted only
`gmail.modify`. The read smoke requires a query that matches exactly one
one-message thread:

```bash
GMAIL_LIVE_TEST=1 \
GMAIL_LIVE_CLIENT_ID='...' \
GMAIL_LIVE_CLIENT_SECRET='...' \
GMAIL_LIVE_FIXTURE_QUERY='subject:(interchange-gmail-e2e-fixture)' \
bun run test:live
```

The full live suite has been verified against a dedicated Gmail fixture account.
It exercises every Gmail tool:

- `gmail_search_threads` finds the one-message fixture thread.
- `gmail_get_thread` and `gmail_get_message` retrieve its metadata.
- `gmail_list_labels` lists the account labels.
- `gmail_create_draft` and `gmail_list_drafts` create and find a disposable
draft, which the suite then deletes.
- `gmail_label_message` and `gmail_unlabel_message` add and remove `STARRED`
on the fixture message.
- `gmail_label_thread` and `gmail_unlabel_thread` add and remove `STARRED`
on every message in the fixture thread.

Add `GMAIL_LIVE_MUTATION_TEST=1` to run the draft and label checks. The suite
deletes its draft, restores the fixture labels, and never sends email.

The first run opens Google OAuth and stores the refresh token in the ignored
`.local/gmail-live-token.json` file. Set `GMAIL_LIVE_TOKEN_FILE` when using a
different token file. Credentials and message contents are not logged.
114 changes: 73 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,64 +1,96 @@
# @corbits/google-tools

Google API clients and Interchange tools, starting with Gmail. Authentication
is supplied through the Interchange `gmail-api` mediated credential; the
package never accepts or returns raw tokens.
Gmail tools for AI agents: search, read, label and draft over the Gmail REST API, authenticated by an HTTP credential that signs each request so the agent never holds an OAuth token. A tool pack for the Interchange sidecar that also runs standalone.

## Requirements
## Why @corbits/google-tools?

Node.js 24 or newer (`engines.node >=24`).
1. **No tokens in the agent.** Tools call Gmail through a credential the host resolves at run time. The OAuth token never enters the model context or tool arguments.
2. **Human approval on every tool.** Every tool uses `approval: "ask"`, so Interchange pauses each call until a person approves it or saves an always-allow grant. No tool can send mail.
3. **Grant-gated in Interchange.** The package declares the `gmail-api` credential and the `gmail.modify` scope it needs. The hub (Interchange's control plane) stores the credential, and a grant (a record of which agent may use it) decides which agents get it.

It covers Gmail only, not other Google APIs.

## Install

```bash
bun add github:corbitsdev/google-tools
bun add @corbits/google-tools @intx/agent @intx/types
```

## Working on it
Requires Node.js 24 or newer.

```bash
bun install
bun run typecheck
bun test
bun run build
## Quickstart

Gives an Interchange agent the Gmail tools:

```ts
import { defineAgent } from "@intx/agent";
import { gmail } from "@corbits/google-tools/sidecar-bundle";

export const inboxAgent = defineAgent({
id: "inbox-agent",
systemPrompt: "Triage my Gmail inbox and draft replies for me to review.",
tools: [gmail],
capabilities: [],
inference: { sources: [{ provider: "ollama", model: "gpt-oss:20b" }] },
});
```

The tests inject `fetch` implementations and do not call Gmail.
When the agent runs, the sidecar resolves the `gmail-api` credential the hub
granted it and passes it to the tools. The token stays out of the agent.

## Live Gmail checks
## Where it fits

Use a dedicated Gmail fixture account and a Desktop OAuth client granted only
`gmail.modify`. The read smoke requires a query that matches exactly one
one-message thread:
[Interchange](https://github.com/faremeter/interchange) runs AI agents as principals (accounts that hold their own identity, permissions and credentials).

```bash
GMAIL_LIVE_TEST=1 \
GMAIL_LIVE_CLIENT_ID='...' \
GMAIL_LIVE_CLIENT_SECRET='...' \
GMAIL_LIVE_FIXTURE_QUERY='subject:(interchange-gmail-e2e-fixture)' \
bun run test:live
```
- Runs in the sidecar, the runtime that hosts each agent and its tools.
- Registers its tools with `defineTool` from
[`@intx/agent`](https://github.com/faremeter/interchange/tree/main/packages/agent).
- Reads the Gmail credential from the runtime capabilities in
[`@intx/types`](https://github.com/faremeter/interchange/tree/main/packages/types).
- Pairs with
[`@corbits/oauth-core`](https://github.com/corbitsdev/corbits-oauth-core),
which obtains the Google credential the hub stores.

## Tools

| Tool | Purpose |
| ----------------------- | --------------------------------------------------- |
| `gmail_search_threads` | Search the mailbox and return thread summaries. |
| `gmail_get_thread` | Fetch one thread with selected message detail. |
| `gmail_get_message` | Fetch one message with selected detail. |
| `gmail_list_labels` | List the mailbox labels. |
| `gmail_create_draft` | Create a draft, optionally as a reply. Never sends. |
| `gmail_list_drafts` | List drafts with Gmail-query filtering. |
| `gmail_label_message` | Add labels to a message. |
| `gmail_unlabel_message` | Remove labels from a message. |
| `gmail_label_thread` | Add labels to every message in a thread. |
| `gmail_unlabel_thread` | Remove labels from every message in a thread. |

### Approvals

Every tool declares `approval: "ask"`, including the read-only ones, because
Gmail is third-party data. Interchange pauses each call until a person
responds. Allowing once runs that call; always allowing saves an `allow`
grant for that tool on that agent, so later calls run without asking.

## Using with Interchange

The full live suite has been verified against a dedicated Gmail fixture account.
It exercises every Gmail tool:
The sidecar loads tools from the `./sidecar-bundle` export. Its `gmail` export
builds the tools from the `capabilities` the sidecar puts in the agent's
environment.

- `gmail_search_threads` finds the one-message fixture thread.
- `gmail_get_thread` and `gmail_get_message` retrieve its metadata.
- `gmail_list_labels` lists the account labels.
- `gmail_create_draft` and `gmail_list_drafts` create and find a disposable
draft, which the suite then deletes.
- `gmail_label_message` and `gmail_unlabel_message` add and remove `STARRED`
on the fixture message.
- `gmail_label_thread` and `gmail_unlabel_thread` add and remove `STARRED`
on every message in the fixture thread.
The package's `package.json` tells Interchange what to load:

Add `GMAIL_LIVE_MUTATION_TEST=1` to run the draft and label checks. The suite
deletes its draft, restores the fixture labels, and never sends email.
- `interchange.tools` points at `./dist/sidecar-bundle.js`.
- `interchange.credentials` asks for a credential named `gmail-api` with the
`https://www.googleapis.com/auth/gmail.modify` scope.

The first run opens Google OAuth and stores the refresh token in the ignored
`.local/gmail-live-token.json` file. Set `GMAIL_LIVE_TOKEN_FILE` when using a
different token file. Credentials and message contents are not logged.
To connect it, store the user's Google OAuth credential on the hub as
`gmail-api` and grant it to the agent. The sidecar resolves it for the tools at
run time.

## License

LGPL-2.1-only. See [LICENSE](./LICENSE).
LGPL-2.1-only. See
[LICENSE](https://github.com/corbitsdev/google-tools/blob/main/LICENSE).
Development notes are in [CONTRIBUTING.md](https://github.com/corbitsdev/google-tools/blob/main/CONTRIBUTING.md).
Loading