79 Commits

Author SHA1 Message Date
drassi adbca874ab fix: handle array-of-arrays in CSV formatter (#288)
* fix: handle array-of-arrays in CSV formatter

The CSV formatter assumed all items were JSON objects when collecting
column names. APIs that return arrays of arrays (e.g. Sheets values)
produced empty newlines instead of data.

Add an early return path for non-object arrays that emits each inner
array's elements as CSV cells directly, mirroring the table formatter's
existing "array of non-objects" handling.

Fixes #283

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

* test: add coverage for flat scalar CSV formatting

---------

Co-authored-by: Dan Rassi <129646+drassi@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 15:26:06 -06:00
Malo Bourgon 4d4b09f9cc feat(gmail): add recipient management flags and extract shared message builder (#362)
* feat(gmail): add --cc, --bcc, --to recipient management flags

Add --cc and --bcc to +send, --to and --bcc to +reply and +reply-all,
and --bcc to +forward. This brings all four Gmail helpers to feature
parity for basic recipient management.

Key behaviors:
- --to on reply/reply-all is additive (appends to auto-computed To)
- --remove only affects auto-computed recipients, not explicit flags
- Dedup with priority To > CC > BCC via new dedup_recipients()
- Validation deferred until after all additions and dedup
- Empty/whitespace --cc/--bcc/--to filtered to None at parse time
- BCC header included in raw message (Gmail API strips before delivery)

Also includes:
- CAUTION boxes in all four SKILL.md files
- SendConfig visibility narrowed to pub(super)
- Consistent "email address(es)" wording across clap help and SKILL.md

* refactor(gmail): extract shared MessageBuilder and fix pre-existing issues

Extract duplicated header-construction logic from send.rs, reply.rs, and
forward.rs into a shared MessageBuilder in mod.rs. This centralizes CRLF
header-injection sanitization and RFC 2047 subject encoding that were
previously inconsistent across the three paths.

Additional changes:
- Fix silent auth failure in send.rs (Err(_) swallowed all auth errors)
- Fix try_get_one error swallowing in parse_reply_args (explicit match
  on MatchesError::UnknownArgument, propagate unexpected errors)
- Extract shared helpers: build_references, parse_optional_trimmed,
  encode_header_value, sanitize_header_value
- Introduce ForwardEnvelope (analogous to ReplyEnvelope)
- Introduce ThreadingHeaders to group in_reply_to/references
2026-03-10 14:13:32 -06:00
Justin Poehnelt 8d89325a8b feat(credential_store): add GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND env var (#359)
* feat(credential_store): add GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND env var

Add gogcli-style backend selection for encryption key storage:
- keyring (default): OS keyring with file fallback
- file: .encryption_key file only (Docker/CI/headless)

Never delete .encryption_key — it always serves as a durable fallback
for environments where the keyring is ephemeral. When generating new
keys with backend=keyring, save to both keyring and file.

Extracts KeyringProvider trait + resolve_key() for testability.
25 tests covering both backends and all edge cases.

Fixes #344

* chore: regenerate skills [skip ci]

* fix(credential_store): use O_EXCL for race-safe key generation

Use create_new(true) (O_EXCL on Unix, CREATE_NEW on Windows) when
generating a new encryption key file. If another process wins the
race, read their key instead. Platform-independent.

* fix(credential_store): sync winner's key into keyring after file race

When two processes race to create the encryption key file, the loser
now syncs the winner's key back into the keyring. Without this, the
keyring and file could permanently diverge.

* test(credential_store): add 9 tests covering file exclusion, env parsing, and race paths

- save_key_file_exclusive: creates new file, rejects existing
- save_key_file: overwrites existing
- ensure_key_dir: creates nested dirs
- KeyringBackend: file/FILE/invalid parsing
- Race loser: syncs winner key to keyring
- Race loser: corrupt file gets overwritten

* feat(credential_store): security and robustness hardening

1. Warn on unrecognized KEYRING_BACKEND values instead of silent default
2. fsync after key file writes for crash durability
3. Zeroize decoded key material from heap after copy
4. Warn if key file has overly permissive Unix permissions (mode & 077)
5. Log which keyring backend was selected to stderr
6. Expose keyring_backend in 'gws auth status' JSON output

---------

Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-10 11:17:35 -06:00
Malo Bourgon 5e7d1200df fix(gmail): bring +forward behavior in line with Gmail web UI (#353)
- Pass original threadId to keep forward in sender's thread
- Add In-Reply-To and References headers for RFC 5322 compliance
- Add blank line between forwarded message metadata and body
- Remove spurious closing delimiter from forwarded message block
- Update SKILL.md to remove outdated threading tip
- Add test for References chain construction

Closes #88
2026-03-09 17:47:35 -06:00
Justin Poehnelt 06aa698e23 fix(auth): format and deduplicate dynamic scope fallback (#352)
* fix(auth): fall back to Discovery docs when `-s` specifies services not in static scope lists

When `gws auth login -s chat` (or any service not in the 7 static
scope lists) is used, the static filter returns no matching scopes.

Add a dynamic fallback that detects unmatched services and fetches
their OAuth scopes from the Google Discovery API. This leverages the
existing `fetch_discovery_document` with 24h caching.

Fixes #236

* refactor: optimize find_unmatched_services and parallelize Discovery fetches

Address review feedback:
- Avoid per-service HashSet allocation in find_unmatched_services by
  collecting matched services first then computing the difference.
- Use futures_util::future::join_all to fetch Discovery docs in
  parallel instead of sequentially.

* refactor: extract map_service_to_scope_prefix to deduplicate alias mapping

Share the service-name-to-scope-prefix mapping between
scope_matches_service and find_unmatched_services via a single helper.

* fix(auth): format extract_scopes_from_doc and deduplicate dynamic scopes

- Break long method chain in extract_scopes_from_doc to pass cargo fmt
- Deduplicate dynamic scopes in augment_with_dynamic_scopes to prevent
  duplicate entries when dynamic results overlap with static ones

---------

Co-authored-by: Frank <qwer4488999@gmail.com>
Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
2026-03-09 17:36:49 -06:00
zerone0x 2782cf101e fix: use gmail.readonly scope in +triage to avoid metadata scope 403 (#304)
The +triage helper uses the `q` query parameter when listing messages,
but Gmail's metadata scope does not support `q` and returns 403. When a
user's OAuth token includes both gmail.metadata and gmail.modify scopes,
the API may resolve to the metadata code path and reject the query.

Switch +triage from gmail.modify to gmail.readonly, which is the
minimum scope that supports query filtering and aligns with the
read-only nature of the triage command.

Fixes #265
2026-03-09 17:08:34 -06:00
Justin Poehnelt 5872dbe474 fix(credential_store): stop persisting encryption key file when keyring is available (#345)
Previously, get_or_create_key() unconditionally wrote the encryption key
to ~/.config/gws/.encryption_key on first run, even when the OS keyring
was available. This left the key material on disk as a plain file,
making credentials portable by copying the config directory.

Changes:
- Extract save_key_file() helper to deduplicate file-writing logic
- On keyring read success: delete stale .encryption_key (migration)
- On NoEntry + existing file: migrate key into keyring, then delete file
- On NoEntry + new key: try keyring first, only write file as fallback

Fixes #344

Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
2026-03-09 15:51:05 -06:00
jonameijers 08716f8bcd fix: RFC 2047 encode non-ASCII email subjects in +send helper (#322)
* fix: RFC 2047 encode non-ASCII email subjects in +send helper

* chore: add changeset for RFC 2047 fix

* fix: use standard Base64 and fold long encoded-words per RFC 2047

* fix: chunk at char boundaries to avoid splitting multi-byte UTF-8

* chore: fix changeset package

Fix garbled non-ASCII email subjects in gmail +send by RFC 2047 encoding the Subject header and adding MIME-Version/Content-Type headers.

---------

Co-authored-by: Jona Meijers <jonameijers@gmail.com>
Co-authored-by: Justin Poehnelt <justin.poehnelt@gmail.com>
2026-03-09 11:48:06 -06:00
Si Zengyu 7d15365518 feat(gmail): add +reply, +reply-all, and +forward helpers (#105)
* feat(gmail): add +reply, +reply-all, and +forward helper commands

Add first-class reply and forward support to the Gmail helpers,
addressing the gap described in #88. These commands handle the
complex RFC 2822 threading mechanics (In-Reply-To, References,
threadId) that agents and CLI users struggle with today.

New commands:
- +reply: reply to a message with automatic threading
- +reply-all: reply to all recipients with --remove/--cc support
- +forward: forward a message with quoted original content

* fix(gmail): encode message_id in URL path and fix auth signature

- Use crate::validate::encode_path_segment() on message_id in
  fetch_message_metadata URL construction per AGENTS.md rules
- Update auth::get_token calls to pass None for the new account
  parameter added on main

* refactor(gmail): extract send_raw_email and deduplicate handlers

- Add send_raw_email() to mod.rs: shared encode→json→auth→execute
  pattern for sending raw RFC 2822 messages via users.messages.send
- Simplify handle_reply: delegate send logic to send_raw_email
- Simplify handle_forward: delegate send logic to send_raw_email

Addresses code duplication feedback from PR review.

* fix(gmail): register --dry-run flag on reply/forward commands

The handlers read matches.get_flag("dry-run") but the flag was missing
from the clap command definitions, so it always returned false. Now
dry-run works for +reply, +reply-all, and +forward.

* chore: add changeset for gmail reply/forward feature

* style: apply cargo fmt formatting

* fix(gmail): register --dry-run flag on +send command

Same class of bug fixed for +reply/+reply-all/+forward — the handler
reads matches.get_flag("dry-run") but the arg was not registered.

* fix(gmail): honor Reply-To header and use exact address matching

- Prefer Reply-To over From when selecting reply recipients, fixing
  incorrect routing for mailing lists and support systems
- Use exact email address comparison instead of substring matching
  for --remove filtering and sender deduplication, preventing
  unintended recipient removal (e.g. ann@ no longer drops joann@)

* test(gmail): add comprehensive coverage for reply address handling

- extract_email: malformed input (no closing bracket), empty string,
  whitespace-only
- build_reply_all_recipients: display-name sender exclusion,
  --remove with display name, extra --cc, CC becomes None when all
  filtered, case-insensitive sender exclusion

* Improves reply-all recipient deduplication

Corrects how `build_reply_all_recipients` handles multi-address `Reply-To` headers.
Previously, only the first address from `Reply-To` was used for deduplication, leading to potential redundancy by including those addresses in the `Cc` field.
The updated logic now parses all addresses in `Reply-To`, ensuring they are fully moved to the `To` field and properly excluded from `Cc`.

* style(gmail): add missing Apache 2.0 copyright headers

reply.rs and forward.rs were missing the copyright header that all
other source files in the repo include.

* fix(gmail): use try_get_one for optional --remove arg in +reply

parse_reply_args used get_one("remove") which panics when called
from +reply (which does not register --remove). Switch to
try_get_one to safely return None for unregistered args.

* feat(gmail): support --dry-run without auth for reply/forward commands

Skip auth and message fetch when --dry-run is set by using placeholder
OriginalMessage data. This lets users preview the request structure
without needing credentials.

* fix(gmail): use RFC-aware mailbox list parsing for recipient splitting

Replace naive comma-split with split_mailbox_list that respects
quoted strings, so display names containing commas like
"Doe, John" <john@example.com> are handled correctly in reply-all
recipient parsing, deduplication, and --remove filtering.

* fix(gmail): handle escaped quotes in mailbox list splitting

split_mailbox_list toggled quote state on every `"` without accounting
for backslash-escaped quotes (`\"`), causing display names like
`"Doe \"JD, Sr\""` to split incorrectly at interior commas.

Track `prev_backslash` so `\"` inside quoted strings is treated as a
literal quote character rather than a delimiter toggle. Double
backslashes (`\\`) are handled correctly as well.

* fix(gmail): address PR review feedback for reply/forward helpers

- Use reqwest .query() for metadata params per AGENTS.md convention
- Add MIME-Version and Content-Type headers to raw messages
- Add --from flag to +reply, +reply-all, +forward for send-as/alias
- Narrow ReplyConfig/ForwardConfig visibility to pub(super)
- Refactor create_reply_raw_message args into ReplyEnvelope struct

* fix(gmail): address review feedback for reply/forward helpers

- Exclude authenticated user's own email from reply-all CC by
  fetching user profile via Gmail API
- Use format=full to extract full plain-text body instead of
  truncated snippet for quoting and forwarding
- Deduplicate CC addresses using a HashSet
- Reuse auth token from message fetch in send_raw_email to
  eliminate double auth round-trip
- Propagate auth errors in send_raw_email instead of silently
  falling back to unauthenticated requests
- Use consistent CRLF line endings in quoted and forwarded
  message bodies per RFC 2822

* fix(gmail): Gmail reply and forward helpers

* fix(gmail): refactor shared reply-forward helpers

* Preserve repeated Gmail address headers

* chore: regenerate skills [skip ci]

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-09 11:47:02 -06:00
Max Headley f083eb9af0 fix: Improve auth setup project-creation error handling and retry UX (#95)
* Improve setup project creation error recovery

* Add post-setup login continuation flow

---------

Co-authored-by: mkh09353 <6936686+mkh09353@users.noreply.github.com>
2026-03-09 11:30:31 -06:00
github-actions[bot] 6f67da39b6 style: cargo fmt 2026-03-09 00:21:14 +00:00
Shane Huntley 4d41e52198 fix(auth): prioritize local project configuration over global ADC for quota attribution (#295)
* fix(auth): prioritize local project configuration over global ADC for quota attribution

* chore: add changeset for project ID priority fix
2026-03-08 18:20:56 -06:00
Steve Bazyl dd3fc9074d fix!: Remove MCP server mode (#275)
* BREAKING CHANGE: Remove MCP server mode

* Add changeset file
2026-03-06 11:33:23 -07:00
Justin Poehnelt d34576c5c7 chore: Remove a subset of skills and recipes along with their service entries and registry references (#254) 2026-03-05 19:51:59 -08:00
Justin Poehnelt d6372105eb feat!: remove multi-account, DWD, and impersonation support (#253)
* feat!: remove multi-account, DWD, and impersonation support

BREAKING CHANGE: Remove domain-wide delegation, multi-account support,
and impersonation from the CLI authentication flow.

Removed:
- `gws auth list` and `gws auth default` commands
- `--account` flag from `gws auth login` and `gws auth logout`
- `GOOGLE_WORKSPACE_CLI_ACCOUNT` env var
- `GOOGLE_WORKSPACE_CLI_IMPERSONATED_USER` env var
- Per-account credential storage (accounts.json registry)
- Service account impersonation (subject/DWD)

Preserved:
- `GOOGLE_WORKSPACE_CLI_TOKEN` (raw access token)
- `GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE` (SA key path)
- `GOOGLE_WORKSPACE_CLI_CLIENT_ID` / `CLIENT_SECRET` (OAuth config)
- `GOOGLE_WORKSPACE_CLI_CONFIG_DIR` (config dir override)

* chore: update changeset description

* docs: remove multi-account and DWD references from docs

---------

Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
2026-03-05 22:43:43 -05:00
Steve Bazyl e1505afe12 chore: Remove dwd support (#250)
* chore: Remove dwd support

* Add changeset file

* chore: regenerate skills [skip ci]

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-03-05 19:26:27 -08:00
Adeel Khan 54b3b31728 fix: quota header discovery (#242) 2026-03-05 18:03:20 -08:00
Frank c86b964de4 fix: respect account selection in MCP server and CLI --account flag (#223)
* fix: respect account selection in MCP server and CLI --account flag (#221, #181)

MCP server now reads GOOGLE_WORKSPACE_CLI_ACCOUNT env var and passes it
to get_token instead of always using None (default account).

CLI filter_args_for_subcommand now dynamically locates the service name
instead of hardcoding skip(2), fixing --account before service name
causing unrecognized subcommand errors.

* fix: skip --api-version in first_arg detection

The first_arg loop only skipped --account but not --api-version,
so `gws --api-version v3 drive ...` would misidentify --api-version
as the service name. Now both global flags are consistently skipped.
2026-03-05 16:31:57 -08:00
Justin Poehnelt 322529d8a9 fix: document all environment variables and enable CONFIG_DIR override (#222)
* docs: document all environment variables and enable CONFIG_DIR override (#171)

* docs: clarify env vars are trusted inputs in AGENTS.md

* chore: add Gemini Code Assist style guide

---------

Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
2026-03-05 16:09:01 -08:00
Shreyas Karnik 6daf90d331 fix(mcp): conditionally include body/upload in tool schemas, drop empty body on execution (#213)
* fix(mcp): conditionally include body/upload in full-mode tool schemas and drop empty body on execution

Full-mode tool schemas now only include `body` when the Discovery Document
method defines a request body, and `upload` when `supportsMediaUpload` is
true. This prevents LLMs from hallucinating these fields on GET-only methods.

Additionally, empty body objects (`{}`) are filtered out before execution
in both compact and full modes, and empty upload strings are ignored. LLMs
commonly send "body": {} on read-only methods, which causes Google APIs to
return HTTP 400.

* style: cargo fmt and add changeset for MCP tool schema fix

* fix(mcp): conditionally include page_all only for paginated methods

Only include the page_all property in full-mode tool schemas when the
method has a pageToken parameter, preventing LLMs from attempting
pagination on non-paginable methods.

* docs: update changeset to include page_all conditional change
2026-03-05 15:37:17 -08:00
github-actions[bot] 8a897e955b style: cargo fmt 2026-03-05 23:36:21 +00:00
Andrew Barnes c80eb5274d fix: replace strip_suffix(".readonly").unwrap() with unwrap_or (#192)
Two call sites in auth_commands.rs and setup_tui.rs used
.strip_suffix(".readonly").unwrap(), which panics if a scope URL
flagged as is_readonly doesn't end with ".readonly".

Replace with .unwrap_or() to gracefully fall back to the original
URL rather than crashing on inconsistent discovery data.
2026-03-05 15:35:56 -08:00
Chris Wood ff53538b9f fix: prevent gmail.metadata scope from blocking query parameters (#200)
* fix: select broadest scope instead of all method scopes

Discovery Documents list method scopes as alternatives (any one grants
access), but passing all of them to yup_oauth2 caused Google to include
restrictive scopes like gmail.metadata in the token. The API then
enforced that scope's restrictions, blocking query parameters like `q`.

Select only the first (broadest) scope from the method's scope list.

* fix: filter gmail.metadata from login scopes and remove token cache superset fallback

gmail.metadata restricts API behavior (blocks `q` parameter) even when
broader scopes are present in the token. Filter it out during login when
broader Gmail scopes like gmail.modify or mail.google.com are selected.

Also remove the superset fallback in token storage to prevent stale
all-scopes tokens from being reused when a narrower scope is requested.

* fix: apply select_scope to MCP server code path

The MCP server had the same bug as the CLI — passing all method scopes
to get_token. Use select_scope to pick only the broadest scope.

* style: fix cargo fmt formatting issues

---------

Co-authored-by: Justin Poehnelt <jpoehnelt@google.com>
2026-03-05 15:31:01 -08:00
Adeel Khan 2173a929d3 fix(client): For ADC, send x-goog-user-project header (#215)
* fix(client): send x-goog-user-project header from ADC quota project

When using Application Default Credentials with a quota_project_id set,
API requests failed with 403 because the quota project header was never
sent. Read quota_project_id from ADC and set it as a default header.

* Update src/auth.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-05 15:29:22 -08:00
Joe Eftekhari 132c3b19a7 fix: warn on credential file permission failures instead of ignoring (#124)
Replaced silent `let _ =` on set_permissions calls in save_encrypted
with eprintln! warnings so users are aware if their credential files
end up with insecure permissions. Also log keyring access failures
instead of silently falling through to file storage.
2026-03-05 14:50:55 -08:00
Andrew Barnes 9a780d73d4 fix: log token cache errors instead of silently swallowing them (#193)
load_from_disk used four nested if-let-Ok blocks that silently
returned an empty HashMap on any failure. When the encryption key
rotated or the cache file was corrupted, tokens silently stopped
loading and users were forced to re-authenticate with no explanation.

Replace with explicit match arms that log specific warnings to
stderr for each failure mode:
- Decryption failure (key changed, corrupted data)
- Invalid UTF-8 in decrypted data
- JSON deserialization failure

File-not-found is still silent since that's normal on first run.
2026-03-05 14:37:29 -08:00
Justin Poehnelt 28fa25a513 Clean up nits from PR #175 auth fix (#217)
- Update stale docstring on resolve_account to reflect fallthrough behavior
- Add breadcrumb comment on string-based error matching in main.rs
- Move identity scope injection before authenticator build for readability

Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
2026-03-05 14:31:15 -08:00
Harshal Patil a926e3f4c0 fix: auth failures when accounts.json registry is missing (#175)
Three related bugs caused all API calls to fail with "Access denied.
No credentials provided" even after successful `gws auth login`:

1. resolve_account() rejected valid credentials.enc as "legacy" when
   accounts.json was absent, instead of falling through to use them.

2. main.rs silently swallowed all auth errors (Err(_) => None),
   masking real failures behind a generic "no credentials" message.

3. auth login didn't include openid/email scopes, so
   fetch_userinfo_email() couldn't identify the user, causing
   credentials to be saved without an accounts.json entry.

{attribution.commit: ""}
2026-03-05 14:24:37 -08:00
Justin Poehnelt 3d59b2e5a9 fix: isolate flaky auth tests from host ADC credentials (#208)
* fix: isolate flaky auth tests from host ADC credentials

Fixes #206

Both test_load_credentials_no_options and
test_get_token_env_var_empty_falls_through now override HOME to a
temp dir and clear GOOGLE_APPLICATION_CREDENTIALS, preventing the
well-known ADC path from matching on CI runners that have gcloud
credentials.

* refactor: use RAII EnvVarGuard for panic-safe env var cleanup in tests

- Introduce EnvVarGuard struct that saves/restores env vars on Drop
- Replace all manual save/restore patterns in auth tests
- Fix bug where test_get_token_env_var_empty_falls_through did not
  restore GOOGLE_WORKSPACE_CLI_TOKEN
- Ensures cleanup runs even if a test panics

* fix: use var_os/OsString in EnvVarGuard for non-UTF-8 safety

---------

Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
2026-03-05 13:48:59 -08:00
github-actions[bot] 24a25a6ee6 style: cargo fmt 2026-03-05 21:10:22 +00:00
Frank cb1f988344 fix(executor): add Content-Length: 0 for body-less POST requests (#183)
* fix(executor): add Content-Length: 0 header for body-less POST/PUT/PATCH requests

Google API servers return HTTP 411 (Length Required) when a POST request
is sent without a Content-Length header, even if there is no body.
This affects all Discovery API methods where httpMethod is POST but no
requestBody is defined (e.g. gmail users.messages.trash).

Fixes #182

* chore: add changeset for content-length fix
2026-03-05 13:10:02 -08:00
Justin Poehnelt d4080512b1 style: cargo fmt 2026-03-05 13:54:31 -07:00
zerone0x b38b760d79 feat: add Application Default Credentials (ADC) support (#125)
* feat: add Application Default Credentials (ADC) support (#103)

Extends the credential chain in get_token() to include ADC as a 4th source:
  1. GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE env var
  2. Encrypted credentials (~/.config/gws/credentials.enc)
  3. Plaintext credentials (~/.config/gws/credentials.json)
  4. ADC — GOOGLE_APPLICATION_CREDENTIALS env var, then
     ~/.config/gcloud/application_default_credentials.json

Both authorized_user and service_account ADC formats are detected via the
'type' field and parsed accordingly.  This means users can authenticate with:
  gcloud auth application-default login --client-id-file=client_secret.json

and gws will automatically pick up those credentials.

Closes #103

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

* fix(auth): address review feedback on ADC support

- Extract duplicated JSON credential parsing into parse_credential_file()
  helper to reduce duplication between GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE
  and ADC code paths; uses serde_json::from_value to avoid second string parse
- Fix well-known ADC path on macOS: dirs::config_dir() returns
  ~/Library/Application Support on macOS, not ~/.config; use
  dirs::home_dir().join('.config/gcloud/...') instead
- Hard-error when GOOGLE_APPLICATION_CREDENTIALS points to a missing file
  (was: silently fall through to 'No credentials found')
- Add test_load_credentials_adc_env_var_service_account covering service
  account credentials loaded via GOOGLE_APPLICATION_CREDENTIALS
- Remove unnecessary unsafe blocks from env var tests (set_var/remove_var
  are not unsafe functions; thread safety is already handled by serial_test)
- Update changeset to include GOOGLE_WORKSPACE_CLI_TOKEN at top of lookup
  order and clarify ADC fallback behaviour

Addresses review feedback from jpoehnelt on #125.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-05 13:23:13 -07:00
Hafez 5205467ecb fix(setup): drain stale keypresses between TUI screen transitions (#186)
* fix(setup): drain stale keypresses between TUI screen transitions

The picker's vim-style j/k navigation can leave buffered keypresses in
crossterm's event queue that leak into the next input field, corrupting
pre-populated values like client_id.

* refactor: extract drain logic into helper, add changeset
2026-03-05 09:39:05 -08:00
Justin Poehnelt 473dd30500 chore: cargo fmt 2026-03-05 09:17:50 -07:00
Frank 9cf6e0e66d feat(mcp): add compact tool mode to reduce context window usage (#172) 2026-03-05 08:17:19 -08:00
Justin Poehnelt 0a16d0bca3 fix: allow services flag for login and improve default list for consumer accounts (#177)
* feat: add -s/--services flag to auth login for scope picker filtering

* fix: restrict recommended scope template to minimal consumer scopes

* refactor: address PR review comments — dedup parsing, extract helpers, use is_none_or
2026-03-05 08:14:54 -08:00
Justin Poehnelt e1e08ebfb6 fix: use reverse video for TUI highlight style (closes #139) (#146)
Replace bg(Color::DarkGray) with Modifier::REVERSED in highlight_style
so the selected-row highlight adapts to both light and dark terminal themes.

Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
2026-03-05 01:08:03 -08:00
zerone0x fc6bc9593b fix: exclude Workspace-admin-only scopes from Recommended preset (#119) (#127)
Admin-only scopes (apps.*, cloud-identity.*, ediscovery, directory.readonly,
groups) require Workspace domain-admin access and cannot be granted to personal
@gmail.com accounts — Google returns 400 invalid_scope when they're included.

Changes:
- Add is_workspace_admin_scope() helper (mirrors is_app_only_scope())
  to identify scopes that fail for personal Google accounts
- Exclude these scopes from the template_selects of the 'Recommended' preset
  in run_discovery_scope_picker()
- Exclude them from the resolved scope list when the Recommended template
  is confirmed
- Add 8 unit tests covering the new helper

Workspace admins can still access these scopes via 'Full Access' template
or by selecting them individually in the picker.

Note: this is complementary to PR #108 which filters alertcenter scopes
at the API-discovery level. This PR handles the broader set at the
recommendation layer.

Addresses #119 (Bug 1: admin scopes in Recommended preset)

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-05 00:44:46 -08:00
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