40 Commits

Author SHA1 Message Date
Justin Poehnelt 2590768222 style: fmt for linter 2026-03-05 01:42:13 -07:00
Frank d3e90e4931 fix: use ~/.config/gws on all platforms for consistent config path (#134)
Previously used dirs::config_dir() which resolves to different paths per
OS (~/Library/Application Support/gws on macOS, %APPDATA%\gws on Windows),
contradicting the documented ~/.config/gws/ path and causing users to place
config files in the wrong location (ref #119).

Now uses ~/.config/gws/ everywhere with a fallback to the legacy OS-specific
path for existing installs. Also consolidates duplicated dirs::config_dir()
calls in auth.rs and discovery.rs to use the central config_dir() helper.
2026-03-05 01:30:19 -07:00
zerone0x dbda001367 fix: add manual project ID entry to setup project picker (#116) (#123)
When `gcloud projects list` times out (10s limit) for users with many
projects, the picker now includes a '⌨ Enter project ID manually'
option so they can type a known project ID instead of waiting or failing.

- Add '⌨ Enter project ID manually' item to project picker
- Handle the new item by prompting for input and calling set_gcloud_project
- Add EnterProjectId variant to SetupAction (tests)
- Add test_project_select_enter_manually unit test

Fixes #116

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-04 23:54:41 -07:00
Joe Eftekhari 364542b2c5 fix: reject DEL character (0x7F) in input validation (#122)
The reject_control_chars helper rejected bytes 0x00-0x1F but allowed
the DEL character (0x7F), which is also an ASCII control character.
This could allow malformed input from LLM agents to bypass validation.
2026-03-04 23:50:09 -07:00
Frank 263a8e5479 fix: use gcloud.cmd on Windows and show platform-correct config paths (#126)
* fix: use gcloud.cmd on Windows and show platform-correct config paths

On Windows, Google Cloud SDK installs `gcloud.cmd` (not `gcloud.exe`).
Rust's `Command::new("gcloud")` does not search PATHEXT, so all gcloud
invocations failed silently. Add `gcloud_bin()` helper that returns
`gcloud.cmd` on Windows and `gcloud` elsewhere.

Also replace hardcoded `~/.config/gws/` paths in error messages with
the actual platform-resolved path (`%APPDATA%\gws\` on Windows).

* chore: add changeset for Windows gcloud compat fix
2026-03-04 23:48:27 -07:00
Devadath S 75cec1b444 fix: URL template rendering for upload endpoints (#129) 2026-03-04 23:46:14 -07:00
Andrew Barnes a6994ad068 fix: filter alertcenter scopes from user OAuth login flow (#108)
* fix: filter alertcenter scopes from user OAuth login flow

The `apps.alerts` scope is restricted to service accounts with
domain-wide delegation and fails with `400 invalid_scope` when
used in the standard 3-legged OAuth consent flow. Filter it out
alongside the existing chat.app/chat.bot/keep exclusions.

Fixes #73

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: also filter apps.alerts in is_app_only_scope()

The scope filter exists in two locations: setup.rs (fetch_scopes_for_apis)
and auth_commands.rs (is_app_only_scope). Both need the apps.alerts
exclusion to prevent it from appearing in the interactive scope picker.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: retrigger CLA check

* chore: retrigger CI after CLA signing

---------

Co-authored-by: Andrew Barnes <andrew.jaguars@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 23:37:57 -07:00
Joe Eftekhari 1ad4f34da6 fix: replace unwrap() calls with proper error handling in MCP server (#109)
* fix: replace unwrap() calls with proper error handling in MCP server

Replace four serde_json::to_string().unwrap() calls in the MCP server
that could panic the process if serialization ever fails. Instead, log
the error to stderr and skip the response gracefully.

Also propagate serialization errors for params/body via the ? operator,
and log a warning when authentication silently falls back to
unauthenticated mode so users are aware of degraded state.

* fix: resolve clippy redundant_closure warnings

Replace `.map(|v| serde_json::to_string(v))` with
`.map(serde_json::to_string)` per clippy's redundant_closure lint.
2026-03-04 23:29:57 -07:00
Justin Poehnelt ed409e3022 fix: harden URL and path construction across helper modules (#102)
* fix: harden URL and path construction across helper modules

Closes #87

- gmail/watch.rs: encode msg_id with encode_path_segment(), use
  .query() for format and history params instead of format!
- modelarmor.rs: validate template with validate_resource_name() in
  handle_sanitize, validate project/location/template_id in
  parse_create_template_args, encode all path segments in
  build_create_template_url
- discovery.rs: validate service/version with validate_api_identifier()
  before use in cache filenames and discovery URLs, encode path segments
- validate.rs: add validate_api_identifier() for safe API name chars
- Add tests for all new validation and encoding paths

* refactor: pass API version as a query parameter for alternative discovery URLs.
2026-03-04 23:29:34 -07:00
andrew-kline a1be14f0c7 fix: drain stdout pipe to prevent project listing deadlock (#106)
The list_gcloud_projects() function piped stdout from `gcloud projects
list` but only read it after the child process exited. When a user has
enough GCP projects that the output exceeds the OS pipe buffer (~64KB),
gcloud blocks on write, the parent blocks waiting for exit, and neither
side makes progress — hitting the 10s timeout.

Spawn a thread to drain stdout concurrently so the pipe buffer never
fills up while the main thread polls for process completion.

Fixes googleworkspace/cli#96
2026-03-04 23:28:56 -07:00
Justin Poehnelt d1825f9385 feat: multi-account support (#85)
* feat: multi-account support with --account flag, per-account credential storage

- Add --account global flag and GOOGLE_WORKSPACE_CLI_ACCOUNT env var
- Per-account encrypted credential files (credentials.<b64-email>.enc)
- Per-account token cache (token_cache.<b64-email>.json)
- accounts.json registry with default account tracking
- New auth subcommands: list, default, per-account logout
- login_hint in OAuth URL for account pre-selection
- Email validation via Google userinfo after OAuth flow
- 12 new unit tests (380 total)

BREAKING CHANGE: Existing users must run 'gws auth login' again.
Credential storage changed from single credentials.enc to per-account files.

* refactor: Improve error handling for file system operations, rename `GWS_ACCOUNT` to `GOOGLE_WORKSPACE_CLI_ACCOUNT`, and refine service account token cache path generation.

* fix: clean up per-account token caches on logout

---------

Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
2026-03-04 23:19:14 -07:00
Justin Poehnelt 70d0cdddb5 fix: build_url now falls back to method.path when flatPath placeholders do not match parameter names, resolving Slides API issues. (#120) 2026-03-04 23:02:56 -07:00
Justin Poehnelt 1991d536b4 docs: add note about not official product more prominently (#115) 2026-03-04 22:45:22 -07:00
Miguel 🦈 4bca6935d6 fix: credential masking panic and silent token write errors (#81)
Two bugs fixed:

1. `auth_commands.rs` — `gws auth export` masking used `s[..4]` which
   panics on strings shorter than 4 characters, and `s[s.len().min(4)..]`
   which evaluates to `s[4..]` on long strings — showing the entire
   secret instead of masking it. Replaced with a `mask_secret()` helper
   that safely shows only the first 4 and last 4 characters, or "***"
   for short strings.

2. `token_storage.rs` — `save_to_disk` silently discarded the return
   value of `atomic_write_async` with `let _`, causing the function to
   return `Ok(())` even when the write failed. Token persistence failures
   now properly propagate via `?`.
2026-03-04 20:28:28 -07:00
kai f84ce37007 fix: encode URL template path params in build_url (#84)
* fix: encode URL path template params in build_url

* chore: trigger CLA recheck

* fix: validate +path params and align replacement logic
2026-03-04 20:27:50 -07:00
Justin Poehnelt 704928bd99 fix: enable APIs individually and surface gcloud errors (#77)
* fix: enable APIs individually and surface gcloud errors

Previously, `gws auth setup` used a single batch `gcloud services enable`
call for all 22 Workspace APIs. If any one API failed, the entire batch
was marked as failed. Additionally, stderr was piped to /dev/null, so
users never saw why APIs failed to enable.

Changes:
- Enable each API individually so one failure doesn't block the rest
- Capture stderr from gcloud and include error messages in output
- Show failure details with ⚠ warnings in the wizard status
- Include structured error objects in JSON output (apis_failed array)

Fixes #75

* address review: parallelize API enablement with tokio, remove clone

- Use tokio::process::Command + futures_util::stream::buffer_unordered(5)
  for parallel, non-blocking API enablement
- Remove unnecessary failed_apis.clone() by moving into ctx.failed first
- Add changeset

* test: add coverage for enable_apis and JSON output structure

- test_enable_apis_all_already_enabled: empty input returns empty results
- test_enable_apis_with_invalid_project: verifies errors are captured (not swallowed)
- test_failed_apis_json_structure: validates {api, error} JSON shape
- test_failed_apis_json_empty: empty failures produce empty array

* Update src/setup.rs

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

---------

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-03-04 18:44:59 -07:00
Justin Poehnelt 92e66a308f feat: add gws version bare subcommand (#71)
* feat: add `gws version` bare subcommand

* refactor: extract help and version flag checks into dedicated functions and add test coverage guidance to AGENTS.md

---------

Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
2026-03-04 15:29:02 -08:00
Justin Poehnelt 8fadbd6901 feat: smarter truncation of method/resource descriptions (#68)
* feat: smarter truncation of method/resource descriptions

- Add shared truncate_description() in src/text.rs with configurable
  limits and optional markdown link stripping
- Named constants: CLI_DESCRIPTION_LIMIT (200), FRONTMATTER_DESCRIPTION_LIMIT (120),
  SKILL_BODY_DESCRIPTION_LIMIT (500)
- CLI help and frontmatter strip markdown links to save space
- Skill body preserves URLs so agents can follow references
- Truncation prefers sentence boundaries, falls back to word boundaries
- Reads full descriptions from discovery doc for skill body text

Fixes #64

* fix: address PR review comments and improve coverage

- Fix sentence boundary detection at exact end of prefix (HIGH)
- Use extend() for link text emission (MEDIUM)
- Idiomatic find_char_from with position/map (MEDIUM)
- Idiomatic rfind_char_boundary with rposition (MEDIUM)
- Add 18 new tests for generate_skills.rs coverage:
  truncate_desc, lookup_method_description, capitalize_first,
  product_name_from_title
- Add sentence_boundary_at_exact_limit and zero_max_chars tests

---------

Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
2026-03-04 15:03:59 -08:00
Justin Poehnelt 670267f6ec feat: add gws mcp server (#58)
* feat: add gws mcp server

Adds a new `gws mcp` subcommand that starts a Model Context Protocol
(MCP) server over stdio, exposing Google Workspace APIs as structured
tools to any MCP-compatible client.

- New `src/mcp_server.rs`: JSON-RPC stdio transport, handles
  `initialize`, `tools/list`, and `tools/call`
- Tool discovery dynamically builds schemas from Google Discovery Docs
- Filtering via `-s <services>` flag (e.g. `-s drive,gmail` or `-s all`)
- `-w/--workflows` and `-e/--helpers` flags for optional extras
- stderr startup warning when no services are configured
- Refactored `executor::execute_method` to support output capture
  (returns `Option<Value>` instead of printing to stdout) so the MCP
  transport is not corrupted
- Updated README.md with MCP Server section and usage examples

* fix: address PR review comments

- Add stderr warning when discovery doc fails to load (mcp_server.rs)
- Remove redundant 'all' string check in service validation (mcp_server.rs)
- Validate upload path to prevent arbitrary file reads - security fix (mcp_server.rs)
- Remove redundant inner capture_output check in handle_binary_response (executor.rs)
- Add changeset for minor version bump

* fix: resolve CI lint, fmt, and test failures

- cargo fmt: format all changed files
- clippy: add #[allow(clippy::too_many_arguments)] on private handle_json_response
- clippy: collapse else { if } to else if in executor.rs
- clippy: replace svc_name.clone() with std::slice::from_ref in mcp_server.rs
- clippy: replace index-based loop with iterator in walk path resolution
- test: add ::<()> turbofish annotation to handle_error_response test calls
  to fix E0282 type inference errors
2026-03-04 11:58:28 -07:00
Justin Poehnelt 8c1042afc1 fix: use gl-rust/ prefix in x-goog-api-client header (#59)
Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
2026-03-04 11:57:52 -07:00
Justin Poehnelt 77f0f04be7 Merge pull request #52 from googleworkspace/fix/atomic-credential-writes
fix: atomic credential file writes to prevent corruption on crash (fixes #42)
2026-03-04 01:01:41 -07:00
Justin Poehnelt e41bd89574 Merge pull request #51 from googleworkspace/fix/table-flatten-and-unicode
fix(table): flatten nested objects to dot-notation, safe multi-byte truncation (fixes #40 #43)
2026-03-04 01:01:37 -07:00
Justin Poehnelt ab6c45e4b5 Merge pull request #49 from googleworkspace/fix/unknown-format-warning
fix: warn to stderr when unknown --format value is provided (fixes #38)
2026-03-04 01:01:33 -07:00
Justin Poehnelt 666f9a88f4 fix(auth): support --help / -h flag on auth subcommand (fixes #26) (#48)
* fix(auth): support --help / -h flag on auth subcommand (fixes #26)

* add changeset
2026-03-04 00:59:10 -07:00
Justin Poehnelt 173d15572f fix: add YAML document separators when paginating (fixes #39) (#50) 2026-03-04 00:56:26 -07:00
Justin Poehnelt f91f9e0114 style: cargo fmt 2026-03-04 00:54:37 -07:00
Justin Poehnelt ee35e4ab0c fix: warn to stderr when unknown --format value is provided (fixes #38) 2026-03-04 00:54:34 -07:00
Justin Poehnelt 0603bce410 fix: atomic credential file writes to prevent corruption on crash (fixes #42) 2026-03-04 00:52:59 -07:00
Justin Poehnelt bcd24011d0 fix(table): flatten nested objects to dot-notation, safe multi-byte truncation (fixes #40 #43) 2026-03-04 00:50:36 -07:00
jpoehnelt-bot 4b868c7327 docs(gws-shared): add community guidance for stars + issue hygiene (#41)
* docs(gws-shared): add community issue and starring guidance

* feat: add community links to gws help output

* fix(ci): move community section into generate-skills template + add changeset
2026-03-04 00:35:32 -07:00
jpoehnelt-bot e094b02dbe fix: YAML block scalar and repeated --page-all headers in CSV/table (#37)
- YAML: only emit block scalar (|) for strings with genuine newlines;
  single-line strings containing '#' or ':' are now double-quoted instead,
  e.g. 'drive#file' renders as '"drive#file"' not a block scalar.
- --page-all: CSV/table formats no longer re-emit column headers on
  every page; headers appear only on the first page.  A new public
  format_value_paginated() helper replaces the now-removed
  format_value_compact().
- Add unit tests for both fixes (8 new test cases in formatter.rs).
2026-03-04 00:35:27 -07:00
jpoehnelt-bot ee2e216c10 fix: narrow default OAuth scopes to avoid restricted_client, improve non-interactive setup UX (#30)
* fix: narrow default OAuth scopes to avoid restricted_client, add --full flag, improve non-interactive setup UX

Fixes #24, #25

- DEFAULT_SCOPES now aliases MINIMAL_SCOPES (no pubsub/cloud-platform)
  which avoids Google's restricted_client 403 on unverified OAuth apps
- Add FULL_SCOPES and --full flag for users who need the broader set
- Replace cryptic 'run setup interactively' error with step-by-step
  manual OAuth console instructions including URLs, options A/B/C

* chore: add changeset

* chore: cargo fmt

* fix: refactor format! with backslash continuations to concat! macro

Address Gemini review (PR #30): replace hard-to-read backslash line
continuations in large format! macros with concat! for clearer structure:
- manual_oauth_instructions(): full step-by-step guide
- stage_configure_oauth() wizard show_message: interactive prompt text

No functional change; output text is identical.
2026-03-04 00:35:22 -07:00
jpoehnelt-bot de2787e90f feat(error): detect accessNotConfigured and guide users to enable APIs (#33)
* feat(error): detect accessNotConfigured and guide users to enable APIs

When the Google API returns a 403 with reason accessNotConfigured,
gws now:
- Extracts the GCP Console enable URL from the error message.
- Adds an optional enable_url field to the JSON error output.
- Prints an actionable hint with the enable URL to stderr.

Also adds extract_enable_url() helper with tests, and a Troubleshooting
section to README.

Fixes #31

* fix(error): trim trailing punctuation from accessNotConfigured enable URL
2026-03-04 00:30:51 -07:00
jpoehnelt-bot 6ae74271f9 fix(auth): stabilize encrypted credential key fallback (#28)
* fix(auth): stabilize encryption key fallback across runs

* chore: add changeset for auth encryption key fix

* chore: cargo fmt

* fix(auth): address Gemini review comments - OnceLock expect + permission warnings

- Replace unwrap_or(candidate) with expect() in cache_key closure for clearer
  OnceLock race invariant: if set() fails, get() is guaranteed to return Some
- Emit eprintln! warnings (rather than silently ignoring) when set_permissions
  fails on the encryption key directory, matching the warning pattern used
  throughout the codebase (src/auth_commands.rs, helpers/workflows.rs, etc.)
2026-03-04 00:19:42 -07:00
Justin Poehnelt 90adcb4379 fix: harden URL encoding and input validation for AI/LLM callers (#21)
* refactor: replace manual urlencoded() with reqwest .query() builder

Remove duplicate hand-rolled urlencoded() functions from workflows.rs
and calendar.rs. All query parameters are now passed via reqwest's
.query() API, which handles percent-encoding correctly and completely.

* fix: percent-encode path parameters to prevent path traversal

Use percent_encoding::utf8_percent_encode for calendar_id, cal.id,
message_id, and file_id before interpolating into URL path segments.
Addresses code review feedback on security regression.

* fix: add shared URL safety helpers for path params

Add encode_path_segment() for single-segment IDs and
validate_resource_name() for multi-segment resource names.

encode_path_segment: percent-encodes all non-alphanumeric chars,
used for calendar IDs, file IDs, and message IDs.

validate_resource_name: rejects path traversal (..) and control
chars while preserving intentional / structure, used for Chat
space names, task list IDs, and subscription names. Returns clear
error messages for LLM callers.

* test: add AI edge case tests for URL safety helpers

Cover query/fragment injection, double-encoding, unicode, spaces,
path traversal via encoding, control chars (CR/tab), and clear
error message assertions for LLM callers.

* fix: warn on stderr when API calls fail silently

- Daily briefing calendar events fetch
- Daily briefing tasks fetch
- Daily summary calendar events fetch
- Daily summary unread email count fetch

Addresses PR review feedback about confusing silent failures,
especially for LLM callers that cannot see visual cues.

* fix: harden input validation for AI/LLM callers

- Add src/validate.rs with validate_safe_output_dir, validate_msg_format,
  and validate_safe_dir_path helpers
- Validate --output-dir against path traversal in gmail +watch and
  events +subscribe
- Validate --msg-format against allowlist in gmail +watch
- Validate --dir against path traversal in script +push
- Add clap value_parser constraint for --msg-format
- Document input validation patterns in AGENTS.md

Closes #23

* chore: add changesets for PR #21 commits

* test: add comprehensive test coverage for input validation handlers

* docs: document input validation and URL safety patterns in AGENTS.md and CONTRIBUTING.md

* fix: address PR review comments — reject ?/# in resource names, validate subscription arg, remove redundant validate_msg_format

* fix: store validated PathBuf, remove dead code, delete duplicate SubscribeConfig

Addresses review comments:
- Store validated PathBuf from validate_safe_output_dir instead of
  discarding it (output_dir is now Option<PathBuf>)
- Remove duplicate SubscribeConfig from events/mod.rs
- Delete unused validate_msg_format (clap value_parser handles this)
- Remove all #[allow(dead_code)] annotations

* fix: per-segment traversal check in validate_resource_name, fix docs

* fix: harden security validation and deduplicate logic

---------

Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
2026-03-03 18:36:41 -07:00
Justin Poehnelt 76643573b3 test: add test for missing error paths in load_client_config (#19) 2026-03-03 17:20:56 -07:00
Justin Poehnelt c11d3c452d test: Add test for EncryptedTokenStorage::new (#17) 2026-03-03 15:03:10 -08:00
Justin Poehnelt b0d0b95d07 feat: skills expansion (#18)
* wip

* feat: replace admin recipes with 50 consumer-focused recipes

- Remove all admin/security/IT recipes (offboard-user, audit-user-login, etc.)
- Remove enterprise-only recipes (initiate-litigation-hold)
- Replace dangerous recipes (setup-email-forwarding -> create-gmail-filter)
- Remove recipes overlapping with gws-workflow-* helpers
- Remove thin 2-step recipes better served as helpers
- Add 50 curated consumer recipes for Gmail, Drive, Docs, Calendar, Sheets
- Update README: link to docs/skills.md, update skill count to 100+
- Fix clippy needless_borrow warnings in generate_skills.rs
- Fix lefthook.yml: run fmt/clippy sequentially (parallel causes races)

---------

Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
2026-03-03 15:02:34 -08:00
Justin Poehnelt adb2cfa8dc fix: decrypt token cache before extracting refresh token (#11) 2026-03-03 11:49:55 -08:00
Justin Poehnelt f75bf6dcf7 feat: implement cli (#1) 2026-03-02 17:26:21 -07:00