google-workspace-setup

star 452

One-time setup for gws: install, OAuth, scopes, auto-approve.

Lilac-Labs By Lilac-Labs schedule Updated 6/10/2026

name: google-workspace-setup description: "One-time setup for gws: install, OAuth, scopes, auto-approve." license: MIT compatibility: "macOS and Linux. Requires Homebrew (or another package manager) and a Google account." metadata: gini: version: 3.8.0 author: Gini platforms: [macos, linux] prerequisites: commands: [gws]


Google Workspace Setup

One-time onboarding for the Google Workspace skills (google-gmail, google-calendar, google-drive, google-docs, google-sheets, google-meet, google-forms). Installs gws and gcloud, signs the user into their own Google Cloud project, enables the Workspace APIs, captures an OAuth Desktop client through the inline Connect form, and completes gws auth login.

The OAuth client lives in the user's own GCP project. The Client ID and Client Secret are captured through the inline Connect form (request_connector tool) and stored in Gini's encrypted secret store — never write them to chat or logs, and never write client_secret.json to disk.

This skill is idempotent — re-running it re-verifies the install and lets the user widen scopes.

Provisioning vs signing in. The work splits in two: provisioning (install, gcloud auth login, project, APIs, request_connector) runs once, ever; signing in (gws auth login) runs again whenever the user's gws session expires. gcloud is only ever needed for provisioning — never for a re-auth. Step 0 decides which case you're in.

The Flow

This is the exact first-time sequence (Step 0 short-circuits the re-auth case before you reach it). Within the first-time flow do not branch into shortcuts, do not pre-ask whether they have an existing OAuth client, do not list completed actions retrospectively. Status messages are action-oriented: what the user must do next.

  1. The user asks Gini to do a Workspace thing (read mail, check calendar, share a Drive file, etc.).
  2. Confirm setup with the user.
  3. Install gws and gcloud silently in the background.
  4. Run gcloud auth login, which pops up the user's default browser for sign-in.
  5. After they sign in, create the Cloud project and enable the seven Workspace APIs in the background.
  6. Send a single chat bubble with the last-step instructions (two Cloud Console URLs) and call request_connector — the inline form renders below the bubble.
  7. After the user pastes the credentials and clicks Save, ask for a tag for this account and call skill_run google-account-login, which pops up the user's default browser for OAuth consent and registers the tagged account.
  8. After they sign in, the original ask resumes.

Step 0 — First-time or re-auth?

Before anything else, call list_connectors and look for a connector named google-workspace-oauth.

  • It exists → the OAuth client is already provisioned and only the user's gws session expired. This is a re-auth, not setup. Ask once ("Your Google sign-in expired — want me to sign you back in?") and on yes go straight to Step 6, which now signs in by calling skill_run google-account-login for the relevant account. Re-auth re-uses the expired account's existing config dir so it keeps its id and tag — pass that account's configDir (and its tag), NOT a fresh login (which would mint a duplicate) and NOT adopt: true (which skips login and only works on an already-valid session):

    • A tagged account (from the "Connected Google accounts" block) whose session expired → skill_run google-account-login with { tag: "<that account's tag>", configDir: "<that account's configDir from the block>" }.
    • The default-dir session (~/.config/gws) that has EXPIRED → { tag: "<its tag>", configDir: "~/.config/gws" } (non-adopt, so it actually re-logs in).

    Then run Step 8 (smoke test). Do not run gcloud, create a project, or call request_connector — provisioning already happened and none of it is needed again. (Edge case: if gws is not on $PATH, run Step 2's install first, then Step 6. If login fails with invalid_client, the stored client is broken — fall through to the full first-time flow to re-provision it.)

  • It does not exist → true first-time setup. Continue to Step 1.

Step 1 — Confirm setup

Tell the user, in one short sentence, that Google Workspace isn't set up yet, and ask whether to set it up now. Wait for confirmation before doing anything.

If they say yes, proceed silently — do not narrate each substep. The user sees a chat bubble per milestone (sign in, last step), not per command.

Step 2 — Install gws and gcloud

Both installs are silent and run through terminal_exec. If a binary is already on $PATH, skip its install.

Detect first:

command -v gws
command -v gcloud

Install whichever is missing:

# gws (macOS / Linux)
brew install googleworkspace-cli

# gcloud (macOS)
brew install --cask google-cloud-sdk

# gcloud (Linux) — see https://docs.cloud.google.com/sdk/docs/install for the
# tarball install. Use the platform-appropriate command via terminal_exec.

Verify both are on $PATH afterwards:

gws --version
gcloud --version

If either install fails (network, sudo, broken Homebrew), STOP and tell the user verbatim what failed and the one-line command to try manually. Do not loop.

Step 3 — Sign in with gcloud

gcloud auth login

This opens the user's default browser to Google's OAuth consent page. They sign in there. The command returns when the user completes consent.

If gcloud auth list already shows an active account, ask once: "gcloud is signed in as <email>. Use this account?" — proceed on confirmation. Otherwise run gcloud auth login straight through.

Step 4 — Reach a Cloud project with the seven APIs enabled

The goal of this step is a single value, <PROJECT_ID>, that names a Cloud project the active gcloud account owns AND in which the seven Workspace APIs are enabled. Every later step substitutes that string into URLs and gcloud --project= flags.

Invariants for this step and every later gcloud call:

  • Pass --project=<PROJECT_ID> explicitly on every gcloud command that accepts it. Don't rely on gcloud config get-value project; its value can carry over from an unrelated shell or a prior failed run, which is exactly how the wrong-project PERMISSION_DENIED failure happens.
  • Treat <PROJECT_ID> as a value you discover, not a value you write. Read it from the output of projects list or projects create, then thread it through.
  • If the user named a specific project earlier in this chat ("use my work project acme-data-1234"), use that as <PROJECT_ID> and skip straight to 4c. Don't second-guess by listing or creating.

4a. Look for an ACTIVE Gini-managed project first

gcloud projects list --filter="projectId:gini-workspace-* lifecycleState:ACTIVE" --format="value(projectId)"

The filter is on project ID prefix, which we control — not display name, which collides freely. Every project this skill creates has an ID starting with gini-workspace-. If the command returns one or more IDs, pick the first as <PROJECT_ID> and skip ahead to 4d. A prior setup run already provisioned it; making another would just leave orphans.

4b. Otherwise, undelete a recently-deleted one

Google retains deleted project IDs in DELETE_REQUESTED state for ~30 days. During that window the name is reserved (so creating with the same ID fails with ALREADY_EXISTS) but the project can be restored with one call. Check before creating:

gcloud projects list --filter="projectId:gini-workspace-* lifecycleState:DELETE_REQUESTED" --format="value(projectId)"

If this returns one or more IDs, pick the most recent and undelete it:

gcloud projects undelete <PROJECT_ID>

Undelete is a single API call — it does NOT count against the per-minute project-create write quota the way gcloud projects create does, so this also avoids RATE_LIMIT_EXCEEDED during heavy testing. Most enabled APIs survive the undelete, but treat that as best-effort — 4d will re-enable any that didn't.

Skip ahead to 4d once undelete succeeds. (Service Usage takes 30-90 s to re-recognize the project after undelete; 4d's services enable retries internally so it tolerates the gap.)

4c. Otherwise, create a fresh one

The bare ID gini-workspace is almost always globally claimed — don't waste an attempt on it. Derive a deterministic, account-scoped suffix from the gcloud account so re-runs converge instead of accumulating projects:

gcloud config get-value account
# → something like "shelden@lilaclabs.ai"

Take the part before @, lowercase, and strip to [a-z0-9-]. The project-ID cap is 30 chars; gini-workspace- is 15, so truncate the suffix to 15 chars if needed.

gcloud projects create gini-workspace-<suffix> --name="Gini Workspace"

If even that ID is taken AND no DELETE_REQUESTED match was found in 4b (rare — only when a different user with the same email local-part already claimed it globally), append a 4-char random tiebreaker. <PROJECT_ID> is whichever ID succeeded.

If gcloud projects create errors with RATE_LIMIT_EXCEEDED for cloudresourcemanager.googleapis.com.write_requests, the user has burned through Google's per-account project-create quota (usually from repeated testing). Surface the error verbatim and ask: "Google is rate-limiting project creates; the quota typically clears in ~10 minutes. Want to wait and retry, or do you have an existing Cloud project I can use? Reply with a project ID or 'wait'." Do not loop the create call.

Organization-restricted accounts. If create instead errors with PERMISSION_DENIED and a message like You do not have permission to create projects, the account belongs to an organization (common on managed @company Google Workspace accounts) whose policy reserves project creation for admins. Accepting the Terms of Service will not change this. Surface the error verbatim and ask: "Your Google account can't create Cloud projects — your Workspace admin restricts that. Reply with an existing Cloud project ID I should use, or set Gini up with a personal @gmail.com account instead." Take a project ID as the new <PROJECT_ID> and resume from projects describe in 4d.

4d. Verify access, then enable the APIs

projects describe is the cheapest probe that fails fast if the active account doesn't own <PROJECT_ID> — much better than learning it from services enable's permission error after the model has already committed to a flow.

gcloud projects describe <PROJECT_ID> --format="value(projectId)"

On success, enable the seven APIs (already-enabled ones are no-ops, which is fine when 4a reused an existing project):

gcloud services enable \
  gmail.googleapis.com \
  calendar-json.googleapis.com \
  drive.googleapis.com \
  docs.googleapis.com \
  sheets.googleapis.com \
  forms.googleapis.com \
  meet.googleapis.com \
  --project=<PROJECT_ID>

Calendar's service ID is calendar-json.googleapis.com (not calendar.googleapis.com).

If either 4d command errors with PERMISSION_DENIED, the active account doesn't own <PROJECT_ID>. Surface the error verbatim and ask the user briefly: "I don't have access to <PROJECT_ID>. Which project should I use?" Take their answer as the new <PROJECT_ID> and re-run from projects describe. Do not fall back to gcloud config get-value project to recover — that's the same stale value that produced the original failure.

Step 5 — Last step: capture OAuth Desktop credentials

This is the only step that requires the user to click in a browser. Send one chat bubble with the two Cloud Console URLs and call request_connector immediately after. The inline form renders below the bubble; the user pastes the Client ID and Client Secret and clicks Save.

Construct the reason string as multi-line markdown with the URLs and click instructions. Substitute <PROJECT_ID> with the actual project id from Step 4 — there is no runtime substitution.

Use this exact format:

**Last step.** Complete the two Cloud Console pages below, then paste the credentials.

**Step 1 — OAuth consent screen** (skip if already configured)

https://console.cloud.google.com/apis/credentials/consent?project=<PROJECT_ID>

- User Type: **External**
- App name: **Gini Workspace**
- Your email for support contact and developer contact
- Save through Scopes (no scopes to add)
- Add yourself as a **Test user**

**Step 2 — Create an OAuth client**

https://console.cloud.google.com/apis/credentials?project=<PROJECT_ID>

- Click **Create Credentials → OAuth client ID**
- Application type: **Desktop app**
- Name it whatever (e.g. "Gini")
- Click **Create**

Then paste the **Client ID** and **Client Secret** below.

Then call:

request_connector {
  provider: "google-oauth-desktop",
  reason: "<the constructed markdown string above, with <PROJECT_ID> filled in>"
}

Do NOT post a separate chat message before the tool call. Do NOT open <url> for either Console URL — let the user click from the bubble. Don't gate on "reply done" between the two pages — the form submission is what advances the flow.

On Save, the connector is created with env bindings (GOOGLE_WORKSPACE_CLI_CLIENT_ID, GOOGLE_WORKSPACE_CLI_CLIENT_SECRET), and the chat-task resumes at Step 6.

Tagging the account

Before login, ask the user for a short tag for THIS account — e.g. "personal", "work", "school". On first-time setup it's fine to ask "What should I call this account?" The tag labels the account everywhere (it's how Gini will later refer to it and how the user picks which account to use). Tags are unique across accounts.

Step 6 — Sign in with skill_run google-account-login

Now that the google-workspace-oauth connector exists, delegate the login to the google-account-login skill — do NOT run gws auth login from terminal_exec yourself. That skill's script gets the Client ID/Secret injected into its env, mints this account's own config dir, opens the user's browser to the consent screen, waits for them to finish, and registers the tagged account:

skill_run {
  skill: "google-account-login",
  script: "account-login",
  args: { tag: "<the tag the user chose>", services: ["drive","gmail","calendar","docs","sheets","meet","forms"] }
}

The script opens the user's default browser automatically and blocks until they complete consent — sign-in is human-in-the-loop, so don't type their email or password. It returns { ok, id, tag, email, configDir, scopes } on success, or { ok: false, error } on failure. On success, tell the user which account (by email + tag) is now connected, then go to Step 8.

If ok is false, surface the error verbatim and act on it:

  • "gws never printed the consent URL" or a gws auth login failure → the user may have closed the browser without approving; just call skill_run again (idempotent). If it mentions invalid_client, the stored Client ID/Secret are wrong — re-run request_connector for google-oauth-desktop (Step 5). A redirect_uri mismatch means the OAuth client was created as Web type, not Desktop — re-create as Desktop and re-paste.
  • "Login did not produce a valid session." → the user didn't finish consent; offer to try again.

Always request every service we enabled APIs for in services, regardless of what the user originally asked. The user enabled APIs for all seven Workspace products in Step 4, and Google's consent screen renders each scope as its own row with per-scope checkboxes (for unverified apps in testing mode) — the user picks which to grant there, not by us pre-filtering the services list. Narrowing services to just "calendar" because the user's first ask was a calendar question silently locks them out of Drive / Gmail / Docs / etc. for the rest of the session.

When the user wants different scopes than the default

Two cases warrant deviating from the all-seven-services default — pass them through the args:

  • The user explicitly asks for a narrower or read-only grant ("I only use Gmail, skip the rest" / "give Gini read-only access"). Trust them: pass services: ["gmail"] and/or readonly: true.
  • The user is on a personal @gmail.com account AND wants the "full" Gmail scope (https://mail.google.com/, which includes permanent delete). The default recommended preset will fail on unverified personal apps; pass the full scope URL(s) via scopes instead, which overrides services.
# Read-only across the user's chosen services
args: { tag: "...", services: ["gmail","drive"], readonly: true }

# Exact per-scope picks (full URLs) — for the rare case the user names a
# specific scope shape service names can't express
args: { tag: "...", scopes: ["https://www.googleapis.com/auth/gmail.readonly","https://www.googleapis.com/auth/drive"] }

services takes service names, not scope strings — services: ["gmail.readonly"] is silently dropped; use scopes with full URLs for that.

Reference table of services name ↔ full scope URL (only useful when the user names a specific scope shape; the default is the full seven-service services list, not anything from this table):

  • Gmail-s gmailhttps://www.googleapis.com/auth/gmail.modify (read + send + reply + label + draft; NOT permanent delete). Narrower: .readonly / .send / .compose.
  • Gmail (full, incl. permanent delete)--scopes "https://mail.google.com/". No -s shorthand.
  • Drive-s drivehttps://www.googleapis.com/auth/drive (.file, .readonly, .metadata.readonly available as full URLs).
  • Calendar-s calendarhttps://www.googleapis.com/auth/calendar (.events, .readonly, .freebusy available as full URLs).
  • Docs-s docshttps://www.googleapis.com/auth/documents (.readonly available).
  • Sheets-s sheetshttps://www.googleapis.com/auth/spreadsheets (.readonly available as a full URL).
  • Meet-s meethttps://www.googleapis.com/auth/meetings.space.created (.readonly available).
  • Forms-s formshttps://www.googleapis.com/auth/forms.body (.body.readonly, .responses.readonly available).

Never pass --full or the default recommended preset on a personal @gmail.com account — those expand to 80+ scopes including pubsub and cloud-platform, which an unverified app cannot grant. The seven-service list stays under the ~25-scope cap.

Adding another account later

When the user wants to connect an additional Google account (one OAuth client can authorize many accounts), there's no new project, connector, or gcloud work — the existing google-workspace-oauth connector / Cloud project is re-used. Just:

  1. Ask the user for a tag for the new account ("What should I call this one?").

  2. If the Cloud app is still in testing mode, the new account's email must be a Test user on the OAuth consent screen, or consent will fail. Give them the consent-screen URL to add it (substitute the project id): https://console.cloud.google.com/apis/credentials/consent?project=<PROJECT_ID>

  3. Call skill_run google-account-login with the new tag (NOT adopt):

    skill_run {
      skill: "google-account-login",
      script: "account-login",
      args: { tag: "<new tag>", services: ["drive","gmail","calendar","docs","sheets","meet","forms"] }
    }
    

This signs the second account into its own config dir and registers it alongside the first.

Adopting the existing sign-in

Use adopt: true ONLY to IMPORT an already-valid ~/.config/gws session as a tagged account without a fresh login — e.g. the user had a pre-existing gws setup and you just want it on the account list. It never opens the browser, never touches ~/.config/gws's tokens, and fails if that dir's session is not currently live.

skill_run {
  skill: "google-account-login",
  script: "account-login",
  args: { tag: "<tag>", adopt: true }
}

Do NOT use adopt: true to fix an expired default-dir session — adopt skips login, so it can't re-auth. To re-log-in the default dir, pass configDir: "~/.config/gws" (non-adopt) per Step 0's re-auth path. Rule of thumb: adopt = import a valid session; configDir = re-auth / target a specific existing dir with a real login.

Step 7 — Stop the per-call approval prompt (optional)

Every gws call goes through Gini's approval-gated terminal_exec tool. To stop the prompt firing on every invocation, patch the per-instance auto-approve list:

curl -X PATCH -H "Authorization: Bearer $TOKEN" -H "content-type: application/json" \
  -d '{"patterns": ["gws *"]}' \
  http://127.0.0.1:<port>/api/settings/auto-approve

Set $TOKEN from ~/.gini/instances/<instance>/config.json (the apiToken field).

For finer-grained gating, list each product:

{ "patterns": ["gws gmail *", "gws calendar *", "gws drive *", "gws docs *"] }

Step 8 — Smoke test

A read-only call that exercises auth:

gws drive files list --params '{"pageSize": 1}'

If that returns JSON without an auth error, the setup is complete. Resume the user's original ask (read mail, list calendar events, etc.).

Rules

  1. Walk this skill end-to-end on first-time setup. Do not skip to request_connector or the google-account-login skill without the install + project + APIs in place. The one exception is the re-auth path (Step 0): when the google-workspace-oauth connector already exists, skill_run google-account-login alone is the whole job — gcloud, project creation, and request_connector are provisioning-only and must not re-run.
  2. Sign-in is a human-in-the-loop step. Never attempt to type the user's email or password. gcloud auth login and the google-account-login script both open the default browser — wait for the command (or skill_run) to return.
  3. Capture credentials through the inline form, not files. Always use request_connector { provider: "google-oauth-desktop" }. Never ask the user for a path to client_secret.json, never write a JSON file under ~/.config/gws/, and never cat or echo the credentials back into chat.
  4. Enable all seven Workspace APIs in Step 4 regardless of which product triggered setup. One gcloud services enable call covers them all; this lets the user pivot to another product later without re-running setup.
  5. Status messages are action-oriented and ungrouped. Do not list "Installed gws, installed gcloud, signed in, created project, enabled APIs." The user sees a chat bubble per milestone (confirm setup, last-step form, done) — not a retrospective changelog.
  6. Fail gracefully. If gcloud errors with PERMISSION_DENIED or ALREADY_EXISTS, surface the error verbatim and ask the user. If an install fails, STOP — do not retry in a loop, hand off to the user with the one-line manual command.
  7. The services you pass to google-account-login include every service we enabled APIs for in Step 4, not just the one the user happened to ask about. Google's consent screen renders each scope as its own checkbox row in testing mode — the user picks there. Narrowing services based on the current request silently locks the user out of the other six surfaces; they'd have to re-run setup the next time they want anything else. The only time you narrow is when the user explicitly says so ("read-only," "Gmail only," etc.).
  8. If the user is in a CI or headless environment, point them at the export flow (gws auth export --unmasked > credentials.json on a desktop machine, then GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE=… on the headless one).

Manual Fallback

If gcloud cannot be installed at all (uncommon — Homebrew is the standard path on macOS, and Linux has a documented tarball install), hand off the Cloud Console flow to the user manually:

  1. Tell them to open https://console.cloud.google.com/ and create a project named gini-workspace.
  2. Enable the seven APIs at https://console.cloud.google.com/apis/library — Gmail, Calendar, Drive, Docs, Sheets, Forms, Meet.
  3. Then resume from Step 5 (configure OAuth consent, create Desktop OAuth client, paste credentials into the inline form).

For flags not shown here, run gws auth --help and gws --help.

Install via CLI
npx skills add https://github.com/Lilac-Labs/gini-agent --skill google-workspace-setup
Repository Details
star Stars 452
call_split Forks 107
navigation Branch main
article Path SKILL.md
More from Creator