Google OAuth interactive guide
Official Kody guide
A coding agent looks up the official Kody guides, then walks a naive user through Lane B Google OAuth — bring-your-own client for Gmail inbox reading — one console step at a time.
Example of a conversation you might have with your agent.
Find the path
You I want Kody to read my Gmail inbox for invoices from Acme. I have no idea how to connect Google — can you walk me through it?
Reasoning I do not already know the Google connect steps. I will search for official Kody guides on Google, Gmail, and OAuth.
searchFind official guides for Google Gmail OAuthmemoryContext{ "task": "Connect Google so Kody can read my Gmail inbox for Acme invoices", "entities": [ "Google", "Gmail", "Acme invoices" ] }query"google gmail inbox oauth"ReturnsconversationId: 8g4q1n6m2s9t Tool conversation id; pass it back on subsequent search/execute calls. # Search results For full detail on entity-backed hits, call `search` with `entity: "{id}:{type}"`. 1. **capability** `coding_guide_get` (`coding`) — Load an official Kody guide (markdown, bundled from the kody repository). Prefer this capability plus `search` results over local repo spelunking when Kody auth or integration behavior is already documented. Entity: `coding_guide_get:capability` `kody.coding_guide_get(args)` — `type CodingGuideGetInput = { guide: "integration_bootstrap" | "oauth" | "provider_google" | ... }`; use entity detail for the full definition 2. **guide** Connect Google (Gmail, Calendar, Drive) — Built-in Calendar scopes vs bring-your-own OAuth for inbox reading. Entity: `provider_google:guide` 3. **guide** OAuth guide (standard path) — Hosted `/connect/oauth`, redirect URI, PKCE vs confidential. Entity: `oauth:guide`
searchOpen coding_guide_get for the guide idsconversationId"8g4q1n6m2s9t"entity"coding_guide_get:capability"ReturnsconversationId: 8g4q1n6m2s9t Tool conversation id; pass it back on subsequent search/execute calls. # Capability — `coding_guide_get` Load an official Kody guide (markdown, bundled from the kody repository). Use `guide: "integration_bootstrap"` before building anything that depends on third-party auth. Use `guide: "oauth"` for the hosted `/connect/oauth` path and redirect URI. Use `guide: "provider_google"` for Google console steps, built-in vs BYO lanes, and the Testing-status refresh-token trap. ## Summary - Entity: `coding_guide_get:capability` - Domain: `coding` - Required input fields: `guide` ## Execute from `execute` ```ts import { kody } from 'kody:runtime' export default async function main(input = {}) { return await kody.coding_guide_get(input) } ```
executeLoad bootstrap, OAuth, and Google provider guidesconversationId"8g4q1n6m2s9t"memoryContext{ "task": "Connect Google so Kody can read my Gmail inbox for Acme invoices", "entities": [ "Google", "Gmail", "Acme invoices" ] }codeimport { kody } from 'kody:runtime' export default async function main() { const bootstrap = await kody.coding_guide_get({ guide: 'integration_bootstrap', }) const oauth = await kody.coding_guide_get({ guide: 'oauth' }) const google = await kody.coding_guide_get({ guide: 'provider_google' }) return { bootstrap, oauth, google } }ReturnsconversationId: 8g4q1n6m2s9t Tool conversation id; pass it back on subsequent search/execute calls. { "bootstrap": { "title": "Integration bootstrap guide", "body": "# Integration bootstrap guide\n\nRead this guide first when a user wants a package, package app, or workflow that depends on a third-party integration.\n\n## Core rule\n\nDo not save or present an auth-dependent package as complete until a minimal authenticated smoke test succeeds.\n\n…" }, "oauth": { "title": "OAuth guide (standard path)", "body": "# OAuth guide\n\nSend the signed-in user to `https://kody.codes/connect/oauth` with query parameters that describe the provider.\n\n## Redirect URI\n\nThe redirect URI is `https://kody.codes/connect/oauth`.\n\n…" }, "google": { "title": "Connect Google (Gmail, Calendar, Drive)", "body": "# Connect Google (Gmail, Calendar, Drive)\n\nPrefer the built-in Google integration when its scope menu covers the task. Bring your own Google OAuth client when you need access outside that menu — especially inbox reading.\n\n## Lane A: built-in Google (default)\n\n…\n\n## Lane B: bring-your-own OAuth client\n\n…" } }Reasoning Inbox reading (`gmail.readonly`) is outside the built-in Google scope menu. That means Lane B: bring-your-own OAuth client in Google Cloud, not the one-click `/connect/oauth?provider=google` path.
Agent Reading your Gmail inbox needs a Google Cloud OAuth client you own — the built-in Google connect only covers scopes like Calendar and send-only Gmail, not inbox reading. I will walk you through the console one step at a time. Open https://console.cloud.google.com and create a new project (or select one you already use). Reply when that is done.
Same conversation — one console step at a time
Build the OAuth client
You I created a project. What is next?
Agent In `APIs & Services → Library`, enable `Gmail API` for that project. Skip Calendar and Drive unless you need them for this task. Reply when Gmail API shows as enabled.
You Done, what is next?
Agent Open `APIs & Services → Google Auth Platform` and click `Get started`. Fill in `Branding` (app name and support email), set `Audience` to `External`, and add your contact email. Reply when the wizard finishes.
You Done.
Agent On the `Clients` tab, click `Create Client`, choose application type `Web application`, and add this redirect URI exactly: `https://kody.codes/connect/oauth`. Copy the client ID and client secret when Google shows them — you will paste those into Kody later, not here.
You What is a redirect URI? And is it okay if I paste the client secret here so you can save it?
Agent A redirect URI is the URL the provider sends the browser back to after you approve access. For Kody it is always `https://kody.codes/connect/oauth` — register it exactly, no trailing slash. Do not paste the client secret in chat. When we get to the connect form, paste the client ID and client secret only on that Kody page.
You Okay, client is created. Next?
Agent On the `Data Access` tab, add this scope: `https://www.googleapis.com/auth/gmail.readonly`. That is the inbox-read scope this invoice task needs.
You Added. Done, what is next?
Agent While publishing status is `Testing`, open the `Audience` tab and add your own Google account as a test user. Reply when you are listed.
You I am a test user. Can we connect now? It already works in Testing.
Agent Almost — but stay on `Testing` and refresh tokens expire after seven days. Everything looks fine during setup, then the integration dies a week later with `invalid_grant` on refresh. On the `Audience` page, click `Publish app` to move to `Production`. Google shows an “unverified app” warning on consent; for a personal app that is expected — click Advanced, then continue to your app when you authorize. Reply when status is Production.
You Published. Ready for the Kody side.
Same conversation — connect on Kody, then prove it
Connect and verify
Agent Open this prefilled connect link while signed in to Kody: https://kody.codes/connect/oauth?provider=google&authorizeUrl=https%3A%2F%2Faccounts.google.com%2Fo%2Foauth2%2Fv2%2Fauth&tokenUrl=https%3A%2F%2Foauth2.googleapis.com%2Ftoken&flow=confidential&scopes=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fgmail.readonly&allowedHosts=gmail.googleapis.com&extraAuthorizeParams=%7B%22access_type%22%3A%22offline%22%2C%22prompt%22%3A%22consent%22%7D Paste the client ID and client secret into that form (confidential flow). Authorize Google when prompted. The link already requests `gmail.readonly`, allows `gmail.googleapis.com`, and sets `access_type=offline` with `prompt=consent` so you get a refresh token.
You Here is my client secret — can I paste it here so you finish setup?
Agent No — never paste a client secret into chat. Delete that message if you can. Paste the client ID and client secret only on the Kody `/connect/oauth` form I linked, then authorize in the browser.
You I authorized. What is next?
Reasoning I will smoke-test the Gmail profile endpoint with the saved Google integration before we call this done.
executeSmoke-test Gmail with the saved Google integrationconversationId"8g4q1n6m2s9t"memoryContext{ "task": "Connect Google so Kody can read my Gmail inbox for Acme invoices", "entities": [ "Google", "Gmail", "Acme invoices" ] }codeimport { createAuthenticatedFetch } from 'kody:runtime' export default async function main() { const googleFetch = await createAuthenticatedFetch('google') const response = await googleFetch( 'https://gmail.googleapis.com/gmail/v1/users/me/profile', ) if (!response.ok) { throw new Error( `Gmail smoke test failed: ${response.status} ${await response.text()}`, ) } return await response.json() }ReturnsconversationId: 8g4q1n6m2s9t Tool conversation id; pass it back on subsequent search/execute calls. { "emailAddress": "you@example.com", "messagesTotal": 1842, "threadsTotal": 921, "historyId": "123456" }
executeList a couple of recent message subjectsconversationId"8g4q1n6m2s9t"memoryContext{ "task": "Connect Google so Kody can read my Gmail inbox for Acme invoices", "entities": [ "Google", "Gmail", "Acme invoices" ] }codeimport { createAuthenticatedFetch } from 'kody:runtime' export default async function main() { const googleFetch = await createAuthenticatedFetch('google') const listResponse = await googleFetch( 'https://gmail.googleapis.com/gmail/v1/users/me/messages?maxResults=2', ) if (!listResponse.ok) { throw new Error(`Gmail list failed: ${listResponse.status}`) } const list = (await listResponse.json()) as { messages?: Array<{ id: string }> } const subjects: Array<string> = [] for (const message of list.messages ?? []) { const detailResponse = await googleFetch( `https://gmail.googleapis.com/gmail/v1/users/me/messages/${message.id}?format=metadata&metadataHeaders=Subject`, ) if (!detailResponse.ok) continue const detail = (await detailResponse.json()) as { payload?: { headers?: Array<{ name: string; value: string }> } } const subject = detail.payload?.headers?.find( (header) => header.name.toLowerCase() === 'subject', )?.value if (subject) subjects.push(subject) } return { subjects } }ReturnsconversationId: 8g4q1n6m2s9t Tool conversation id; pass it back on subsequent search/execute calls. { "subjects": [ "Acme Invoice #4412", "Your weekly digest" ] }Agent Connected. Your Gmail profile loads, and recent mail includes an Acme invoice subject. Later reconnects can use `/connect/oauth?provider=google` alone — Kody already has the endpoints from this setup.