From 5e30f403fa75a85262f91e1f9e73ae5bb1f22948 Mon Sep 17 00:00:00 2001 From: jpoehnelt-bot Date: Thu, 5 Mar 2026 16:50:10 -0700 Subject: [PATCH 1/3] docs: document all environment variables and enable CONFIG_DIR override (#171) --- .changeset/document-env-vars.md | 5 ++++ .env.example | 41 +++++++++++++++++++++++---------- AGENTS.md | 41 +++++++++++++++++++++++++++++---- README.md | 20 ++++++++++++++++ src/auth_commands.rs | 5 ++-- src/main.rs | 13 +++++++++++ 6 files changed, 106 insertions(+), 19 deletions(-) create mode 100644 .changeset/document-env-vars.md diff --git a/.changeset/document-env-vars.md b/.changeset/document-env-vars.md new file mode 100644 index 0000000..216abf7 --- /dev/null +++ b/.changeset/document-env-vars.md @@ -0,0 +1,5 @@ +--- +"@googleworkspace/cli": patch +--- + +Document all environment variables and enable GOOGLE_WORKSPACE_CLI_CONFIG_DIR in release builds diff --git a/.env.example b/.env.example index c1b15c8..89dda10 100644 --- a/.env.example +++ b/.env.example @@ -1,18 +1,35 @@ -# OAuth Client Credentials -# Create these at https://console.cloud.google.com/apis/credentials -GOOGLE_WORKSPACE_CLI_CLIENT_ID= -GOOGLE_WORKSPACE_CLI_CLIENT_SECRET= +# gws — Google Workspace CLI +# Copy this file to .env and uncomment the variables you need. +# All variables are optional. See README.md for details. -# Authentication -# Path to a service account JSON key file or user credentials +# ── Authentication ──────────────────────────────────────────────── +# Pre-obtained OAuth2 access token (highest priority; bypasses all credential loading) +# GOOGLE_WORKSPACE_CLI_TOKEN= + +# Path to OAuth credentials JSON (user or service account) # GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE= -# Impersonation (Domain-Wide Delegation) -# Email address of the user to impersonate when using a service account +# Default account email for multi-account usage (overridden by --account flag) +# GOOGLE_WORKSPACE_CLI_ACCOUNT= + +# Email of user to impersonate via Domain-Wide Delegation (service accounts only) # GOOGLE_WORKSPACE_CLI_IMPERSONATED_USER= -# Model Armor Sanitization -# Default template resource name for --sanitize -# GOOGLE_WORKSPACE_CLI_SANITIZE_TEMPLATE=projects/my-project/locations/us-central1/templates/my-template -# Sanitization mode: 'warn' (default) or 'block' +# ── OAuth Client ────────────────────────────────────────────────── +# OAuth client ID and secret (alternative to saving client_secret.json) +# GOOGLE_WORKSPACE_CLI_CLIENT_ID= +# GOOGLE_WORKSPACE_CLI_CLIENT_SECRET= + +# ── Configuration ───────────────────────────────────────────────── +# Override the config directory (default: ~/.config/gws) +# GOOGLE_WORKSPACE_CLI_CONFIG_DIR= + +# ── Model Armor (response sanitization) ────────────────────────── +# Default Model Armor template (overridden by --sanitize flag) +# GOOGLE_WORKSPACE_CLI_SANITIZE_TEMPLATE= +# Sanitization mode: warn (default) or block # GOOGLE_WORKSPACE_CLI_SANITIZE_MODE=warn + +# ── Helpers ─────────────────────────────────────────────────────── +# GCP project ID fallback for gmail watch and events subscribe (overridden by --project) +# GOOGLE_WORKSPACE_PROJECT_ID= diff --git a/AGENTS.md b/AGENTS.md index d6cf6d5..4c61e10 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -165,7 +165,40 @@ Use these labels to categorize pull requests and issues: ## Environment Variables -- `GOOGLE_WORKSPACE_CLI_TOKEN` — Pre-obtained OAuth2 access token (highest priority; bypasses all credential file loading) -- `GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE` — Path to OAuth credentials JSON (no default; if unset, falls back to credentials secured by the OS Keyring and encrypted in `~/.config/gws/`) -- `GOOGLE_WORKSPACE_CLI_ACCOUNT` — Default account email for multi-account usage (overridden by `--account` flag) -- Supports `.env` files via `dotenvy` +### Authentication + +| Variable | Description | +|---|---| +| `GOOGLE_WORKSPACE_CLI_TOKEN` | Pre-obtained OAuth2 access token (highest priority; bypasses all credential file loading) | +| `GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE` | Path to OAuth credentials JSON (no default; if unset, falls back to credentials secured by the OS Keyring and encrypted in `~/.config/gws/`) | +| `GOOGLE_WORKSPACE_CLI_ACCOUNT` | Default account email for multi-account usage (overridden by `--account` flag) | +| `GOOGLE_WORKSPACE_CLI_IMPERSONATED_USER` | Email of user to impersonate with Domain-Wide Delegation (service accounts only) | +| `GOOGLE_APPLICATION_CREDENTIALS` | Standard Google ADC path; used as fallback when no gws-specific credentials are configured | + +### Configuration + +| Variable | Description | +|---|---| +| `GOOGLE_WORKSPACE_CLI_CONFIG_DIR` | Override the config directory (default: `~/.config/gws`) | + +### OAuth Client + +| Variable | Description | +|---|---| +| `GOOGLE_WORKSPACE_CLI_CLIENT_ID` | OAuth client ID (for `gws auth login` when no `client_secret.json` is saved) | +| `GOOGLE_WORKSPACE_CLI_CLIENT_SECRET` | OAuth client secret (paired with `CLIENT_ID` above) | + +### Sanitization (Model Armor) + +| Variable | Description | +|---|---| +| `GOOGLE_WORKSPACE_CLI_SANITIZE_TEMPLATE` | Default Model Armor template (overridden by `--sanitize` flag) | +| `GOOGLE_WORKSPACE_CLI_SANITIZE_MODE` | `warn` (default) or `block` | + +### Helpers + +| Variable | Description | +|---|---| +| `GOOGLE_WORKSPACE_PROJECT_ID` | GCP project ID fallback for `gmail watch` and `events subscribe` helpers (overridden by `--project` flag) | + +All variables can also live in a `.env` file (loaded via `dotenvy`). diff --git a/README.md b/README.md index 0b0e54b..151a9ee 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ npm install -g @googleworkspace/cli - [AI Agent Skills](#ai-agent-skills) - [MCP Server](#mcp-server) - [Advanced Usage](#advanced-usage) +- [Environment Variables](#environment-variables) - [Architecture](#architecture) - [Troubleshooting](#troubleshooting) - [Development](#development) @@ -356,6 +357,25 @@ gws gmail users messages get --params '...' \ | `GOOGLE_WORKSPACE_CLI_SANITIZE_TEMPLATE` | Default Model Armor template | | `GOOGLE_WORKSPACE_CLI_SANITIZE_MODE` | `warn` (default) or `block` | +## Environment Variables + +All variables are optional. See [`.env.example`](.env.example) for a copy-paste template. + +| Variable | Description | +|---|---| +| `GOOGLE_WORKSPACE_CLI_TOKEN` | Pre-obtained OAuth2 access token (highest priority) | +| `GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE` | Path to OAuth credentials JSON (user or service account) | +| `GOOGLE_WORKSPACE_CLI_ACCOUNT` | Default account email (overridden by `--account` flag) | +| `GOOGLE_WORKSPACE_CLI_IMPERSONATED_USER` | Email for Domain-Wide Delegation (service accounts) | +| `GOOGLE_WORKSPACE_CLI_CLIENT_ID` | OAuth client ID (alternative to `client_secret.json`) | +| `GOOGLE_WORKSPACE_CLI_CLIENT_SECRET` | OAuth client secret (paired with `CLIENT_ID`) | +| `GOOGLE_WORKSPACE_CLI_CONFIG_DIR` | Override config directory (default: `~/.config/gws`) | +| `GOOGLE_WORKSPACE_CLI_SANITIZE_TEMPLATE` | Default Model Armor template | +| `GOOGLE_WORKSPACE_CLI_SANITIZE_MODE` | `warn` (default) or `block` | +| `GOOGLE_WORKSPACE_PROJECT_ID` | GCP project ID fallback for helper commands | + +Environment variables can also be set in a `.env` file (loaded via [dotenvy](https://crates.io/crates/dotenvy)). + ## Architecture `gws` uses a **two-phase parsing** strategy: diff --git a/src/auth_commands.rs b/src/auth_commands.rs index 64991e5..c9b12e1 100644 --- a/src/auth_commands.rs +++ b/src/auth_commands.rs @@ -92,7 +92,6 @@ const READONLY_SCOPES: &[&str] = &[ ]; pub fn config_dir() -> PathBuf { - #[cfg(test)] if let Ok(dir) = std::env::var("GOOGLE_WORKSPACE_CLI_CONFIG_DIR") { return PathBuf::from(dir); } @@ -266,7 +265,7 @@ async fn handle_login(args: &[String]) -> Result<(), GwsError> { } // Determine scopes: explicit flags > interactive TUI > defaults - let mut scopes = resolve_scopes( + let scopes = resolve_scopes( &filtered_args, project_id.as_deref(), services_filter.as_ref(), @@ -277,7 +276,7 @@ async fn handle_login(args: &[String]) -> Result<(), GwsError> { // gmail.metadata blocks query parameters like `q`, and is redundant // when broader scopes (gmail.modify, gmail.readonly, mail.google.com) // are already included. - let scopes = filter_redundant_restrictive_scopes(scopes); + let mut scopes = filter_redundant_restrictive_scopes(scopes); let secret = yup_oauth2::ApplicationSecret { client_id: client_id.clone(), diff --git a/src/main.rs b/src/main.rs index d0fa709..a0d0a45 100644 --- a/src/main.rs +++ b/src/main.rs @@ -454,6 +454,19 @@ fn print_usage() { println!( " GOOGLE_WORKSPACE_CLI_ACCOUNT Default account email for multi-account" ); + println!( + " GOOGLE_WORKSPACE_CLI_IMPERSONATED_USER Email for Domain-Wide Delegation (service accounts)" + ); + println!( + " GOOGLE_WORKSPACE_CLI_CONFIG_DIR Override config directory (default: ~/.config/gws)" + ); + println!(" GOOGLE_WORKSPACE_CLI_SANITIZE_TEMPLATE Default Model Armor template"); + println!( + " GOOGLE_WORKSPACE_CLI_SANITIZE_MODE Sanitization mode: warn (default) or block" + ); + println!( + " GOOGLE_WORKSPACE_PROJECT_ID GCP project ID fallback for helper commands" + ); println!(); println!("COMMUNITY:"); println!(" Star the repo: https://github.com/googleworkspace/cli"); From bb5b747f351694c0b29d95d3fff1a6be1bb2359c Mon Sep 17 00:00:00 2001 From: jpoehnelt-bot Date: Thu, 5 Mar 2026 17:04:12 -0700 Subject: [PATCH 2/3] docs: clarify env vars are trusted inputs in AGENTS.md --- AGENTS.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 4c61e10..4ea9873 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -84,6 +84,9 @@ ASCII art title cards live in `art/`. The `scripts/show-art.sh` helper clears th > [!IMPORTANT] > This CLI is frequently invoked by AI/LLM agents. Always assume inputs can be adversarial — validate paths against traversal (`../../.ssh`), restrict format strings to allowlists, reject control characters, and encode user values before embedding them in URLs. +> [!NOTE] +> **Environment variables are trusted inputs.** The validation rules above apply to **CLI arguments** that may be passed by untrusted AI agents. Environment variables (e.g. `GOOGLE_WORKSPACE_CLI_CONFIG_DIR`) are set by the user themselves — in their shell profile, `.env` file, or deployment config — and are not subject to path traversal validation. This is consistent with standard conventions like `XDG_CONFIG_HOME`, `CARGO_HOME`, etc. + ### Path Safety (`src/validate.rs`) When adding new helpers or CLI flags that accept file paths, **always validate** using the shared helpers: From 04e31e911b52cc3091d2a366a5af8c89397a2cf3 Mon Sep 17 00:00:00 2001 From: jpoehnelt-bot Date: Thu, 5 Mar 2026 17:06:10 -0700 Subject: [PATCH 3/3] chore: add Gemini Code Assist style guide --- .gemini/style_guide.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .gemini/style_guide.md diff --git a/.gemini/style_guide.md b/.gemini/style_guide.md new file mode 100644 index 0000000..67b4bf1 --- /dev/null +++ b/.gemini/style_guide.md @@ -0,0 +1,26 @@ +# Code Review Style Guide + +## Project Architecture + +`gws` is a Rust CLI that dynamically generates commands from Google Discovery Documents at runtime. It does NOT use generated Rust crates (`google-drive3`, etc.) for API interaction. Do not suggest adding API-specific crates to `Cargo.toml`. + +## Security: Trusted vs Untrusted Inputs + +This CLI is frequently invoked by AI/LLM agents. CLI arguments may be adversarial. + +- **CLI arguments (untrusted)** — Must validate paths against traversal (`../../`), reject control characters, percent-encode URL path segments, and use `reqwest .query()` for query parameters. Validators: `validate_safe_output_dir()`, `validate_safe_dir_path()`, `encode_path_segment()`, `validate_resource_name()`. +- **Environment variables (trusted)** — Set by the user in their shell profile, `.env` file, or deployment config. Do NOT flag missing path validation on environment variable values. This is consistent with `XDG_CONFIG_HOME`, `CARGO_HOME`, etc. + +## Test Coverage + +The `codecov/patch` check requires new/modified lines to be covered by tests. Prefer extracting testable helper functions over embedding logic in `main`/`run`. Tests should cover both happy paths and rejection paths (e.g., pass `../../.ssh` and assert `Err`). + +## Changesets + +Every PR must include a `.changeset/.md` file. Use `patch` for fixes/chores, `minor` for features, `major` for breaking changes. + +## Code Style + +- Rust: `cargo clippy -- -D warnings` must pass. `cargo fmt` enforced via pre-commit hook. +- Node.js: Use `pnpm` not `npm`. +- OAuth scope strings in test code will trigger "restricted/sensitive scope" warnings — these are expected and should be ignored.