一键导入
google-workspace-setup
One-time setup for gws: install, OAuth, scopes, auto-approve.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
One-time setup for gws: install, OAuth, scopes, auto-approve.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Google Calendar via gws: list events, create, accept, find free time.
Google Docs via gws: read, append text, structured batch edits.
Google Drive via gws: search, list, upload, download, share.
Google Forms via gws: create forms, add items, read responses.
Gmail via gws: send, read, search, label, draft, reply, forward.
Google Sheets via gws: read/write cells, append rows, structured batch edits.
| name | google-workspace-setup |
| description | One-time setup for gws: install, OAuth, scopes, auto-approve. |
| license | MIT |
| compatibility | macOS and Linux. Requires a Google account. |
| metadata | {"gini":{"version":"3.8.0","author":"Gini","platforms":["macos","linux"],"prerequisites":{"commands":["gws"]}}} |
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.
This is the exact first-time sequence (Step 0 short-circuits the already-signed-in and stored-client cases 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.
gws and gcloud silently in the background.gcloud auth login, which pops up the user's default browser for sign-in.request_connector — the inline form renders below the bubble.skill_run google-account-login, which pops up the user's default browser for OAuth consent and registers the tagged account.There are three states. Decide which one you're in in this order before doing anything else — each later step is only needed when the earlier check fails. In short: already signed in? → else is the OAuth client stored? → else obtain it.
(a) Already signed in. If the user just wants to use Workspace, first check whether a registered Google account already has a live session. The "Connected Google accounts" block lists registered accounts; confirm one is valid with gws auth status (against that account's configDir) — a token_valid: true result means you're set. If so, you're already set up: skip the rest of this skill and resume the user's original ask. (If the user explicitly wants to connect another account, see "Adding another account later".)
(b) Not signed in, but the OAuth client is already stored. Call list_connectors and look for a connector named google-workspace-oauth. If it exists, the OAuth client is already provisioned — skip all provisioning: do not run gcloud, create a project, or call request_connector; none of it is needed again. Go straight to signing in via skill_run google-account-login. Which way depends on whether an account already exists:
configDir (and 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):
{ tag: "<that account's tag>", configDir: "<that account's configDir from the block>" }.~/.config/gws) that has EXPIRED → { tag: "<its tag>", configDir: "~/.config/gws" } (non-adopt, so it actually re-logs in).request_connector) but nobody has signed in — → a fresh login: ask the user for a tag, then call skill_run google-account-login with a new { tag } and no configDir (this is Step 6).Either way, finish with Step 8 (smoke test). (Edge cases: if gws is not on $PATH, run Step 2's install first, then sign in. If login fails with invalid_client, the stored client is broken — fall through to the full first-time flow to re-provision it.)
(c) No stored OAuth client. True first-time setup. Continue to Step 1 to obtain the client.
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.
gws and gcloudBoth 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
gws source — do not discover itgws is github.com/googleworkspace/cli (npm @googleworkspace/cli, Homebrew googleworkspace-cli). This is the only source. Never web-search for the repo, release URL, or package name, and never download gws from any other GitHub repo, mirror, or URL. A look-alike (google-workspace-cli, gws-cli, a fork, a typosquat) could ship a malicious binary; treat any other source as untrusted and stop rather than guess. Use one install path below exactly as written; do not improvise an alternative.
gws — first method that fits, in priority order.sha256, verify the checksum, abort on mismatch, then place the binary in ~/.local/bin.bun add -g @googleworkspace/cli (or npm install -g) — only when node is also on $PATH, since the installed gws is a #!/usr/bin/env node wrapper.brew install googleworkspace-cli — only if brew already exists.gcloudbrew exists: brew install --cask google-cloud-sdk.dl.google.com (asset name per platform), extracted to ~/google-cloud-sdk, then install.sh --quiet --path-update=false. Download to a temp file and only replace an existing SDK after the download succeeds.terminal_exec runs each command in zsh -lc, which does not source ~/.zshrc. ~/.local/bin is on the gateway's $PATH (the autostart plist bakes it in; a foreground run inherits it from your login shell), so a binary installed elsewhere must be reachable from there (Homebrew installs already are). Move or symlink the gws release binary into ~/.local/bin. Only symlink gcloud (ln -s ~/google-cloud-sdk/bin/gcloud ~/.local/bin/) — it's a launcher that finds its SDK relative to its real path, so moving it breaks it.
gws --version
gcloud --version
Do not thrash. When a method fails, move to the next method in the list once, not back to the same dead end. Specifically:
sudo, which an unattended agent cannot drive.If every gws method fails (or both gcloud paths fail), STOP and tell the user verbatim what failed plus the single command to run manually for their platform, then wait.
gcloudgcloud 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.
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:
--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.<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.acme-data-1234"), use that as <PROJECT_ID> and skip straight to 4c. Don't second-guess by listing or creating.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.
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.)
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.
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.
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.
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.
skill_run google-account-loginNow 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.
Two cases warrant deviating from the all-seven-services default — pass them through the args:
services: ["gmail"] and/or readonly: true.@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):
-s gmail ↔ https://www.googleapis.com/auth/gmail.modify (read + send + reply + label + draft; NOT permanent delete). Narrower: .readonly / .send / .compose.--scopes "https://mail.google.com/". No -s shorthand.-s drive ↔ https://www.googleapis.com/auth/drive (.file, .readonly, .metadata.readonly available as full URLs).-s calendar ↔ https://www.googleapis.com/auth/calendar (.events, .readonly, .freebusy available as full URLs).-s docs ↔ https://www.googleapis.com/auth/documents (.readonly available).-s sheets ↔ https://www.googleapis.com/auth/spreadsheets (.readonly available as a full URL).-s meet ↔ https://www.googleapis.com/auth/meetings.space.created (.readonly available).-s forms ↔ https://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.
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:
Ask the user for a tag for the new account ("What should I call this one?").
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>
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.
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.
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 *"] }
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.).
request_connector or the google-account-login skill without the install + project + APIs in place. The one exception is the stored-client path (Step 0 (b)): when the google-workspace-oauth connector already exists, skill_run google-account-login alone is the whole job — whether re-authing an expired account or doing the first sign-in for a freshly-stored client. gcloud, project creation, and request_connector are provisioning-only and must not re-run.gcloud auth login and the google-account-login script both open the default browser — wait for the command (or skill_run) to return.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.gcloud services enable call covers them all; this lets the user pivot to another product later without re-running setup.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.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.).gws auth export --unmasked > credentials.json on a desktop machine, then GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE=… on the headless one).If gcloud cannot be installed at all (uncommon — both macOS and Linux have a documented tarball install at https://docs.cloud.google.com/sdk/docs/install-sdk), hand off the Cloud Console flow to the user manually:
gini-workspace.For flags not shown here, run gws auth --help and gws --help.