* fix: auto-install binary on run if missing
pnpm skips postinstall when the package is already cached/installed.
This commit ensures run.js will auto-trigger install.js if the
binary is missing, fixing the 'gws binary not found' error.
* chore: add changeset for pnpm binary fix
---------
Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
* chore: remove cargo-dist, use native fetch for npm installer
* fix: harden npm scripts — spawnSync, signal handling, binary-exists check
* fix: address PR review comments
- Add null body guard for fetch response
- Fix PowerShell Expand-Archive path quoting vulnerability
- Sanitize error output to prevent ANSI escape injection
- Add proxy support limitation note
- Fix upgrade bug: use .version marker so npm update downloads new binary
- Downgrade changeset from minor to patch (chore, not feature)
- Update AGENTS.md: remove stale cargo-dist reference from labels
* fix: use flat archives for consistent tar/zip extraction
Both tar.gz and zip archives now contain files at root (no nested
directory). Removes --strip-components 1 from install.js since it is
no longer needed. This makes extraction consistent across platforms.
---------
Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
* fix(auth): implement platform-aware keyring storage
* chore: regenerate skills [skip ci]
* fix(auth): address PR comments on error handling and TOCTOU vulnerabilities
* fix(test): isolate config directory to prevent race conditions during cargo test
* refactor(auth): deduplicate key generation logic in credential store
* fix(test): execute credential tests serially to prevent environment pollution
* fix(auth): sanitize os keyring error strings and handle missing file fallbacks
This commit removes the inadvertently included skills, uses output::sanitize_for_terminal to protect the user from escape sequence injection via keyring errors, and ensures removal of the fallback file properly alerts the user when an io error occurs.
* chore: regenerate skills [skip ci]
---------
Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
Co-authored-by: googleworkspace-bot <googleworkspace-bot@users.noreply.github.com>
The Lint Skills job uses uvx to run agentskills validate, but uv
is not pre-installed on GitHub Actions runners. Add astral-sh/setup-uv
action and a skills path filter to the changes detection.
* feat: add HTTP proxy support via environment variables
When http_proxy/https_proxy/all_proxy environment variables are set,
use reqwest (which natively supports proxy) for token refresh instead
of yup-oauth2's hyper-based client (which doesn't support proxy).
This enables gws to work in environments that require HTTP proxy to
access Google APIs (e.g., users in China).
Changes:
- Cargo.toml: Enable reqwest's default features including proxy support
- src/auth.rs: Add proxy-aware token refresh using reqwest as fallback
Fixes#422
* feat: add proxy support for auth login flow
When proxy env vars are set, use a custom OAuth flow with reqwest
for token exchange instead of yup-oauth2's hyper-based client.
Changes to auth_commands.rs:
- Add login_with_proxy_support() for proxy-aware OAuth login
- Add exchange_code_with_reqwest() for token exchange via reqwest
- Detect proxy env vars and choose appropriate flow
* fix: address proxy auth review feedback
* fix: reuse shared reqwest client for auth flows
When --draft is set, calls users.drafts.create instead of
users.messages.send. Message construction is identical; only the
API method and metadata wrapper change. Threaded drafts (replies
and forwards) preserve threadId in the draft metadata.
The original bug from #513 (display names with commas/parens causing
'Invalid To header') was already fixed by the mail-builder migration
in #482. This adds explicit regression tests to prevent regressions.
Closes#513
Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
Include original message attachments on +forward by default, matching
Gmail web behavior. Add --no-original-attachments flag to opt out
(skips file attachments but preserves inline images in HTML mode).
Preserve cid: inline images in HTML mode for both +forward and
+reply/+reply-all by building the correct multipart/related MIME
structure via mail-builder's MimePart API. Gmail's API rewrites
Content-Disposition: inline to attachment in multipart/mixed, so
explicit multipart/related is required.
In plain-text mode, inline images are not included for both forward
and reply, matching Gmail web behavior.
Key implementation details:
- Single-pass MIME payload walker replaces separate text/html extractors
- OriginalPart metadata type with lazy attachment data fetching
- Part classification uses Content-Disposition to distinguish regular
attachments from inline images (some clients set Content-ID on both)
- Content-ID and content_type sanitized against CRLF header injection
- Size preflight before downloading original attachments
- Remote filename sanitization (not rejection) for sender-controlled names
- Walker does not recurse into hydratable parts (e.g., message/rfc822)
Add dry-run mode to gws events +renew and gws events +subscribe commands.
When --dry-run is specified, the commands print what actions would be
taken without making any API calls. This allows agents to simulate
requests and learn without reaching the server.
Replace flow sequences (bins: ["gws"], skills: [...]) with block-style
sequences in all generated SKILL.md frontmatter templates.
Flow sequences are valid YAML but rejected by strictyaml, which the
Agent Skills reference implementation (agentskills validate) uses to
parse frontmatter. This caused all 93 generated skills to fail
validation.
Also adds unit tests verifying that service, shared, persona, and
recipe skill templates produce block-style sequences only.
Fixes#521
* refactor: replace manual arg parsing in auth commands with clap
- Add ScopeMode enum (Default, Readonly, Full, Custom) for type-safe
scope selection
- Build auth_command() with clap subcommands: login, setup, status,
export, logout
- Replace manual --help check and match dispatch in handle_auth_command
with clap subcommand routing
- Replace two-pass manual parsing in handle_login (services extraction)
and resolve_scopes (scopes/readonly/full flags) with single clap parse
- Add --unmasked flag to export subcommand via clap
- Preserve run_login(&[]) public API for setup.rs compatibility
- Update all 63 tests to use ScopeMode-based API
* refactor: extract parse_login_args helper to deduplicate scope/services parsing
* fix: make --readonly, --full, --scopes mutually exclusive via clap conflicts
* fix: filter empty strings from custom scopes parsing
* refactor: extract build_login_subcommand to eliminate fragile .expect() lookup
---------
Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
* fix(setup): handle --help/-h before launching setup wizard
Check for --help/-h at the top of run_setup() before any work that
requires gcloud. Previously, running 'gws auth setup --help' would
launch the full setup flow.
Fixes#280
* refactor: extract SETUP_USAGE into const per review
* refactor: replace manual setup arg parsing with clap
Use clap::Command for gws auth setup argument parsing instead of manual
while-loop parser. This gives us:
- Automatic --help/-h handling (no manual check needed)
- Proper error messages for unknown flags
- Consistent with other clap usage in the codebase
- Help text stays in sync with actual arguments
* fix: distinguish --help (clean exit) from invalid flags (error)
Change parse_setup_args return type from Result<SetupOptions, ()> to
Result<Option<SetupOptions>, GwsError> so that:
- Ok(Some(opts)) = successful parse
- Ok(None) = --help/--version displayed (clean exit, code 0)
- Err(GwsError) = invalid flags (error exit, non-zero code)
* fix: propagate e.print() IO errors instead of silently ignoring
---------
Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
* fix(auth): propagate errors when token directory creation/permissions fail
Previously, failures to create the token directory or set its permissions
were silently ignored using 'let _ = ...'. This could lead to confusing
errors later or security issues if permissions were left insecure.
Now properly propagates errors from:
- tokio::fs::create_dir_all()
- std::fs::set_permissions() (on Unix)
Also sanitizes the path in error messages to prevent terminal escape
sequence injection, aligned with codebase security practices.
* fix(auth): use spawn_blocking for set_permissions to avoid blocking async runtime
Following the reviewer suggestion, std::fs::set_permissions is now executed
via tokio::task::spawn_blocking to avoid potentially blocking the async
runtime thread, which can cause performance issues or deadlocks under load.
* fix(auth): use tokio::fs::set_permissions instead of spawn_blocking
Simplifies the code by using Tokio's native async set_permissions,
removing the need for manual thread spawning.
* feat: handle SIGTERM in +watch and +subscribe for clean shutdown
Add shared shutdown_signal() helper that merges SIGINT and SIGTERM
into a single future. Replace tokio::signal::ctrl_c() in both watch
and subscribe pull loops so they exit cleanly under Kubernetes,
Docker, and systemd.
On non-Unix platforms, only SIGINT (Ctrl+C) is handled.
* fix: register SIGTERM handler once via persistent background task
Use OnceLock + tokio::sync::Notify so the signal handler stays active
for the process lifetime. Eliminates the race window between loop
iterations where a SIGTERM would bypass the handler.
* fix: graceful fallback when SIGTERM registration fails
Replace expect() with match: if signal(SIGTERM) fails, log a warning
and fall back to SIGINT-only. Prevents silent task death that would
hang all shutdown_signal() callers indefinitely.
* fix: prevent spurious shutdown from ignored ctrl_c errors
Use Ok(_) pattern matching in select! branches and expect() for
standalone ctrl_c().await calls. Previously .ok() silently swallowed
errors, causing notify_waiters() to fire immediately.
* fix: handle ctrl_c error in select! to avoid losing SIGINT branch
Bind the full Result from ctrl_c() and expect() on it instead of
pattern matching Ok(_), which silently dropped the branch on Err.
---------
Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
* feat(gmail): auto-populate From with display name from send-as settings
Fetch the user's send-as identities via /users/me/settings/sendAs to set
the From header with a display name in all mail helpers, matching Gmail
web client behavior. The gmail.modify scope already covers this endpoint.
Three resolution cases:
- No --from: use the default send-as identity (display name + email)
- --from with bare email: enrich from send-as list if the alias exists
- --from with display name: use as-is, skip the API call
For Workspace accounts where the primary address inherits its display
name from the organization directory (sendAs returns empty displayName),
falls back to the People API to fetch the profile name. This requires
the userinfo.profile scope, which is now included in the identity scopes
that auth login always requests. Degrades gracefully with a tip if the
scope hasn't been granted yet.
Introduces build_api_error, a shared helper that parses Google API JSON
error responses (extracting message, reason, and enable URL), matching
the executor's handle_error_response pattern. Used by all four Gmail API
functions. All error messages printed to stderr are sanitized via
sanitize_for_terminal.
In +send, auth uses the discovery doc scopes rather than hardcoding
gmail.modify, preserving compatibility with narrower send-only OAuth
setups. In reply-all, the profile endpoint is always called for
self-email dedup since the primary address may differ from the send-as
alias.
* fix(gmail): handle reply-all to own message correctly
When replying-all to a message you sent, the original sender (you) was
excluded from To, leaving it empty and producing an error. Gmail web
handles this by using the original To recipients as reply targets.
Detect self-reply by checking if the original From matches the user's
primary email or send-as alias, then swap the candidate logic:
- Self-reply: To = original To, CC = original CC
- Normal reply: To = Reply-To or From, CC = original To + CC
The +send subcommand defined its own "attachment" arg in addition to the
"attach" arg already provided by common_mail_args. Since parse_attachments
reads "attach", the duplicate "attachment" arg was dead — +send --attachment
was silently accepted by clap but the value was never read.